From c62234e1caf4a71ce720ecb6b098cd0f26f89338 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Mon, 20 Oct 2025 17:55:52 +0200 Subject: [PATCH] docker setup # Conflicts: # .gitignore # backend/vite.config.js # frontend/package-lock.json --- .devcontainer/README.md | 207 +++ .devcontainer/devcontainer.json | 94 ++ .gitignore | 77 +- DOCKER-SETUP.md | 296 ++++ README.md | 9 +- backend/composer.json | 4 +- backend/composer.lock | 1497 ++++++++++------- backend/package-lock.json | 320 ++-- backend/vite.config.js | 64 +- docker-compose.yml | 162 ++ dot-line-system/.gitignore | 24 + .../.history/index_20250515080205.html | 86 + .../.history/index_20250515093839.html | 86 + .../.history/index_20250522083017.html | 125 ++ .../.history/index_20250522083042.html | 92 + .../.history/index_20250522083155.html | 93 + .../.history/index_20250522085953.html | 28 + .../.history/index_20250522090004.html | 28 + .../.history/index_20250522090012.html | 25 + .../.history/index_20250522090611.html | 26 + .../.history/index_20250522090656.html | 26 + .../.history/index_20250522090717.html | 27 + .../.history/index_20250522090730.html | 26 + .../.history/index_20250522095109.html | 29 + .../.history/index_20250522114322.html | 29 + .../.history/index_20250522114359.html | 30 + .../.history/index_20250522114425.html | 29 + .../.history/index_20250522114819.html | 29 + .../.history/index_20250522130240.html | 29 + .../.history/index_20250522130258.html | 29 + .../.history/index_20250522130455.html | 29 + .../.history/index_20250522130505.html | 25 + .../.history/index_20250522130518.html | 25 + .../.history/index_20250522130539.html | 25 + .../.history/index_20250522130613.html | 25 + .../.history/index_20250522132851.html | 25 + .../.history/index_20250522133036.html | 25 + .../.history/index_20250522222202.html | 25 + .../.history/index_20250522222233.html | 25 + .../.history/package_20250515093313.json | 15 + .../.history/package_20250515093412.json | 20 + .../.history/package_20250515093415.json | 20 + .../.history/readme_20250521221324.md | 0 .../.history/readme_20250521221329.md | 1 + .../.history/readme_20250521221343.md | 2 + .../.history/readme_20250521221558.md | 3 + .../.history/readme_20250521221603.md | 2 + .../.history/readme_20250521221932.md | 4 + .../.history/readme_20250522081835.md | 7 + .../.history/readme_20250522081843.md | 8 + ...nnectedDotsVisualization_20250515080205.ts | 545 ++++++ ...nnectedDotsVisualization_20250515094002.ts | 545 ++++++ ...nnectedDotsVisualization_20250515094010.ts | 545 ++++++ ...nnectedDotsVisualization_20250515094030.ts | 545 ++++++ ...nnectedDotsVisualization_20250515094033.ts | 545 ++++++ ...nnectedDotsVisualization_20250515094036.ts | 545 ++++++ ...nnectedDotsVisualization_20250515094059.ts | 545 ++++++ ...nnectedDotsVisualization_20250515094215.ts | 545 ++++++ ...nnectedDotsVisualization_20250515094221.ts | 545 ++++++ ...nnectedDotsVisualization_20250515094419.ts | 545 ++++++ ...nnectedDotsVisualization_20250522091556.ts | 553 ++++++ ...nnectedDotsVisualization_20250522091605.ts | 553 ++++++ ...nnectedDotsVisualization_20250522091611.ts | 551 ++++++ ...nnectedDotsVisualization_20250522091723.ts | 561 ++++++ ...nnectedDotsVisualization_20250522091732.ts | 561 ++++++ ...nnectedDotsVisualization_20250522091747.ts | 561 ++++++ ...nnectedDotsVisualization_20250522091858.ts | 565 +++++++ ...nnectedDotsVisualization_20250522091957.ts | 570 +++++++ ...nnectedDotsVisualization_20250522092004.ts | 570 +++++++ ...nnectedDotsVisualization_20250522092036.ts | 570 +++++++ ...nnectedDotsVisualization_20250522092041.ts | 570 +++++++ ...nnectedDotsVisualization_20250522092303.ts | 631 +++++++ ...nnectedDotsVisualization_20250522092543.ts | 632 +++++++ ...nnectedDotsVisualization_20250522092725.ts | 641 +++++++ ...nnectedDotsVisualization_20250522092810.ts | 647 +++++++ ...nnectedDotsVisualization_20250522092817.ts | 648 +++++++ ...nnectedDotsVisualization_20250522092936.ts | 648 +++++++ ...nnectedDotsVisualization_20250522093038.ts | 650 +++++++ ...nnectedDotsVisualization_20250522093141.ts | 583 +++++++ ...nnectedDotsVisualization_20250522093202.ts | 583 +++++++ ...nnectedDotsVisualization_20250522093316.ts | 500 ++++++ ...nnectedDotsVisualization_20250522093434.ts | 507 ++++++ ...nnectedDotsVisualization_20250522093501.ts | 504 ++++++ ...nnectedDotsVisualization_20250522093509.ts | 509 ++++++ ...nnectedDotsVisualization_20250522093827.ts | 498 ++++++ ...nnectedDotsVisualization_20250522094341.ts | 502 ++++++ ...nnectedDotsVisualization_20250522094352.ts | 502 ++++++ ...nnectedDotsVisualization_20250522094405.ts | 502 ++++++ ...nnectedDotsVisualization_20250522094427.ts | 502 ++++++ ...nnectedDotsVisualization_20250522094438.ts | 499 ++++++ ...nnectedDotsVisualization_20250522094441.ts | 499 ++++++ ...nnectedDotsVisualization_20250522094711.ts | 503 ++++++ ...nnectedDotsVisualization_20250522094716.ts | 503 ++++++ ...nnectedDotsVisualization_20250522094721.ts | 503 ++++++ ...nnectedDotsVisualization_20250522094758.ts | 504 ++++++ ...nnectedDotsVisualization_20250522094923.ts | 504 ++++++ ...nnectedDotsVisualization_20250522094927.ts | 504 ++++++ ...nnectedDotsVisualization_20250522094944.ts | 505 ++++++ ...nnectedDotsVisualization_20250522095334.ts | 503 ++++++ ...nnectedDotsVisualization_20250522095400.ts | 505 ++++++ ...nnectedDotsVisualization_20250522095543.ts | 505 ++++++ ...nnectedDotsVisualization_20250522101227.ts | 516 ++++++ ...nnectedDotsVisualization_20250522102503.ts | 513 ++++++ ...nnectedDotsVisualization_20250522102521.ts | 513 ++++++ ...nnectedDotsVisualization_20250522102742.ts | 515 ++++++ ...nnectedDotsVisualization_20250522103253.ts | 513 ++++++ ...nnectedDotsVisualization_20250522104846.ts | 514 ++++++ ...nnectedDotsVisualization_20250522104938.ts | 513 ++++++ ...nnectedDotsVisualization_20250522105020.ts | 519 ++++++ ...nnectedDotsVisualization_20250522105023.ts | 519 ++++++ ...nnectedDotsVisualization_20250522105055.ts | 522 ++++++ ...nnectedDotsVisualization_20250522105115.ts | 522 ++++++ ...nnectedDotsVisualization_20250522105129.ts | 522 ++++++ ...nnectedDotsVisualization_20250522105224.ts | 522 ++++++ ...nnectedDotsVisualization_20250522105229.ts | 522 ++++++ ...nnectedDotsVisualization_20250522105242.ts | 522 ++++++ ...nnectedDotsVisualization_20250522105702.ts | 518 ++++++ ...nnectedDotsVisualization_20250522105928.ts | 518 ++++++ ...nnectedDotsVisualization_20250522110035.ts | 518 ++++++ ...nnectedDotsVisualization_20250522110335.ts | 520 ++++++ ...nnectedDotsVisualization_20250522110431.ts | 520 ++++++ ...nnectedDotsVisualization_20250522110437.ts | 520 ++++++ ...nnectedDotsVisualization_20250522110859.ts | 520 ++++++ ...nnectedDotsVisualization_20250522110953.ts | 520 ++++++ ...nnectedDotsVisualization_20250522111000.ts | 520 ++++++ ...nnectedDotsVisualization_20250522111003.ts | 520 ++++++ ...nnectedDotsVisualization_20250522111016.ts | 520 ++++++ ...nnectedDotsVisualization_20250522111039.ts | 520 ++++++ ...nnectedDotsVisualization_20250522111311.ts | 520 ++++++ ...nnectedDotsVisualization_20250522111404.ts | 520 ++++++ ...nnectedDotsVisualization_20250522111448.ts | 521 ++++++ ...nnectedDotsVisualization_20250522111457.ts | 521 ++++++ ...nnectedDotsVisualization_20250522111743.ts | 510 ++++++ ...nnectedDotsVisualization_20250522111904.ts | 510 ++++++ ...nnectedDotsVisualization_20250522111911.ts | 510 ++++++ ...nnectedDotsVisualization_20250522111915.ts | 510 ++++++ ...nnectedDotsVisualization_20250522111956.ts | 510 ++++++ ...nnectedDotsVisualization_20250522112131.ts | 514 ++++++ ...nnectedDotsVisualization_20250522112202.ts | 512 ++++++ ...nnectedDotsVisualization_20250522112233.ts | 513 ++++++ ...nnectedDotsVisualization_20250522112657.ts | 513 ++++++ ...nnectedDotsVisualization_20250522112705.ts | 513 ++++++ ...nnectedDotsVisualization_20250522113133.ts | 534 ++++++ ...nnectedDotsVisualization_20250522113224.ts | 529 ++++++ ...nnectedDotsVisualization_20250522113610.ts | 556 ++++++ ...nnectedDotsVisualization_20250522113613.ts | 531 ++++++ ...nnectedDotsVisualization_20250522113628.ts | 540 ++++++ ...nnectedDotsVisualization_20250522113832.ts | 538 ++++++ ...nnectedDotsVisualization_20250522113857.ts | 529 ++++++ ...nnectedDotsVisualization_20250522130644.ts | 529 ++++++ ...nnectedDotsVisualization_20250522131005.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131150.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131159.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131202.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131206.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131208.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131212.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131413.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131415.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131457.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131501.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131509.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131516.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131534.ts | 512 ++++++ ...nnectedDotsVisualization_20250522131713.ts | 513 ++++++ ...nnectedDotsVisualization_20250522131724.ts | 513 ++++++ ...nnectedDotsVisualization_20250522131749.ts | 513 ++++++ ...nnectedDotsVisualization_20250522131803.ts | 513 ++++++ ...nnectedDotsVisualization_20250522131811.ts | 513 ++++++ ...nnectedDotsVisualization_20250522131917.ts | 513 ++++++ ...nnectedDotsVisualization_20250522131950.ts | 513 ++++++ ...nnectedDotsVisualization_20250522131954.ts | 513 ++++++ ...nnectedDotsVisualization_20250522131959.ts | 513 ++++++ ...nnectedDotsVisualization_20250522132013.ts | 513 ++++++ ...nnectedDotsVisualization_20250522132035.ts | 513 ++++++ ...nnectedDotsVisualization_20250522132039.ts | 513 ++++++ ...nnectedDotsVisualization_20250522132042.ts | 513 ++++++ ...nnectedDotsVisualization_20250522132047.ts | 513 ++++++ ...nnectedDotsVisualization_20250522132050.ts | 513 ++++++ ...nnectedDotsVisualization_20250522132342.ts | 519 ++++++ ...nnectedDotsVisualization_20250522132517.ts | 520 ++++++ ...nnectedDotsVisualization_20250522132606.ts | 519 ++++++ ...nnectedDotsVisualization_20250522133122.ts | 519 ++++++ ...nnectedDotsVisualization_20250522133129.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151345.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151350.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151359.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151406.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151421.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151436.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151449.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151453.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151603.ts | 519 ++++++ ...nnectedDotsVisualization_20250522151629.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153324.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153329.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153337.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153342.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153345.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153407.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153429.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153437.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153447.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153457.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153502.ts | 519 ++++++ ...nnectedDotsVisualization_20250522153506.ts | 519 ++++++ ...nnectedDotsVisualization_20250522222207.ts | 519 ++++++ ...nnectedDotsVisualization_20250522222913.ts | 519 ++++++ ...nnectedDotsVisualization_20250522222945.ts | 519 ++++++ ...nnectedDotsVisualization_20250522222952.ts | 519 ++++++ ...nnectedDotsVisualization_20250522231943.ts | 519 ++++++ ...nnectedDotsVisualization_20250522232002.ts | 519 ++++++ .../.history/src/main_20250515080205.ts | 120 ++ .../.history/src/main_20250515093647.ts | 120 ++ .../.history/src/main_20250515093655.ts | 120 ++ .../.history/src/main_20250515093704.ts | 120 ++ .../.history/src/main_20250515093739.ts | 121 ++ .../.history/src/main_20250515093749.ts | 121 ++ .../.history/src/main_20250515093755.ts | 121 ++ .../.history/src/main_20250515093801.ts | 121 ++ .../.history/src/main_20250515093807.ts | 121 ++ .../.history/src/main_20250522082517.ts | 198 +++ .../.history/src/main_20250522082653.ts | 66 + .../.history/src/main_20250522082725.ts | 61 + .../.history/src/main_20250522082823.ts | 61 + .../.history/src/main_20250522083059.ts | 93 + .../.history/src/main_20250522084926.ts | 93 + .../.history/src/main_20250522085045.ts | 93 + .../.history/src/main_20250522085108.ts | 93 + .../.history/src/main_20250522085117.ts | 93 + .../.history/src/main_20250522085138.ts | 93 + .../.history/src/main_20250522085307.ts | 93 + .../.history/src/main_20250522085525.ts | 93 + .../.history/src/main_20250522085554.ts | 93 + .../.history/src/main_20250522085614.ts | 93 + .../.history/src/main_20250522085624.ts | 93 + .../.history/src/main_20250522085712.ts | 93 + .../.history/src/main_20250522085720.ts | 93 + .../.history/src/main_20250522085742.ts | 93 + .../.history/src/main_20250522085804.ts | 93 + .../.history/src/main_20250522085818.ts | 93 + .../.history/src/main_20250522085841.ts | 93 + .../.history/src/main_20250522085845.ts | 93 + .../.history/src/main_20250522085849.ts | 93 + .../.history/src/main_20250522085852.ts | 93 + .../.history/src/main_20250522090305.ts | 93 + .../.history/src/main_20250522090350.ts | 93 + .../.history/src/main_20250522090417.ts | 93 + .../.history/src/main_20250522090431.ts | 93 + .../.history/src/main_20250522090436.ts | 93 + .../.history/src/main_20250522091026.ts | 93 + .../.history/src/main_20250522091044.ts | 93 + .../.history/src/main_20250522091103.ts | 93 + .../.history/src/main_20250522091117.ts | 93 + .../.history/src/main_20250522091134.ts | 93 + .../.history/src/main_20250522091203.ts | 93 + .../.history/src/main_20250522091258.ts | 93 + .../.history/src/main_20250522102605.ts | 93 + .../.history/src/main_20250522102612.ts | 93 + .../.history/src/main_20250522102630.ts | 93 + .../.history/src/main_20250522102634.ts | 93 + .../.history/src/main_20250522102637.ts | 93 + .../.history/src/main_20250522103603.ts | 93 + .../.history/src/main_20250522103730.ts | 93 + .../.history/src/main_20250522103753.ts | 93 + .../.history/src/main_20250522104008.ts | 93 + .../.history/src/main_20250522104033.ts | 93 + .../.history/src/main_20250522104601.ts | 93 + .../.history/src/main_20250522130745.ts | 118 ++ .../.history/src/main_20250522131101.ts | 284 ++++ .../.history/src/main_20250522131108.ts | 284 ++++ .../.history/src/main_20250522131112.ts | 284 ++++ .../.history/src/main_20250522131119.ts | 284 ++++ .../.history/src/main_20250522131131.ts | 284 ++++ .../.history/src/main_20250522131843.ts | 284 ++++ .../.history/src/main_20250522131848.ts | 284 ++++ .../.history/src/main_20250522131851.ts | 284 ++++ .../.history/src/main_20250522131901.ts | 284 ++++ .../.history/src/main_20250522131929.ts | 284 ++++ .../.history/src/main_20250522131937.ts | 284 ++++ .../.history/src/main_20250522131944.ts | 284 ++++ .../.history/src/main_20250522132110.ts | 284 ++++ .../.history/src/main_20250522132123.ts | 284 ++++ .../.history/src/main_20250522132137.ts | 284 ++++ .../.history/src/main_20250522132218.ts | 283 ++++ .../.history/src/main_20250522132225.ts | 284 ++++ .../.history/src/main_20250522132350.ts | 284 ++++ .../.history/src/main_20250522132401.ts | 284 ++++ .../.history/src/main_20250522132525.ts | 284 ++++ .../.history/src/main_20250522132532.ts | 284 ++++ .../.history/src/main_20250522133155.ts | 284 ++++ .../.history/src/main_20250522133219.ts | 284 ++++ .../.history/src/main_20250522133230.ts | 284 ++++ .../.history/src/main_20250522150734.ts | 284 ++++ .../.history/src/main_20250522150803.ts | 284 ++++ .../.history/src/main_20250522150809.ts | 284 ++++ .../.history/src/main_20250522222205.ts | 284 ++++ .../.history/src/main_20250522222757.ts | 286 ++++ .../.history/src/main_20250522223705.ts | 286 ++++ .../.history/src/main_20250522224138.ts | 303 ++++ .../.history/src/main_20250522224154.ts | 303 ++++ .../.history/src/main_20250522224417.ts | 303 ++++ .../.history/src/main_20250522224419.ts | 303 ++++ .../.history/src/main_20250522224450.ts | 303 ++++ .../.history/src/main_20250522224516.ts | 303 ++++ .../.history/src/main_20250522224540.ts | 303 ++++ .../.history/src/main_20250522224607.ts | 303 ++++ .../.history/src/main_20250522224641.ts | 303 ++++ .../.history/src/main_20250522224710.ts | 303 ++++ .../.history/src/main_20250522224733.ts | 303 ++++ .../.history/src/main_20250522224743.ts | 303 ++++ .../.history/src/main_20250522224813.ts | 303 ++++ .../.history/src/main_20250522224826.ts | 303 ++++ .../.history/src/main_20250522224845.ts | 303 ++++ .../.history/src/main_20250522225100.ts | 303 ++++ .../.history/src/main_20250522225110.ts | 303 ++++ .../.history/src/main_20250522225139.ts | 303 ++++ .../.history/src/main_20250522231822.ts | 303 ++++ .../.history/src/main_20250523080429.ts | 305 ++++ .../.history/src/main_20250523080510.ts | 321 ++++ .../.history/src/main_20250523080542.ts | 321 ++++ .../.history/src/main_20250523080609.ts | 305 ++++ .../.history/src/main_20250523080934.ts | 320 ++++ .../.history/src/style_20250515080205.css | 0 .../.history/src/style_20250522085955.css | 67 + .../.history/src/style_20250522090021.css | 68 + .../.history/src/style_20250522090641.css | 74 + .../.history/src/style_20250522090651.css | 74 + .../.history/src/style_20250522090747.css | 77 + .../.history/src/style_20250522090807.css | 74 + .../.history/src/style_20250522090836.css | 76 + .../.history/src/style_20250522090841.css | 77 + .../.history/src/style_20250522090850.css | 77 + .../.history/src/style_20250522090854.css | 77 + .../.history/src/style_20250522090906.css | 77 + .../.history/src/style_20250522090923.css | 77 + .../.history/src/style_20250522090926.css | 77 + .../.history/src/style_20250522090936.css | 78 + .../.history/src/style_20250522090943.css | 78 + .../.history/src/style_20250522093837.css | 112 ++ .../.history/src/style_20250522093851.css | 112 ++ .../.history/src/style_20250522093915.css | 113 ++ .../.history/src/style_20250522093919.css | 113 ++ .../.history/src/style_20250522093925.css | 113 ++ .../.history/src/style_20250522095000.css | 114 ++ .../.history/src/style_20250522095014.css | 115 ++ .../.history/src/style_20250522095017.css | 115 ++ .../.history/src/style_20250522095208.css | 119 ++ .../.history/src/style_20250522095223.css | 119 ++ .../.history/src/style_20250522095304.css | 119 ++ .../.history/src/style_20250522095307.css | 119 ++ .../.history/src/style_20250522095311.css | 119 ++ .../.history/src/style_20250522095317.css | 119 ++ .../.history/src/style_20250522095407.css | 120 ++ .../.history/src/style_20250522095421.css | 121 ++ .../.history/src/style_20250522095426.css | 121 ++ .../.history/src/style_20250522095430.css | 121 ++ .../.history/src/style_20250522095436.css | 121 ++ .../.history/src/style_20250522095552.css | 122 ++ .../.history/src/style_20250522095554.css | 122 ++ .../.history/src/style_20250522095604.css | 123 ++ .../.history/src/style_20250522095617.css | 122 ++ .../.history/src/style_20250522100913.css | 122 ++ .../.history/src/style_20250522100924.css | 122 ++ .../.history/src/style_20250522100934.css | 122 ++ .../.history/src/style_20250522100943.css | 122 ++ .../.history/src/style_20250522101311.css | 127 ++ .../.history/src/style_20250522101328.css | 127 ++ .../.history/src/style_20250522101336.css | 128 ++ .../.history/src/style_20250522101350.css | 132 ++ .../.history/src/style_20250522102626.css | 133 ++ .../.history/src/style_20250522102642.css | 132 ++ .../.history/src/style_20250522103246.css | 133 ++ .../.history/src/style_20250522103250.css | 133 ++ .../.history/src/style_20250522103312.css | 132 ++ .../.history/src/style_20250522103342.css | 140 ++ .../.history/src/style_20250522103350.css | 140 ++ .../.history/src/style_20250522103407.css | 134 ++ .../.history/src/style_20250522103422.css | 135 ++ .../.history/src/style_20250522103426.css | 134 ++ .../.history/src/style_20250522103431.css | 134 ++ .../.history/src/style_20250522103440.css | 135 ++ .../.history/src/style_20250522104922.css | 147 ++ .../.history/src/style_20250522105207.css | 148 ++ .../.history/src/style_20250522105209.css | 148 ++ .../.history/src/style_20250522105222.css | 136 ++ .../.history/src/style_20250522110436.css | 136 ++ .../.history/src/style_20250522112314.css | 140 ++ .../.history/src/style_20250522112321.css | 140 ++ .../.history/src/style_20250522112533.css | 140 ++ .../.history/src/style_20250522112544.css | 140 ++ .../.history/src/style_20250522112548.css | 140 ++ .../.history/src/style_20250522112553.css | 140 ++ .../.history/src/style_20250522112559.css | 140 ++ .../.history/src/style_20250522112603.css | 140 ++ .../.history/src/style_20250522112612.css | 140 ++ .../.history/src/style_20250522112618.css | 140 ++ .../.history/src/style_20250522112638.css | 138 ++ .../.history/src/style_20250522112644.css | 138 ++ .../.history/src/style_20250522112711.css | 138 ++ .../.history/src/style_20250522112732.css | 138 ++ .../.history/src/style_20250522113350.css | 140 ++ .../.history/src/style_20250522113403.css | 141 ++ .../.history/src/style_20250522113810.css | 142 ++ .../.history/src/style_20250522114215.css | 169 ++ .../.history/src/style_20250522114227.css | 169 ++ .../.history/src/style_20250522114320.css | 169 ++ .../.history/src/style_20250522114407.css | 169 ++ .../.history/src/style_20250522114517.css | 169 ++ .../.history/src/style_20250522114533.css | 171 ++ .../.history/src/style_20250522114538.css | 169 ++ .../.history/src/style_20250522114616.css | 168 ++ .../.history/src/style_20250522114639.css | 173 ++ .../.history/src/style_20250522114649.css | 168 ++ .../.history/src/style_20250522114653.css | 168 ++ .../.history/src/style_20250522114816.css | 171 ++ .../.history/src/style_20250522114848.css | 167 ++ .../.history/src/style_20250522114856.css | 167 ++ .../.history/src/style_20250522114917.css | 167 ++ .../.history/src/style_20250522114920.css | 167 ++ .../.history/src/style_20250522114934.css | 167 ++ .../.history/src/style_20250522114947.css | 167 ++ .../.history/src/style_20250522115024.css | 168 ++ .../.history/src/style_20250522115045.css | 168 ++ .../.history/src/style_20250522115052.css | 168 ++ .../.history/src/style_20250522115107.css | 169 ++ .../.history/src/style_20250522115113.css | 169 ++ .../.history/src/style_20250522115119.css | 169 ++ .../.history/src/style_20250522115124.css | 169 ++ .../.history/src/style_20250522115129.css | 169 ++ .../.history/src/style_20250522115145.css | 169 ++ .../.history/src/style_20250522115153.css | 169 ++ .../.history/src/style_20250522115156.css | 169 ++ .../.history/src/style_20250522115158.css | 169 ++ .../.history/src/style_20250522115353.css | 177 ++ .../.history/src/style_20250522115410.css | 177 ++ .../.history/src/style_20250522115441.css | 178 ++ .../.history/src/style_20250522115453.css | 178 ++ .../.history/src/style_20250522115508.css | 179 ++ .../.history/src/style_20250522115517.css | 179 ++ .../.history/src/style_20250522115552.css | 179 ++ .../.history/src/style_20250522115602.css | 179 ++ .../.history/src/style_20250522115619.css | 179 ++ .../.history/src/style_20250522115746.css | 181 ++ .../.history/src/style_20250522115820.css | 181 ++ .../.history/src/style_20250522130235.css | 181 ++ .../.history/src/style_20250522130252.css | 181 ++ .../.history/src/style_20250522130310.css | 181 ++ .../.history/src/style_20250522130323.css | 179 ++ .../.history/src/style_20250522130438.css | 179 ++ .../.history/src/style_20250522130450.css | 179 ++ .../.history/src/style_20250522130457.css | 179 ++ .../.history/src/style_20250522130520.css | 179 ++ .../.history/src/style_20250522130537.css | 179 ++ .../.history/src/style_20250522130552.css | 179 ++ .../.history/src/style_20250522130614.css | 179 ++ .../.history/src/style_20250522130959.css | 198 +++ .../.history/src/style_20250522131003.css | 204 +++ .../.history/src/style_20250522131623.css | 202 +++ .../.history/src/style_20250522132147.css | 202 +++ .../.history/src/style_20250522132926.css | 202 +++ .../.history/src/style_20250522132930.css | 202 +++ .../.history/src/style_20250522132942.css | 202 +++ .../.history/src/style_20250522132949.css | 202 +++ .../.history/src/style_20250522132956.css | 202 +++ .../.history/src/style_20250522133003.css | 203 +++ .../.history/src/style_20250522133006.css | 203 +++ .../.history/src/style_20250522133030.css | 204 +++ .../.history/src/style_20250522133046.css | 203 +++ .../.history/src/style_20250522133236.css | 203 +++ .../.history/src/style_20250522133241.css | 203 +++ .../.history/src/style_20250522133243.css | 203 +++ .../.history/src/style_20250522133246.css | 203 +++ .../.history/src/style_20250522133248.css | 203 +++ .../.history/src/style_20250522133254.css | 203 +++ .../.history/src/style_20250522133352.css | 203 +++ .../.history/src/style_20250522133401.css | 199 +++ .../.history/src/style_20250522133408.css | 199 +++ .../.history/src/style_20250522133412.css | 199 +++ .../.history/src/style_20250522153124.css | 199 +++ .../.history/src/style_20250522153129.css | 199 +++ .../.history/src/style_20250522153134.css | 199 +++ .../.history/src/style_20250522153141.css | 199 +++ .../.history/src/style_20250522153151.css | 199 +++ .../.history/src/style_20250522153532.css | 199 +++ .../.history/src/style_20250522153557.css | 200 +++ .../.history/src/style_20250522153600.css | 200 +++ .../.history/src/style_20250522223323.css | 200 +++ .../.history/src/style_20250522230441.css | 200 +++ .../.history/src/style_20250522232137.css | 200 +++ .../.history/src/style_20250522232205.css | 200 +++ .../.history/src/style_20250522232242.css | 205 +++ .../.history/src/style_20250522232300.css | 205 +++ .../.history/src/style_20250522232311.css | 205 +++ .../.history/src/style_20250522232332.css | 205 +++ .../.history/src/style_20250522232342.css | 205 +++ .../.history/src/style_20250522232352.css | 205 +++ .../.history/src/style_20250522232356.css | 200 +++ .../.history/src/style_20250522232403.css | 200 +++ .../.history/src/style_20250522232543.css | 200 +++ .../.history/src/style_20250522232557.css | 200 +++ .../.history/src/style_20250522232605.css | 200 +++ .../.history/src/style_20250522232609.css | 200 +++ .../.history/src/style_20250522232619.css | 200 +++ .../.history/src/style_20250523080846.css | 203 +++ .../.history/src/style_20250523080941.css | 203 +++ .../.history/src/style_20250523081026.css | 203 +++ .../.history/src/style_20250523081040.css | 203 +++ dot-line-system/bun.lock | 128 ++ dot-line-system/index.html | 25 + dot-line-system/package-lock.json | 1006 +++++++++++ dot-line-system/package.json | 20 + dot-line-system/public/ScrollTrigger.min.js | 11 + .../public/ScrollTrigger.min.js.map | 1 + dot-line-system/public/gsap.min.js | 11 + dot-line-system/public/gsap.min.js.map | 1 + dot-line-system/public/images/0_2.png | Bin 0 -> 28961 bytes dot-line-system/public/images/0_3.png | Bin 0 -> 42619 bytes dot-line-system/public/images/disco.png | Bin 0 -> 25986 bytes dot-line-system/public/images/entspannung.png | Bin 0 -> 284110 bytes dot-line-system/public/images/familie.png | Bin 0 -> 28644 bytes dot-line-system/public/images/familie2.png | Bin 0 -> 348425 bytes dot-line-system/public/images/feier.png | Bin 0 -> 183150 bytes dot-line-system/public/images/gpt.png | Bin 0 -> 53546 bytes dot-line-system/public/images/grosseltern.png | Bin 0 -> 24485 bytes dot-line-system/public/images/hochzeit.png | Bin 0 -> 34850 bytes .../public/images/kindergeburtstag.png | Bin 0 -> 114893 bytes dot-line-system/public/images/kinobesuch.png | Bin 0 -> 32990 bytes dot-line-system/public/images/klasse.png | Bin 0 -> 352014 bytes dot-line-system/public/images/oma.png | Bin 0 -> 30789 bytes dot-line-system/public/images/pferd.png | Bin 0 -> 280884 bytes dot-line-system/public/images/see.png | Bin 0 -> 131002 bytes dot-line-system/public/images/sonntag.png | Bin 0 -> 645343 bytes dot-line-system/public/images/work.png | Bin 0 -> 26783 bytes dot-line-system/public/vite.svg | 1 + dot-line-system/readme.md | 8 + .../src/ConnectedDotsVisualization.ts | 519 ++++++ dot-line-system/src/main.ts | 320 ++++ dot-line-system/src/style.css | 203 +++ dot-line-system/src/typescript.svg | 1 + dot-line-system/src/vite-env.d.ts | 1 + dot-line-system/tsconfig.json | 24 + frontend/README.md | 7 + frontend/package-lock.json | 77 +- frontend/quasar.config.js | 3 +- thats-me.test.code-workspace | 25 +- 546 files changed, 141382 insertions(+), 757 deletions(-) create mode 100644 .devcontainer/README.md create mode 100644 .devcontainer/devcontainer.json create mode 100644 DOCKER-SETUP.md create mode 100644 docker-compose.yml create mode 100644 dot-line-system/.gitignore create mode 100644 dot-line-system/.history/index_20250515080205.html create mode 100644 dot-line-system/.history/index_20250515093839.html create mode 100644 dot-line-system/.history/index_20250522083017.html create mode 100644 dot-line-system/.history/index_20250522083042.html create mode 100644 dot-line-system/.history/index_20250522083155.html create mode 100644 dot-line-system/.history/index_20250522085953.html create mode 100644 dot-line-system/.history/index_20250522090004.html create mode 100644 dot-line-system/.history/index_20250522090012.html create mode 100644 dot-line-system/.history/index_20250522090611.html create mode 100644 dot-line-system/.history/index_20250522090656.html create mode 100644 dot-line-system/.history/index_20250522090717.html create mode 100644 dot-line-system/.history/index_20250522090730.html create mode 100644 dot-line-system/.history/index_20250522095109.html create mode 100644 dot-line-system/.history/index_20250522114322.html create mode 100644 dot-line-system/.history/index_20250522114359.html create mode 100644 dot-line-system/.history/index_20250522114425.html create mode 100644 dot-line-system/.history/index_20250522114819.html create mode 100644 dot-line-system/.history/index_20250522130240.html create mode 100644 dot-line-system/.history/index_20250522130258.html create mode 100644 dot-line-system/.history/index_20250522130455.html create mode 100644 dot-line-system/.history/index_20250522130505.html create mode 100644 dot-line-system/.history/index_20250522130518.html create mode 100644 dot-line-system/.history/index_20250522130539.html create mode 100644 dot-line-system/.history/index_20250522130613.html create mode 100644 dot-line-system/.history/index_20250522132851.html create mode 100644 dot-line-system/.history/index_20250522133036.html create mode 100644 dot-line-system/.history/index_20250522222202.html create mode 100644 dot-line-system/.history/index_20250522222233.html create mode 100644 dot-line-system/.history/package_20250515093313.json create mode 100644 dot-line-system/.history/package_20250515093412.json create mode 100644 dot-line-system/.history/package_20250515093415.json create mode 100644 dot-line-system/.history/readme_20250521221324.md create mode 100644 dot-line-system/.history/readme_20250521221329.md create mode 100644 dot-line-system/.history/readme_20250521221343.md create mode 100644 dot-line-system/.history/readme_20250521221558.md create mode 100644 dot-line-system/.history/readme_20250521221603.md create mode 100644 dot-line-system/.history/readme_20250521221932.md create mode 100644 dot-line-system/.history/readme_20250522081835.md create mode 100644 dot-line-system/.history/readme_20250522081843.md create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515080205.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515094002.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515094010.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515094030.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515094033.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515094036.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515094059.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515094215.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515094221.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250515094419.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522091556.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522091605.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522091611.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522091723.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522091732.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522091747.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522091858.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522091957.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522092004.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522092036.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522092041.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522092303.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522092543.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522092725.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522092810.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522092817.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522092936.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522093038.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522093141.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522093202.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522093316.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522093434.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522093501.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522093509.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522093827.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094341.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094352.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094405.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094427.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094438.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094441.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094711.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094716.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094721.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094758.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094923.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094927.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522094944.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522095334.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522095400.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522095543.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522101227.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522102503.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522102521.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522102742.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522103253.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522104846.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522104938.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105020.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105023.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105055.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105115.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105129.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105224.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105229.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105242.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105702.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522105928.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522110035.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522110335.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522110431.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522110437.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522110859.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522110953.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111000.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111003.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111016.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111039.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111311.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111404.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111448.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111457.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111743.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111904.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111911.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111915.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522111956.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522112131.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522112202.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522112233.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522112657.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522112705.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522113133.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522113224.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522113610.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522113613.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522113628.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522113832.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522113857.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522130644.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131005.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131150.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131159.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131202.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131206.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131208.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131212.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131413.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131415.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131457.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131501.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131509.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131516.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131534.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131713.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131724.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131749.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131803.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131811.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131917.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131950.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131954.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522131959.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522132013.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522132035.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522132039.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522132042.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522132047.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522132050.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522132342.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522132517.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522132606.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522133122.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522133129.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151345.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151350.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151359.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151406.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151421.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151436.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151449.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151453.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151603.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522151629.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153324.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153329.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153337.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153342.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153345.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153407.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153429.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153437.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153447.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153457.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153502.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522153506.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522222207.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522222913.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522222945.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522222952.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522231943.ts create mode 100644 dot-line-system/.history/src/ConnectedDotsVisualization_20250522232002.ts create mode 100644 dot-line-system/.history/src/main_20250515080205.ts create mode 100644 dot-line-system/.history/src/main_20250515093647.ts create mode 100644 dot-line-system/.history/src/main_20250515093655.ts create mode 100644 dot-line-system/.history/src/main_20250515093704.ts create mode 100644 dot-line-system/.history/src/main_20250515093739.ts create mode 100644 dot-line-system/.history/src/main_20250515093749.ts create mode 100644 dot-line-system/.history/src/main_20250515093755.ts create mode 100644 dot-line-system/.history/src/main_20250515093801.ts create mode 100644 dot-line-system/.history/src/main_20250515093807.ts create mode 100644 dot-line-system/.history/src/main_20250522082517.ts create mode 100644 dot-line-system/.history/src/main_20250522082653.ts create mode 100644 dot-line-system/.history/src/main_20250522082725.ts create mode 100644 dot-line-system/.history/src/main_20250522082823.ts create mode 100644 dot-line-system/.history/src/main_20250522083059.ts create mode 100644 dot-line-system/.history/src/main_20250522084926.ts create mode 100644 dot-line-system/.history/src/main_20250522085045.ts create mode 100644 dot-line-system/.history/src/main_20250522085108.ts create mode 100644 dot-line-system/.history/src/main_20250522085117.ts create mode 100644 dot-line-system/.history/src/main_20250522085138.ts create mode 100644 dot-line-system/.history/src/main_20250522085307.ts create mode 100644 dot-line-system/.history/src/main_20250522085525.ts create mode 100644 dot-line-system/.history/src/main_20250522085554.ts create mode 100644 dot-line-system/.history/src/main_20250522085614.ts create mode 100644 dot-line-system/.history/src/main_20250522085624.ts create mode 100644 dot-line-system/.history/src/main_20250522085712.ts create mode 100644 dot-line-system/.history/src/main_20250522085720.ts create mode 100644 dot-line-system/.history/src/main_20250522085742.ts create mode 100644 dot-line-system/.history/src/main_20250522085804.ts create mode 100644 dot-line-system/.history/src/main_20250522085818.ts create mode 100644 dot-line-system/.history/src/main_20250522085841.ts create mode 100644 dot-line-system/.history/src/main_20250522085845.ts create mode 100644 dot-line-system/.history/src/main_20250522085849.ts create mode 100644 dot-line-system/.history/src/main_20250522085852.ts create mode 100644 dot-line-system/.history/src/main_20250522090305.ts create mode 100644 dot-line-system/.history/src/main_20250522090350.ts create mode 100644 dot-line-system/.history/src/main_20250522090417.ts create mode 100644 dot-line-system/.history/src/main_20250522090431.ts create mode 100644 dot-line-system/.history/src/main_20250522090436.ts create mode 100644 dot-line-system/.history/src/main_20250522091026.ts create mode 100644 dot-line-system/.history/src/main_20250522091044.ts create mode 100644 dot-line-system/.history/src/main_20250522091103.ts create mode 100644 dot-line-system/.history/src/main_20250522091117.ts create mode 100644 dot-line-system/.history/src/main_20250522091134.ts create mode 100644 dot-line-system/.history/src/main_20250522091203.ts create mode 100644 dot-line-system/.history/src/main_20250522091258.ts create mode 100644 dot-line-system/.history/src/main_20250522102605.ts create mode 100644 dot-line-system/.history/src/main_20250522102612.ts create mode 100644 dot-line-system/.history/src/main_20250522102630.ts create mode 100644 dot-line-system/.history/src/main_20250522102634.ts create mode 100644 dot-line-system/.history/src/main_20250522102637.ts create mode 100644 dot-line-system/.history/src/main_20250522103603.ts create mode 100644 dot-line-system/.history/src/main_20250522103730.ts create mode 100644 dot-line-system/.history/src/main_20250522103753.ts create mode 100644 dot-line-system/.history/src/main_20250522104008.ts create mode 100644 dot-line-system/.history/src/main_20250522104033.ts create mode 100644 dot-line-system/.history/src/main_20250522104601.ts create mode 100644 dot-line-system/.history/src/main_20250522130745.ts create mode 100644 dot-line-system/.history/src/main_20250522131101.ts create mode 100644 dot-line-system/.history/src/main_20250522131108.ts create mode 100644 dot-line-system/.history/src/main_20250522131112.ts create mode 100644 dot-line-system/.history/src/main_20250522131119.ts create mode 100644 dot-line-system/.history/src/main_20250522131131.ts create mode 100644 dot-line-system/.history/src/main_20250522131843.ts create mode 100644 dot-line-system/.history/src/main_20250522131848.ts create mode 100644 dot-line-system/.history/src/main_20250522131851.ts create mode 100644 dot-line-system/.history/src/main_20250522131901.ts create mode 100644 dot-line-system/.history/src/main_20250522131929.ts create mode 100644 dot-line-system/.history/src/main_20250522131937.ts create mode 100644 dot-line-system/.history/src/main_20250522131944.ts create mode 100644 dot-line-system/.history/src/main_20250522132110.ts create mode 100644 dot-line-system/.history/src/main_20250522132123.ts create mode 100644 dot-line-system/.history/src/main_20250522132137.ts create mode 100644 dot-line-system/.history/src/main_20250522132218.ts create mode 100644 dot-line-system/.history/src/main_20250522132225.ts create mode 100644 dot-line-system/.history/src/main_20250522132350.ts create mode 100644 dot-line-system/.history/src/main_20250522132401.ts create mode 100644 dot-line-system/.history/src/main_20250522132525.ts create mode 100644 dot-line-system/.history/src/main_20250522132532.ts create mode 100644 dot-line-system/.history/src/main_20250522133155.ts create mode 100644 dot-line-system/.history/src/main_20250522133219.ts create mode 100644 dot-line-system/.history/src/main_20250522133230.ts create mode 100644 dot-line-system/.history/src/main_20250522150734.ts create mode 100644 dot-line-system/.history/src/main_20250522150803.ts create mode 100644 dot-line-system/.history/src/main_20250522150809.ts create mode 100644 dot-line-system/.history/src/main_20250522222205.ts create mode 100644 dot-line-system/.history/src/main_20250522222757.ts create mode 100644 dot-line-system/.history/src/main_20250522223705.ts create mode 100644 dot-line-system/.history/src/main_20250522224138.ts create mode 100644 dot-line-system/.history/src/main_20250522224154.ts create mode 100644 dot-line-system/.history/src/main_20250522224417.ts create mode 100644 dot-line-system/.history/src/main_20250522224419.ts create mode 100644 dot-line-system/.history/src/main_20250522224450.ts create mode 100644 dot-line-system/.history/src/main_20250522224516.ts create mode 100644 dot-line-system/.history/src/main_20250522224540.ts create mode 100644 dot-line-system/.history/src/main_20250522224607.ts create mode 100644 dot-line-system/.history/src/main_20250522224641.ts create mode 100644 dot-line-system/.history/src/main_20250522224710.ts create mode 100644 dot-line-system/.history/src/main_20250522224733.ts create mode 100644 dot-line-system/.history/src/main_20250522224743.ts create mode 100644 dot-line-system/.history/src/main_20250522224813.ts create mode 100644 dot-line-system/.history/src/main_20250522224826.ts create mode 100644 dot-line-system/.history/src/main_20250522224845.ts create mode 100644 dot-line-system/.history/src/main_20250522225100.ts create mode 100644 dot-line-system/.history/src/main_20250522225110.ts create mode 100644 dot-line-system/.history/src/main_20250522225139.ts create mode 100644 dot-line-system/.history/src/main_20250522231822.ts create mode 100644 dot-line-system/.history/src/main_20250523080429.ts create mode 100644 dot-line-system/.history/src/main_20250523080510.ts create mode 100644 dot-line-system/.history/src/main_20250523080542.ts create mode 100644 dot-line-system/.history/src/main_20250523080609.ts create mode 100644 dot-line-system/.history/src/main_20250523080934.ts create mode 100644 dot-line-system/.history/src/style_20250515080205.css create mode 100644 dot-line-system/.history/src/style_20250522085955.css create mode 100644 dot-line-system/.history/src/style_20250522090021.css create mode 100644 dot-line-system/.history/src/style_20250522090641.css create mode 100644 dot-line-system/.history/src/style_20250522090651.css create mode 100644 dot-line-system/.history/src/style_20250522090747.css create mode 100644 dot-line-system/.history/src/style_20250522090807.css create mode 100644 dot-line-system/.history/src/style_20250522090836.css create mode 100644 dot-line-system/.history/src/style_20250522090841.css create mode 100644 dot-line-system/.history/src/style_20250522090850.css create mode 100644 dot-line-system/.history/src/style_20250522090854.css create mode 100644 dot-line-system/.history/src/style_20250522090906.css create mode 100644 dot-line-system/.history/src/style_20250522090923.css create mode 100644 dot-line-system/.history/src/style_20250522090926.css create mode 100644 dot-line-system/.history/src/style_20250522090936.css create mode 100644 dot-line-system/.history/src/style_20250522090943.css create mode 100644 dot-line-system/.history/src/style_20250522093837.css create mode 100644 dot-line-system/.history/src/style_20250522093851.css create mode 100644 dot-line-system/.history/src/style_20250522093915.css create mode 100644 dot-line-system/.history/src/style_20250522093919.css create mode 100644 dot-line-system/.history/src/style_20250522093925.css create mode 100644 dot-line-system/.history/src/style_20250522095000.css create mode 100644 dot-line-system/.history/src/style_20250522095014.css create mode 100644 dot-line-system/.history/src/style_20250522095017.css create mode 100644 dot-line-system/.history/src/style_20250522095208.css create mode 100644 dot-line-system/.history/src/style_20250522095223.css create mode 100644 dot-line-system/.history/src/style_20250522095304.css create mode 100644 dot-line-system/.history/src/style_20250522095307.css create mode 100644 dot-line-system/.history/src/style_20250522095311.css create mode 100644 dot-line-system/.history/src/style_20250522095317.css create mode 100644 dot-line-system/.history/src/style_20250522095407.css create mode 100644 dot-line-system/.history/src/style_20250522095421.css create mode 100644 dot-line-system/.history/src/style_20250522095426.css create mode 100644 dot-line-system/.history/src/style_20250522095430.css create mode 100644 dot-line-system/.history/src/style_20250522095436.css create mode 100644 dot-line-system/.history/src/style_20250522095552.css create mode 100644 dot-line-system/.history/src/style_20250522095554.css create mode 100644 dot-line-system/.history/src/style_20250522095604.css create mode 100644 dot-line-system/.history/src/style_20250522095617.css create mode 100644 dot-line-system/.history/src/style_20250522100913.css create mode 100644 dot-line-system/.history/src/style_20250522100924.css create mode 100644 dot-line-system/.history/src/style_20250522100934.css create mode 100644 dot-line-system/.history/src/style_20250522100943.css create mode 100644 dot-line-system/.history/src/style_20250522101311.css create mode 100644 dot-line-system/.history/src/style_20250522101328.css create mode 100644 dot-line-system/.history/src/style_20250522101336.css create mode 100644 dot-line-system/.history/src/style_20250522101350.css create mode 100644 dot-line-system/.history/src/style_20250522102626.css create mode 100644 dot-line-system/.history/src/style_20250522102642.css create mode 100644 dot-line-system/.history/src/style_20250522103246.css create mode 100644 dot-line-system/.history/src/style_20250522103250.css create mode 100644 dot-line-system/.history/src/style_20250522103312.css create mode 100644 dot-line-system/.history/src/style_20250522103342.css create mode 100644 dot-line-system/.history/src/style_20250522103350.css create mode 100644 dot-line-system/.history/src/style_20250522103407.css create mode 100644 dot-line-system/.history/src/style_20250522103422.css create mode 100644 dot-line-system/.history/src/style_20250522103426.css create mode 100644 dot-line-system/.history/src/style_20250522103431.css create mode 100644 dot-line-system/.history/src/style_20250522103440.css create mode 100644 dot-line-system/.history/src/style_20250522104922.css create mode 100644 dot-line-system/.history/src/style_20250522105207.css create mode 100644 dot-line-system/.history/src/style_20250522105209.css create mode 100644 dot-line-system/.history/src/style_20250522105222.css create mode 100644 dot-line-system/.history/src/style_20250522110436.css create mode 100644 dot-line-system/.history/src/style_20250522112314.css create mode 100644 dot-line-system/.history/src/style_20250522112321.css create mode 100644 dot-line-system/.history/src/style_20250522112533.css create mode 100644 dot-line-system/.history/src/style_20250522112544.css create mode 100644 dot-line-system/.history/src/style_20250522112548.css create mode 100644 dot-line-system/.history/src/style_20250522112553.css create mode 100644 dot-line-system/.history/src/style_20250522112559.css create mode 100644 dot-line-system/.history/src/style_20250522112603.css create mode 100644 dot-line-system/.history/src/style_20250522112612.css create mode 100644 dot-line-system/.history/src/style_20250522112618.css create mode 100644 dot-line-system/.history/src/style_20250522112638.css create mode 100644 dot-line-system/.history/src/style_20250522112644.css create mode 100644 dot-line-system/.history/src/style_20250522112711.css create mode 100644 dot-line-system/.history/src/style_20250522112732.css create mode 100644 dot-line-system/.history/src/style_20250522113350.css create mode 100644 dot-line-system/.history/src/style_20250522113403.css create mode 100644 dot-line-system/.history/src/style_20250522113810.css create mode 100644 dot-line-system/.history/src/style_20250522114215.css create mode 100644 dot-line-system/.history/src/style_20250522114227.css create mode 100644 dot-line-system/.history/src/style_20250522114320.css create mode 100644 dot-line-system/.history/src/style_20250522114407.css create mode 100644 dot-line-system/.history/src/style_20250522114517.css create mode 100644 dot-line-system/.history/src/style_20250522114533.css create mode 100644 dot-line-system/.history/src/style_20250522114538.css create mode 100644 dot-line-system/.history/src/style_20250522114616.css create mode 100644 dot-line-system/.history/src/style_20250522114639.css create mode 100644 dot-line-system/.history/src/style_20250522114649.css create mode 100644 dot-line-system/.history/src/style_20250522114653.css create mode 100644 dot-line-system/.history/src/style_20250522114816.css create mode 100644 dot-line-system/.history/src/style_20250522114848.css create mode 100644 dot-line-system/.history/src/style_20250522114856.css create mode 100644 dot-line-system/.history/src/style_20250522114917.css create mode 100644 dot-line-system/.history/src/style_20250522114920.css create mode 100644 dot-line-system/.history/src/style_20250522114934.css create mode 100644 dot-line-system/.history/src/style_20250522114947.css create mode 100644 dot-line-system/.history/src/style_20250522115024.css create mode 100644 dot-line-system/.history/src/style_20250522115045.css create mode 100644 dot-line-system/.history/src/style_20250522115052.css create mode 100644 dot-line-system/.history/src/style_20250522115107.css create mode 100644 dot-line-system/.history/src/style_20250522115113.css create mode 100644 dot-line-system/.history/src/style_20250522115119.css create mode 100644 dot-line-system/.history/src/style_20250522115124.css create mode 100644 dot-line-system/.history/src/style_20250522115129.css create mode 100644 dot-line-system/.history/src/style_20250522115145.css create mode 100644 dot-line-system/.history/src/style_20250522115153.css create mode 100644 dot-line-system/.history/src/style_20250522115156.css create mode 100644 dot-line-system/.history/src/style_20250522115158.css create mode 100644 dot-line-system/.history/src/style_20250522115353.css create mode 100644 dot-line-system/.history/src/style_20250522115410.css create mode 100644 dot-line-system/.history/src/style_20250522115441.css create mode 100644 dot-line-system/.history/src/style_20250522115453.css create mode 100644 dot-line-system/.history/src/style_20250522115508.css create mode 100644 dot-line-system/.history/src/style_20250522115517.css create mode 100644 dot-line-system/.history/src/style_20250522115552.css create mode 100644 dot-line-system/.history/src/style_20250522115602.css create mode 100644 dot-line-system/.history/src/style_20250522115619.css create mode 100644 dot-line-system/.history/src/style_20250522115746.css create mode 100644 dot-line-system/.history/src/style_20250522115820.css create mode 100644 dot-line-system/.history/src/style_20250522130235.css create mode 100644 dot-line-system/.history/src/style_20250522130252.css create mode 100644 dot-line-system/.history/src/style_20250522130310.css create mode 100644 dot-line-system/.history/src/style_20250522130323.css create mode 100644 dot-line-system/.history/src/style_20250522130438.css create mode 100644 dot-line-system/.history/src/style_20250522130450.css create mode 100644 dot-line-system/.history/src/style_20250522130457.css create mode 100644 dot-line-system/.history/src/style_20250522130520.css create mode 100644 dot-line-system/.history/src/style_20250522130537.css create mode 100644 dot-line-system/.history/src/style_20250522130552.css create mode 100644 dot-line-system/.history/src/style_20250522130614.css create mode 100644 dot-line-system/.history/src/style_20250522130959.css create mode 100644 dot-line-system/.history/src/style_20250522131003.css create mode 100644 dot-line-system/.history/src/style_20250522131623.css create mode 100644 dot-line-system/.history/src/style_20250522132147.css create mode 100644 dot-line-system/.history/src/style_20250522132926.css create mode 100644 dot-line-system/.history/src/style_20250522132930.css create mode 100644 dot-line-system/.history/src/style_20250522132942.css create mode 100644 dot-line-system/.history/src/style_20250522132949.css create mode 100644 dot-line-system/.history/src/style_20250522132956.css create mode 100644 dot-line-system/.history/src/style_20250522133003.css create mode 100644 dot-line-system/.history/src/style_20250522133006.css create mode 100644 dot-line-system/.history/src/style_20250522133030.css create mode 100644 dot-line-system/.history/src/style_20250522133046.css create mode 100644 dot-line-system/.history/src/style_20250522133236.css create mode 100644 dot-line-system/.history/src/style_20250522133241.css create mode 100644 dot-line-system/.history/src/style_20250522133243.css create mode 100644 dot-line-system/.history/src/style_20250522133246.css create mode 100644 dot-line-system/.history/src/style_20250522133248.css create mode 100644 dot-line-system/.history/src/style_20250522133254.css create mode 100644 dot-line-system/.history/src/style_20250522133352.css create mode 100644 dot-line-system/.history/src/style_20250522133401.css create mode 100644 dot-line-system/.history/src/style_20250522133408.css create mode 100644 dot-line-system/.history/src/style_20250522133412.css create mode 100644 dot-line-system/.history/src/style_20250522153124.css create mode 100644 dot-line-system/.history/src/style_20250522153129.css create mode 100644 dot-line-system/.history/src/style_20250522153134.css create mode 100644 dot-line-system/.history/src/style_20250522153141.css create mode 100644 dot-line-system/.history/src/style_20250522153151.css create mode 100644 dot-line-system/.history/src/style_20250522153532.css create mode 100644 dot-line-system/.history/src/style_20250522153557.css create mode 100644 dot-line-system/.history/src/style_20250522153600.css create mode 100644 dot-line-system/.history/src/style_20250522223323.css create mode 100644 dot-line-system/.history/src/style_20250522230441.css create mode 100644 dot-line-system/.history/src/style_20250522232137.css create mode 100644 dot-line-system/.history/src/style_20250522232205.css create mode 100644 dot-line-system/.history/src/style_20250522232242.css create mode 100644 dot-line-system/.history/src/style_20250522232300.css create mode 100644 dot-line-system/.history/src/style_20250522232311.css create mode 100644 dot-line-system/.history/src/style_20250522232332.css create mode 100644 dot-line-system/.history/src/style_20250522232342.css create mode 100644 dot-line-system/.history/src/style_20250522232352.css create mode 100644 dot-line-system/.history/src/style_20250522232356.css create mode 100644 dot-line-system/.history/src/style_20250522232403.css create mode 100644 dot-line-system/.history/src/style_20250522232543.css create mode 100644 dot-line-system/.history/src/style_20250522232557.css create mode 100644 dot-line-system/.history/src/style_20250522232605.css create mode 100644 dot-line-system/.history/src/style_20250522232609.css create mode 100644 dot-line-system/.history/src/style_20250522232619.css create mode 100644 dot-line-system/.history/src/style_20250523080846.css create mode 100644 dot-line-system/.history/src/style_20250523080941.css create mode 100644 dot-line-system/.history/src/style_20250523081026.css create mode 100644 dot-line-system/.history/src/style_20250523081040.css create mode 100644 dot-line-system/bun.lock create mode 100644 dot-line-system/index.html create mode 100644 dot-line-system/package-lock.json create mode 100644 dot-line-system/package.json create mode 100644 dot-line-system/public/ScrollTrigger.min.js create mode 100644 dot-line-system/public/ScrollTrigger.min.js.map create mode 100644 dot-line-system/public/gsap.min.js create mode 100644 dot-line-system/public/gsap.min.js.map create mode 100644 dot-line-system/public/images/0_2.png create mode 100644 dot-line-system/public/images/0_3.png create mode 100644 dot-line-system/public/images/disco.png create mode 100644 dot-line-system/public/images/entspannung.png create mode 100644 dot-line-system/public/images/familie.png create mode 100644 dot-line-system/public/images/familie2.png create mode 100644 dot-line-system/public/images/feier.png create mode 100644 dot-line-system/public/images/gpt.png create mode 100644 dot-line-system/public/images/grosseltern.png create mode 100644 dot-line-system/public/images/hochzeit.png create mode 100644 dot-line-system/public/images/kindergeburtstag.png create mode 100644 dot-line-system/public/images/kinobesuch.png create mode 100644 dot-line-system/public/images/klasse.png create mode 100644 dot-line-system/public/images/oma.png create mode 100644 dot-line-system/public/images/pferd.png create mode 100644 dot-line-system/public/images/see.png create mode 100644 dot-line-system/public/images/sonntag.png create mode 100644 dot-line-system/public/images/work.png create mode 100644 dot-line-system/public/vite.svg create mode 100644 dot-line-system/readme.md create mode 100644 dot-line-system/src/ConnectedDotsVisualization.ts create mode 100644 dot-line-system/src/main.ts create mode 100644 dot-line-system/src/style.css create mode 100644 dot-line-system/src/typescript.svg create mode 100644 dot-line-system/src/vite-env.d.ts create mode 100644 dot-line-system/tsconfig.json 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 + ) { + // 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_20250522105229.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105229.ts new file mode 100644 index 0000000..1222aed --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105229.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 - 20 + }` + ); + // 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_20250522105242.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105242.ts new file mode 100644 index 0000000..17ac986 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105242.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 - 16 + }` + ); + // 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_20250522105702.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105702.ts new file mode 100644 index 0000000..8ac83b7 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105702.ts @@ -0,0 +1,518 @@ +// 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); + } + +// 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 - 16 + }` + ); + + 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); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + + + 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_20250522105928.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105928.ts new file mode 100644 index 0000000..86baa3a --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522105928.ts @@ -0,0 +1,518 @@ +// 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); + + + // 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 - 16 + }` + ); + + 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); + + tooltip.appendChild(contentContainer); + + 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_20250522110035.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110035.ts new file mode 100644 index 0000000..6937c4e --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110035.ts @@ -0,0 +1,518 @@ +// 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); + } + + // 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 - 16 + }` + ); + + 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); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + + + 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_20250522110335.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110335.ts new file mode 100644 index 0000000..438af12 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110335.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + const arrow = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + tooltipY + tooltipHeight - 16 + }` + ); + + 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); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + arrowContainer.classList.add("arrow-container"); + div.appendChild(arrowContainer); + + 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_20250522110431.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110431.ts new file mode 100644 index 0000000..438af12 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110431.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + const arrow = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + tooltipY + tooltipHeight - 16 + }` + ); + + 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); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + arrowContainer.classList.add("arrow-container"); + div.appendChild(arrowContainer); + + 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_20250522110437.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110437.ts new file mode 100644 index 0000000..438af12 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110437.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + const arrow = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + tooltipY + tooltipHeight - 16 + }` + ); + + 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); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + arrowContainer.classList.add("arrow-container"); + div.appendChild(arrowContainer); + + 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_20250522110859.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110859.ts new file mode 100644 index 0000000..68cb2dd --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110859.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + const arrow = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + tooltipY + tooltipHeight - 16 + }` + ); + + 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); + + arrowContainer.classList.add("arrow-container"); + div.appendChild(arrowContainer); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522110953.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110953.ts new file mode 100644 index 0000000..91bc4d5 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522110953.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + // const arrowContainer = document.createElement("div"); + // const arrow = document.createElementNS( + // "http://www.w3.org/2000/svg", + // "path" + // ); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + // tooltipY + tooltipHeight - 16 + // }` + // ); + + // 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); + + // arrowContainer.classList.add("arrow-container"); + // div.appendChild(arrowContainer); + + // contentContainer.appendChild(div); + // tooltip.appendChild(contentContainer); + + 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_20250522111000.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111000.ts new file mode 100644 index 0000000..8c121c1 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111000.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + // const arrowContainer = document.createElement("div"); + // const arrow = document.createElementNS( + // "http://www.w3.org/2000/svg", + // "path" + // ); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + // tooltipY + tooltipHeight - 16 + // }` + // ); + + // 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); + + // arrowContainer.classList.add("arrow-container"); + // div.appendChild(arrowContainer); + + contentContainer.appendChild(div); + // tooltip.appendChild(contentContainer); + + 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_20250522111003.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111003.ts new file mode 100644 index 0000000..e9552ed --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111003.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + // const arrowContainer = document.createElement("div"); + // const arrow = document.createElementNS( + // "http://www.w3.org/2000/svg", + // "path" + // ); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + // tooltipY + tooltipHeight - 16 + // }` + // ); + + // 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); + + // arrowContainer.classList.add("arrow-container"); + // div.appendChild(arrowContainer); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111016.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111016.ts new file mode 100644 index 0000000..5da3526 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111016.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + // const arrow = document.createElementNS( + // "http://www.w3.org/2000/svg", + // "path" + // ); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + // tooltipY + tooltipHeight - 16 + // }` + // ); + + // 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); + + // arrowContainer.classList.add("arrow-container"); + // div.appendChild(arrowContainer); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111039.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111039.ts new file mode 100644 index 0000000..d55288c --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111039.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + const arrow = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + tooltipY + tooltipHeight - 16 + }` + ); + + // 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); + + // arrowContainer.classList.add("arrow-container"); + // div.appendChild(arrowContainer); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111311.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111311.ts new file mode 100644 index 0000000..dd913cb --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111311.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + const arrow = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + tooltipY + tooltipHeight - 16 + }` + ); + + 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); + + // arrowContainer.classList.add("arrow-container"); + // div.appendChild(arrowContainer); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111404.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111404.ts new file mode 100644 index 0000000..e292a55 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111404.ts @@ -0,0 +1,520 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + const arrow = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + tooltipY + tooltipHeight - 16 + }` + ); + + 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); + + // arrowContainer.classList.add("arrow-container"); + // div.appendChild(arrowContainer); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111448.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111448.ts new file mode 100644 index 0000000..9fc82a8 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111448.ts @@ -0,0 +1,521 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + const arrow = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + tooltipY + tooltipHeight - 16 + }` + ); + + 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); + + arrowContainer.classList.add("arrow-container"); + div.appendChild(arrowContainer); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111457.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111457.ts new file mode 100644 index 0000000..3774628 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111457.ts @@ -0,0 +1,521 @@ +// 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); + } + + // Add arrow path if needed + const arrowContainer = document.createElement("div"); + const arrow = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${ + tooltipY + tooltipHeight - 16 + }` + ); + + 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); + + arrowContainer.classList.add("arrow-container"); + div.appendChild(arrowContainer); + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111743.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111743.ts new file mode 100644 index 0000000..7d44994 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111743.ts @@ -0,0 +1,510 @@ +// 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); + } + + // Create arrow path and add it inside tooltip-content + const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + ); + 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"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111904.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111904.ts new file mode 100644 index 0000000..50d9cb1 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111904.ts @@ -0,0 +1,510 @@ +// 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); + } + + // Create arrow path and add it inside tooltip-content + const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + ); + 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"); + + // div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111911.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111911.ts new file mode 100644 index 0000000..fe01e2e --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111911.ts @@ -0,0 +1,510 @@ +// 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); + } + + // Create arrow path and add it inside tooltip-content + const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + ); + 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"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + // contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111915.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111915.ts new file mode 100644 index 0000000..50d9cb1 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111915.ts @@ -0,0 +1,510 @@ +// 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); + } + + // Create arrow path and add it inside tooltip-content + const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + ); + 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"); + + // div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522111956.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111956.ts new file mode 100644 index 0000000..7d44994 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522111956.ts @@ -0,0 +1,510 @@ +// 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); + } + + // Create arrow path and add it inside tooltip-content + const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + arrow.setAttribute( + "d", + `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + ); + 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"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522112131.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112131.ts new file mode 100644 index 0000000..a91b304 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112131.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); + } + + // Create an empty div for the arrow + const arrowDiv = document.createElement('div'); + + // Optionally set styles directly or via CSS class + arrowDiv.style.width = '0'; + arrowDiv.style.height = '0'; + + // Apply styling through a CSS class if needed + arrowDiv.classList.add('tooltip-arrow'); + + // Append this div to the tooltip content + tooltip.appendChild(arrowDiv); + + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522112202.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112202.ts new file mode 100644 index 0000000..4b18c27 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112202.ts @@ -0,0 +1,512 @@ +// 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); + } + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + const arrowDiv = document.createElement('div'); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522112233.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112233.ts new file mode 100644 index 0000000..3d40c1a --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112233.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); + } + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + + const arrow = document.createElement('div'); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522112657.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112657.ts new file mode 100644 index 0000000..d6f1677 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112657.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", "0"); // 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); + } + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + + const arrow = document.createElement('div'); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522112705.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112705.ts new file mode 100644 index 0000000..2f0cc48 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522112705.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 - 10; // 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", "0"); // 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); + } + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + + const arrow = document.createElement('div'); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522113133.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113133.ts new file mode 100644 index 0000000..2414b46 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113133.ts @@ -0,0 +1,534 @@ +// 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 - 10; // 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", "0"); // 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"); + + // Check if link is available for the image + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_blank"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + img = link; // Use the link as the image container + } else { + img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + } + + + // 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); + } + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + + const arrow = document.createElement('div'); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522113224.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113224.ts new file mode 100644 index 0000000..7c23506 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113224.ts @@ -0,0 +1,529 @@ +// 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_blank"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522113610.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113610.ts new file mode 100644 index 0000000..9675052 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113610.ts @@ -0,0 +1,556 @@ +// 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + +// Create a container div +const imageContainer = document.createElement("div"); +imageContainer.classList.add("image_container"); // Add image_container class + +// Define a variable for handling case with or without link +let imgWrapper: HTMLElement; + +if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_blank"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + +} else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper +} + +// Append imgWrapper to the container +imageContainer.appendChild(imgWrapper); + +// Append the image container to the main div +div.appendChild(imageContainer); + + + + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522113613.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113613.ts new file mode 100644 index 0000000..47cb999 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113613.ts @@ -0,0 +1,531 @@ +// 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_blank"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522113628.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113628.ts new file mode 100644 index 0000000..c369897 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113628.ts @@ -0,0 +1,540 @@ +// 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_blank"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + +} else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper +} + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522113832.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113832.ts new file mode 100644 index 0000000..711322c --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113832.ts @@ -0,0 +1,538 @@ +// 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_blank"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + // Create arrow path and add it inside tooltip-content + // const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path"); + // arrow.setAttribute( + // "d", + // `M ${x} ${tooltipY + tooltipHeight} L ${x} ${tooltipY + tooltipHeight - 16}` + // ); + // arrow.setAttribute("stroke", "white"); // Set the color of the line + // arrow.setAttribute("stroke-width", "1"); // Set the line width to 1px + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522113857.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113857.ts new file mode 100644 index 0000000..349001c --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522113857.ts @@ -0,0 +1,529 @@ +// 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_blank"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522130644.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522130644.ts new file mode 100644 index 0000000..cd217f8 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522130644.ts @@ -0,0 +1,529 @@ +// 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131005.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131005.ts new file mode 100644 index 0000000..53911b5 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131005.ts @@ -0,0 +1,512 @@ +// 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131150.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131150.ts new file mode 100644 index 0000000..8b15f8f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131150.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131159.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131159.ts new file mode 100644 index 0000000..9a81055 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131159.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = 4 * 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131202.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131202.ts new file mode 100644 index 0000000..8b15f8f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131202.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131206.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131206.ts new file mode 100644 index 0000000..444b740 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131206.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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: 8, + 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131208.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131208.ts new file mode 100644 index 0000000..2424805 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131208.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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: 2, + 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131212.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131212.ts new file mode 100644 index 0000000..8b15f8f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131212.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131413.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131413.ts new file mode 100644 index 0000000..c0407b0 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131413.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 + 4) * 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131415.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131415.ts new file mode 100644 index 0000000..8b15f8f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131415.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131457.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131457.ts new file mode 100644 index 0000000..77047be --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131457.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = ` + + // `; + 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", "1"); + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131501.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131501.ts new file mode 100644 index 0000000..8b15f8f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131501.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131509.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131509.ts new file mode 100644 index 0000000..fc75b2e --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131509.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = ` + + // `; + 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.6); + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131516.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131516.ts new file mode 100644 index 0000000..83711f3 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131516.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = ` + + // `; + 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) * 1); + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131534.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131534.ts new file mode 100644 index 0000000..8b15f8f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131534.ts @@ -0,0 +1,512 @@ +// 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 || 80; + 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 = ` + + // `; + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131713.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131713.ts new file mode 100644 index 0000000..e1f37a9 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131713.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131724.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131724.ts new file mode 100644 index 0000000..7103267 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131724.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + 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 * 160; // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131749.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131749.ts new file mode 100644 index 0000000..2ed8e19 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131749.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + 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 * 130; // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131803.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131803.ts new file mode 100644 index 0000000..006961b --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131803.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + 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 * 140; // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131811.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131811.ts new file mode 100644 index 0000000..eed179f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131811.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131917.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131917.ts new file mode 100644 index 0000000..0ddd0fd --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131917.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.6); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131950.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131950.ts new file mode 100644 index 0000000..4b71306 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131950.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131954.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131954.ts new file mode 100644 index 0000000..cb6ec9c --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131954.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.4); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522131959.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131959.ts new file mode 100644 index 0000000..5fd0bec --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522131959.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.3); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522132013.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132013.ts new file mode 100644 index 0000000..4b71306 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132013.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 || 80; + 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 = ` + + // `; + 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 + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522132035.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132035.ts new file mode 100644 index 0000000..112735f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132035.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 || 80; + 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 = ` + + // `; + 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.5; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522132039.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132039.ts new file mode 100644 index 0000000..8a01ad6 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132039.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 || 80; + 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 = ` + + // `; + 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 / 1.5; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522132042.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132042.ts new file mode 100644 index 0000000..8f12e05 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132042.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 || 80; + 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 = ` + + // `; + 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 / 1.75; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522132047.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132047.ts new file mode 100644 index 0000000..8a01ad6 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132047.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 || 80; + 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 = ` + + // `; + 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 / 1.5; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522132050.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132050.ts new file mode 100644 index 0000000..226ae2e --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132050.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in a new tab + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522132342.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132342.ts new file mode 100644 index 0000000..d0bc631 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132342.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + } else { + console.error("Dot has no image URL"); + throw new Error("Dot has no image URL"); + } + + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522132517.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132517.ts new file mode 100644 index 0000000..4b624e8 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132517.ts @@ -0,0 +1,520 @@ +// 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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + } else { + console.error("Dot has no image URL"); + throw new Error("Dot has no image URL"); + } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + if (dot.imageUrl) { + // Append the image container to the main div + div.appendChild(imageContainer); + } + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522132606.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132606.ts new file mode 100644 index 0000000..b7ad570 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522132606.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.5); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522133122.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522133122.ts new file mode 100644 index 0000000..44396a6 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522133122.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.75); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522133129.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522133129.ts new file mode 100644 index 0000000..f8653dd --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522133129.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 140; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151345.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151345.ts new file mode 100644 index 0000000..2a466f5 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151345.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 100; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151350.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151350.ts new file mode 100644 index 0000000..dd0a30f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151350.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 200; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151359.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151359.ts new file mode 100644 index 0000000..490f84c --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151359.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151406.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151406.ts new file mode 100644 index 0000000..2a466f5 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151406.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 100; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151421.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151421.ts new file mode 100644 index 0000000..dd0a30f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151421.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 200; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151436.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151436.ts new file mode 100644 index 0000000..0a4e875 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151436.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * -200; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151449.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151449.ts new file mode 100644 index 0000000..1f21eae --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151449.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 1; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151453.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151453.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151453.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151603.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151603.ts new file mode 100644 index 0000000..1f21eae --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151603.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 1; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522151629.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151629.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522151629.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153324.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153324.ts new file mode 100644 index 0000000..3347189 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153324.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 5) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153329.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153329.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153329.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153337.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153337.ts new file mode 100644 index 0000000..169ec98 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153337.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 1) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153342.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153342.ts new file mode 100644 index 0000000..74fdb33 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153342.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 3) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153345.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153345.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153345.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153407.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153407.ts new file mode 100644 index 0000000..23b08b1 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153407.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 1000; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153429.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153429.ts new file mode 100644 index 0000000..5c09ff4 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153429.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 2000; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153437.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153437.ts new file mode 100644 index 0000000..50cacbb --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153437.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 600; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153447.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153447.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153447.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153457.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153457.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153457.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153502.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153502.ts new file mode 100644 index 0000000..e04360f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153502.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 400; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522153506.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153506.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522153506.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522222207.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522222207.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522222207.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522222913.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522222913.ts new file mode 100644 index 0000000..efab24d --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522222913.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522222945.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522222945.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522222945.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522222952.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522222952.ts new file mode 100644 index 0000000..e04360f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522222952.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 400; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522231943.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522231943.ts new file mode 100644 index 0000000..dd0a30f --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522231943.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 200; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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_20250522232002.ts b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522232002.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/.history/src/ConnectedDotsVisualization_20250522232002.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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/main_20250515080205.ts b/dot-line-system/.history/src/main_20250515080205.ts new file mode 100644 index 0000000..8a942d7 --- /dev/null +++ b/dot-line-system/.history/src/main_20250515080205.ts @@ -0,0 +1,120 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.2, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + value: -0.5, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250515093647.ts b/dot-line-system/.history/src/main_20250515093647.ts new file mode 100644 index 0000000..5e9fbe2 --- /dev/null +++ b/dot-line-system/.history/src/main_20250515093647.ts @@ -0,0 +1,120 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.5, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + value: -0.5, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250515093655.ts b/dot-line-system/.history/src/main_20250515093655.ts new file mode 100644 index 0000000..676a346 --- /dev/null +++ b/dot-line-system/.history/src/main_20250515093655.ts @@ -0,0 +1,120 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.0, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + value: -0.5, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250515093704.ts b/dot-line-system/.history/src/main_20250515093704.ts new file mode 100644 index 0000000..8a942d7 --- /dev/null +++ b/dot-line-system/.history/src/main_20250515093704.ts @@ -0,0 +1,120 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.2, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + value: -0.5, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250515093739.ts b/dot-line-system/.history/src/main_20250515093739.ts new file mode 100644 index 0000000..a19b572 --- /dev/null +++ b/dot-line-system/.history/src/main_20250515093739.ts @@ -0,0 +1,121 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.2, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + //value: -0.5, + value: 1, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250515093749.ts b/dot-line-system/.history/src/main_20250515093749.ts new file mode 100644 index 0000000..bf12248 --- /dev/null +++ b/dot-line-system/.history/src/main_20250515093749.ts @@ -0,0 +1,121 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.2, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + //value: -0.5, + value: 0.25, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250515093755.ts b/dot-line-system/.history/src/main_20250515093755.ts new file mode 100644 index 0000000..65a2ee2 --- /dev/null +++ b/dot-line-system/.history/src/main_20250515093755.ts @@ -0,0 +1,121 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.2, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + //value: -0.5, + value: -2, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250515093801.ts b/dot-line-system/.history/src/main_20250515093801.ts new file mode 100644 index 0000000..ef4c9df --- /dev/null +++ b/dot-line-system/.history/src/main_20250515093801.ts @@ -0,0 +1,121 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.2, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + //value: -0.5, + value: -1, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250515093807.ts b/dot-line-system/.history/src/main_20250515093807.ts new file mode 100644 index 0000000..3723c3a --- /dev/null +++ b/dot-line-system/.history/src/main_20250515093807.ts @@ -0,0 +1,121 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.2, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + //value: -0.5, + value: 0, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250522082517.ts b/dot-line-system/.history/src/main_20250522082517.ts new file mode 100644 index 0000000..0282657 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522082517.ts @@ -0,0 +1,198 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.2, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + //value: -0.5, + value: 0, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + },{ + id: 1, + value: -1.8, + x: -6, + imageUrl: 'https://picsum.photos/200/150?random=1', + title: 'First Point', + description: 'This is the starting point of our journey.', + link: '/page1' + }, + { + id: 2, + value: 1.2, + x: -4, + imageUrl: 'https://picsum.photos/200/150?random=2', + title: 'Rising Up', + description: 'We begin to see an upward trend here.' + }, + { + id: 3, + value: -0.6, + x: -2, + imageUrl: 'https://picsum.photos/200/150?random=3', + title: 'Minor Dip', + description: 'A small setback before the major growth.', + link: '/page3' + }, + { + id: 4, + value: 2.7, + x: 0, + imageUrl: 'https://picsum.photos/200/150?random=4', + title: 'Peak Performance', + description: 'This is our highest point so far!', + link: '/page4' + }, + { + id: 5, + value: 0.8, + x: 2, + imageUrl: 'https://picsum.photos/200/150?random=5', + title: 'Normalization', + description: 'Returning to more sustainable levels.' + }, + { + id: 6, + value: -2.9, + x: 4, + imageUrl: 'https://picsum.photos/200/150?random=6', + title: 'Major Decline', + description: 'A significant drop in our metrics.', + link: '/page6' + }, + { + id: 7, + value: 1.5, + x: 6, + imageUrl: 'https://picsum.photos/200/150?random=7', + title: 'Recovery', + description: 'Bouncing back strongly from the previous low.' + }, + { + id: 8, + //value: -0.5, + value: 0, + x: 8, + imageUrl: 'https://picsum.photos/200/150?random=8', + title: 'Slight Dip', + description: 'A minor correction in our upward trend.', + link: '/page8' + }, + { + id: 9, + value: 2.1, + x: 10, + imageUrl: 'https://picsum.photos/200/150?random=9', + title: 'Second Peak', + description: 'Another high point in our journey.' + } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250522082653.ts b/dot-line-system/.history/src/main_20250522082653.ts new file mode 100644 index 0000000..2f08cdd --- /dev/null +++ b/dot-line-system/.history/src/main_20250522082653.ts @@ -0,0 +1,66 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + const data = [ + { id: 1, value: -1.8, x: -6, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'First Point', description: 'This is the starting point of our journey.', link: '/page1' }, + { id: 2, value: 1.2, x: -4, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Rising Up', description: 'We begin to see an upward trend here.' }, + { id: 3, value: -0.6, x: -2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Minor Dip', description: 'A small setback before the major growth.', link: '/page3' }, + { id: 4, value: 2.7, x: 0, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Peak Performance', description: 'This is our highest point so far!', link: '/page4' }, + { id: 5, value: 0.8, x: 2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Normalization', description: 'Returning to more sustainable levels.' }, + { id: 6, value: -2.9, x: 4, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Major Decline', description: 'A significant drop in our metrics.', link: '/page6' }, + { id: 7, value: 1.5, x: 6, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Recovery', description: 'Bouncing back strongly from the previous low.' }, + { id: 8, value: 0, x: 8, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Slight Dip', description: 'A minor correction in our upward trend.', link: '/page8' }, + { id: 9, value: 2.1, x: 10, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Second Peak', description: 'Another high point in our journey.' }, + { id: 10, value: -1.8, x: -6, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'First Point Duplicate', description: 'This is a duplicate entry of our starting point.', link: '/page1' }, + { id: 11, value: 1.2, x: -4, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Rising Up Duplicate', description: 'Duplicate entry of the upward trend.' }, + { id: 12, value: -0.6, x: -2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Minor Dip Duplicate', description: 'Duplicate entry of the minor dip.', link: '/page3' }, + { id: 13, value: 2.7, x: 0, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Peak Performance Duplicate', description: 'Duplicate entry of our highest point.', link: '/page4' }, + { id: 14, value: 0.8, x: 2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Normalization Duplicate', description: 'Duplicate entry of the normalization stage.' }, + { id: 15, value: -2.9, x: 4, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Major Decline Duplicate', description: 'Duplicate entry of the major decline.', link: '/page6' }, + { id: 16, value: 1.5, x: 6, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Recovery Duplicate', description: 'Duplicate entry of the recovery phase.' }, + { id: 17, value: 0, x: 8, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Slight Dip Duplicate', description: 'Duplicate entry of the slight dip.', link: '/page8' }, + { id: 18, value: 2.1, x: 10, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Second Peak Duplicate', description: 'Duplicate entry of the second peak.' } + ]; + + console.log(data); + +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250522082725.ts b/dot-line-system/.history/src/main_20250522082725.ts new file mode 100644 index 0000000..5b875d4 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522082725.ts @@ -0,0 +1,61 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -6, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'First Point', description: 'This is the starting point of our journey.', link: '/page1' }, + { id: 2, value: 1.2, x: -4, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Rising Up', description: 'We begin to see an upward trend here.' }, + { id: 3, value: -0.6, x: -2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Minor Dip', description: 'A small setback before the major growth.', link: '/page3' }, + { id: 4, value: 2.7, x: 0, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Peak Performance', description: 'This is our highest point so far!', link: '/page4' }, + { id: 5, value: 0.8, x: 2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Normalization', description: 'Returning to more sustainable levels.' }, + { id: 6, value: -2.9, x: 4, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Major Decline', description: 'A significant drop in our metrics.', link: '/page6' }, + { id: 7, value: 1.5, x: 6, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Recovery', description: 'Bouncing back strongly from the previous low.' }, + { id: 8, value: 0, x: 8, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Slight Dip', description: 'A minor correction in our upward trend.', link: '/page8' }, + { id: 9, value: 2.1, x: 10, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Second Peak', description: 'Another high point in our journey.' }, + { id: 10, value: -1.8, x: -6, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'First Point Duplicate', description: 'This is a duplicate entry of our starting point.', link: '/page1' }, + { id: 11, value: 1.2, x: -4, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Rising Up Duplicate', description: 'Duplicate entry of the upward trend.' }, + { id: 12, value: -0.6, x: -2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Minor Dip Duplicate', description: 'Duplicate entry of the minor dip.', link: '/page3' }, + { id: 13, value: 2.7, x: 0, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Peak Performance Duplicate', description: 'Duplicate entry of our highest point.', link: '/page4' }, + { id: 14, value: 0.8, x: 2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Normalization Duplicate', description: 'Duplicate entry of the normalization stage.' }, + { id: 15, value: -2.9, x: 4, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Major Decline Duplicate', description: 'Duplicate entry of the major decline.', link: '/page6' }, + { id: 16, value: 1.5, x: 6, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Recovery Duplicate', description: 'Duplicate entry of the recovery phase.' }, + { id: 17, value: 0, x: 8, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Slight Dip Duplicate', description: 'Duplicate entry of the slight dip.', link: '/page8' }, + { id: 18, value: 2.1, x: 10, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Second Peak Duplicate', description: 'Duplicate entry of the second peak.' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250522082823.ts b/dot-line-system/.history/src/main_20250522082823.ts new file mode 100644 index 0000000..4d51cfd --- /dev/null +++ b/dot-line-system/.history/src/main_20250522082823.ts @@ -0,0 +1,61 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -10, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'First Point', description: 'This is the starting point of our journey.', link: '/page1' }, + { id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Rising Up', description: 'We begin to see an upward trend here.' }, + { id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Minor Dip', description: 'A small setback before the major growth.', link: '/page3' }, + { id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Peak Performance', description: 'This is our highest point so far!', link: '/page4' }, + { id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Normalization', description: 'Returning to more sustainable levels.' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Major Decline', description: 'A significant drop in our metrics.', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Recovery', description: 'Bouncing back strongly from the previous low.' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Slight Dip', description: 'A minor correction in our upward trend.', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Second Peak', description: 'Another high point in our journey.' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'First Point Duplicate', description: 'This is a duplicate entry of our starting point.', link: '/page1' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Rising Up Duplicate', description: 'Duplicate entry of the upward trend.' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Minor Dip Duplicate', description: 'Duplicate entry of the minor dip.', link: '/page3' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Peak Performance Duplicate', description: 'Duplicate entry of our highest point.', link: '/page4' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Normalization Duplicate', description: 'Duplicate entry of the normalization stage.' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Major Decline Duplicate', description: 'Duplicate entry of the major decline.', link: '/page6' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Recovery Duplicate', description: 'Duplicate entry of the recovery phase.' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Slight Dip Duplicate', description: 'Duplicate entry of the slight dip.', link: '/page8' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Second Peak Duplicate', description: 'Duplicate entry of the second peak.' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ +}); diff --git a/dot-line-system/.history/src/main_20250522083059.ts b/dot-line-system/.history/src/main_20250522083059.ts new file mode 100644 index 0000000..5dc1e7e --- /dev/null +++ b/dot-line-system/.history/src/main_20250522083059.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -10, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'First Point', description: 'This is the starting point of our journey.', link: '/page1' }, + { id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Rising Up', description: 'We begin to see an upward trend here.' }, + { id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Minor Dip', description: 'A small setback before the major growth.', link: '/page3' }, + { id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Peak Performance', description: 'This is our highest point so far!', link: '/page4' }, + { id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Normalization', description: 'Returning to more sustainable levels.' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Major Decline', description: 'A significant drop in our metrics.', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Recovery', description: 'Bouncing back strongly from the previous low.' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Slight Dip', description: 'A minor correction in our upward trend.', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Second Peak', description: 'Another high point in our journey.' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'First Point Duplicate', description: 'This is a duplicate entry of our starting point.', link: '/page1' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Rising Up Duplicate', description: 'Duplicate entry of the upward trend.' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Minor Dip Duplicate', description: 'Duplicate entry of the minor dip.', link: '/page3' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Peak Performance Duplicate', description: 'Duplicate entry of our highest point.', link: '/page4' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Normalization Duplicate', description: 'Duplicate entry of the normalization stage.' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Major Decline Duplicate', description: 'Duplicate entry of the major decline.', link: '/page6' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Recovery Duplicate', description: 'Duplicate entry of the recovery phase.' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Slight Dip Duplicate', description: 'Duplicate entry of the slight dip.', link: '/page8' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Second Peak Duplicate', description: 'Duplicate entry of the second peak.' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522084926.ts b/dot-line-system/.history/src/main_20250522084926.ts new file mode 100644 index 0000000..f4c63cb --- /dev/null +++ b/dot-line-system/.history/src/main_20250522084926.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -10, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Beginn des Abenteuers', description: '01.10.2024', link: '/page1' }, +{ id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Neuanfang', description: '02.10.2024' }, +{ id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Kleiner Rückschlag', description: '03.10.2024', link: '/page3' }, +{ id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Höhepunkt', description: '04.10.2024', link: '/page4' }, +{ id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Rückkehr zur Normalität', description: '05.10.2024' }, +{ id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Großer Rückgang', description: '06.10.2024', link: '/page6' }, +{ id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholung', description: '07.10.2024' }, +{ id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Leichter Rückgang', description: '08.10.2024', link: '/page8' }, +{ id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Zweiter Höhepunkt', description: '09.10.2024' }, +{ id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Beginn des Abenteuers (Wiederholung)', description: '10.10.2024', link: '/page1' }, +{ id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Neuanfang (Wiederholung)', description: '11.10.2024' }, +{ id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Kleiner Rückschlag (Wiederholung)', description: '12.10.2024', link: '/page3' }, +{ id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Höhepunkt (Wiederholung)', description: '13.10.2024', link: '/page4' }, +{ id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Rückkehr zur Normalität (Wiederholung)', description: '14.10.2024' }, +{ id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Großer Rückgang (Wiederholung)', description: '15.10.2024', link: '/page6' }, +{ id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholung (Wiederholung)', description: '16.10.2024' }, +{ id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Leichter Rückgang (Wiederholung)', description: '17.10.2024', link: '/page8' }, +{ id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Zweiter Höhepunkt (Wiederholung)', description: '18.10.2024' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085045.ts b/dot-line-system/.history/src/main_20250522085045.ts new file mode 100644 index 0000000..094e55e --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085045.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -10, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, +{ id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024' }, +{ id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, +{ id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, +{ id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024' }, +{ id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, +{ id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024' }, +{ id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, +{ id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024' }, +{ id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page1' }, +{ id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024' }, +{ id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page3' }, +{ id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page4' }, +{ id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung bei einem Buch', description: '14.10.2024' }, +{ id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page6' }, +{ id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024' }, +{ id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang im Park', description: '17.10.2024', link: '/page8' }, +{ id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085108.ts b/dot-line-system/.history/src/main_20250522085108.ts new file mode 100644 index 0000000..967f927 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085108.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -10, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, +{ id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024' }, +{ id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, +{ id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, +{ id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024' }, +{ id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, +{ id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024' }, +{ id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, +{ id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024' }, +{ id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page1' }, +{ id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024' }, +{ id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page3' }, +{ id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page4' }, +{ id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024' }, +{ id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page6' }, +{ id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024' }, +{ id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang im Park', description: '17.10.2024', link: '/page8' }, +{ id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085117.ts b/dot-line-system/.history/src/main_20250522085117.ts new file mode 100644 index 0000000..cb2f5c9 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085117.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -10, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, +{ id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024' }, +{ id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, +{ id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, +{ id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024' }, +{ id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, +{ id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024' }, +{ id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, +{ id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024' }, +{ id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page1' }, +{ id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024' }, +{ id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page3' }, +{ id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page4' }, +{ id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024' }, +{ id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page6' }, +{ id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024' }, +{ id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page8' }, +{ id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085138.ts b/dot-line-system/.history/src/main_20250522085138.ts new file mode 100644 index 0000000..1c17c30 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085138.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -10, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024' }, + { id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page1' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page3' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page4' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page6' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page8' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085307.ts b/dot-line-system/.history/src/main_20250522085307.ts new file mode 100644 index 0000000..4703cc4 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085307.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -10, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024' }, + { id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page1' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page3' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page4' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page6' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page8' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085525.ts b/dot-line-system/.history/src/main_20250522085525.ts new file mode 100644 index 0000000..9598b8a --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085525.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -10, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085554.ts b/dot-line-system/.history/src/main_20250522085554.ts new file mode 100644 index 0000000..2e857a9 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085554.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -12, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: -2, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085614.ts b/dot-line-system/.history/src/main_20250522085614.ts new file mode 100644 index 0000000..2be28dd --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085614.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -12, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: -8, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: -6, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: -4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: -1, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085624.ts b/dot-line-system/.history/src/main_20250522085624.ts new file mode 100644 index 0000000..e97e1e5 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085624.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -5, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: -4, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: -3, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: -2, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: -1, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085712.ts b/dot-line-system/.history/src/main_20250522085712.ts new file mode 100644 index 0000000..c0d2d99 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085712.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: 0, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: -4, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: -3, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: -2, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: -1, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085720.ts b/dot-line-system/.history/src/main_20250522085720.ts new file mode 100644 index 0000000..e3838d3 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085720.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: -4, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: -3, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: -2, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: -1, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 0, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 2, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 6, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085742.ts b/dot-line-system/.history/src/main_20250522085742.ts new file mode 100644 index 0000000..687ac9f --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085742.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 8, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 10, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 12, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 14, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085804.ts b/dot-line-system/.history/src/main_20250522085804.ts new file mode 100644 index 0000000..54555de --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085804.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 18, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 20, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 22, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 24, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085818.ts b/dot-line-system/.history/src/main_20250522085818.ts new file mode 100644 index 0000000..a535060 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085818.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085841.ts b/dot-line-system/.history/src/main_20250522085841.ts new file mode 100644 index 0000000..64a3999 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085841.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -4, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085845.ts b/dot-line-system/.history/src/main_20250522085845.ts new file mode 100644 index 0000000..68f1fd8 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085845.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -3, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085849.ts b/dot-line-system/.history/src/main_20250522085849.ts new file mode 100644 index 0000000..6dc5b28 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085849.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: 0, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522085852.ts b/dot-line-system/.history/src/main_20250522085852.ts new file mode 100644 index 0000000..a535060 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522085852.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Shoppingtag in der Stadt', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522090305.ts b/dot-line-system/.history/src/main_20250522090305.ts new file mode 100644 index 0000000..b1e2bbf --- /dev/null +++ b/dot-line-system/.history/src/main_20250522090305.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Unverhoffter Krankentag', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522090350.ts b/dot-line-system/.history/src/main_20250522090350.ts new file mode 100644 index 0000000..7f9f988 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522090350.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522090417.ts b/dot-line-system/.history/src/main_20250522090417.ts new file mode 100644 index 0000000..f0504a8 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522090417.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 2.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522090431.ts b/dot-line-system/.history/src/main_20250522090431.ts new file mode 100644 index 0000000..66d2197 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522090431.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.7, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522090436.ts b/dot-line-system/.history/src/main_20250522090436.ts new file mode 100644 index 0000000..a2aeb04 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522090436.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Wanderung in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522091026.ts b/dot-line-system/.history/src/main_20250522091026.ts new file mode 100644 index 0000000..b8c5d3d --- /dev/null +++ b/dot-line-system/.history/src/main_20250522091026.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522091044.ts b/dot-line-system/.history/src/main_20250522091044.ts new file mode 100644 index 0000000..c171fec --- /dev/null +++ b/dot-line-system/.history/src/main_20250522091044.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://picsum.photos/200/150?random=7', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522091103.ts b/dot-line-system/.history/src/main_20250522091103.ts new file mode 100644 index 0000000..6272538 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522091103.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522091117.ts b/dot-line-system/.history/src/main_20250522091117.ts new file mode 100644 index 0000000..5d700d5 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522091117.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://picsum.photos/200/150?random=8', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522091134.ts b/dot-line-system/.history/src/main_20250522091134.ts new file mode 100644 index 0000000..89a05ac --- /dev/null +++ b/dot-line-system/.history/src/main_20250522091134.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://picsum.photos/200/150?random=2', title: 'Omas Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522091203.ts b/dot-line-system/.history/src/main_20250522091203.ts new file mode 100644 index 0000000..40e6325 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522091203.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522091258.ts b/dot-line-system/.history/src/main_20250522091258.ts new file mode 100644 index 0000000..eb5aeca --- /dev/null +++ b/dot-line-system/.history/src/main_20250522091258.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522102605.ts b/dot-line-system/.history/src/main_20250522102605.ts new file mode 100644 index 0000000..327b59b --- /dev/null +++ b/dot-line-system/.history/src/main_20250522102605.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessensdfdsfs', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522102612.ts b/dot-line-system/.history/src/main_20250522102612.ts new file mode 100644 index 0000000..eb5aeca --- /dev/null +++ b/dot-line-system/.history/src/main_20250522102612.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522102630.ts b/dot-line-system/.history/src/main_20250522102630.ts new file mode 100644 index 0000000..14be066 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522102630.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessensdfdfs', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522102634.ts b/dot-line-system/.history/src/main_20250522102634.ts new file mode 100644 index 0000000..eb5aeca --- /dev/null +++ b/dot-line-system/.history/src/main_20250522102634.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522102637.ts b/dot-line-system/.history/src/main_20250522102637.ts new file mode 100644 index 0000000..eb5aeca --- /dev/null +++ b/dot-line-system/.history/src/main_20250522102637.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522103603.ts b/dot-line-system/.history/src/main_20250522103603.ts new file mode 100644 index 0000000..e38932a --- /dev/null +++ b/dot-line-system/.history/src/main_20250522103603.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://picsum.photos/200/150?random=5', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522103730.ts b/dot-line-system/.history/src/main_20250522103730.ts new file mode 100644 index 0000000..6e527f7 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522103730.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://picsum.photos/200/150?random=3', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522103753.ts b/dot-line-system/.history/src/main_20250522103753.ts new file mode 100644 index 0000000..427c257 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522103753.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://picsum.photos/200/150?random=1', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522104008.ts b/dot-line-system/.history/src/main_20250522104008.ts new file mode 100644 index 0000000..622dfcc --- /dev/null +++ b/dot-line-system/.history/src/main_20250522104008.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://picsum.photos/200/150?random=4', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522104033.ts b/dot-line-system/.history/src/main_20250522104033.ts new file mode 100644 index 0000000..f924c87 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522104033.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://picsum.photos/200/150?random=9', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522104601.ts b/dot-line-system/.history/src/main_20250522104601.ts new file mode 100644 index 0000000..2803a2d --- /dev/null +++ b/dot-line-system/.history/src/main_20250522104601.ts @@ -0,0 +1,93 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if(!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + +}); diff --git a/dot-line-system/.history/src/main_20250522130745.ts b/dot-line-system/.history/src/main_20250522130745.ts new file mode 100644 index 0000000..3197851 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522130745.ts @@ -0,0 +1,118 @@ +import { ConnectedDotsVisualization, type DotConfig } from './ConnectedDotsVisualization'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { id: 1, value: -1.8, x: -2, imageUrl: 'https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png', title: 'Beginn des neuen Abenteuers', description: '01.10.2024', link: '/page1' }, + { id: 2, value: 1.2, x: 0, imageUrl: 'https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png', title: 'Omas Annis Geburtstag', description: '02.10.2024', link: '/page2' }, + { id: 3, value: -0.6, x: 2, imageUrl: 'https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png', title: 'Konzertbesuch mit Freunden', description: '03.10.2024', link: '/page3' }, + { id: 4, value: 2.0, x: 4, imageUrl: 'https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png', title: 'Wanderreiten in den Bergen', description: '04.10.2024', link: '/page4' }, + { id: 5, value: 0.8, x: 6, imageUrl: 'https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png', title: 'Ruhiger Tag zu Hause', description: '05.10.2024', link: '/page5' }, + { id: 6, value: -2.9, x: 8, imageUrl: 'https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png', title: 'Oma Erna verstorben', description: '06.10.2024', link: '/page6' }, + { id: 7, value: 1.5, x: 10, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png', title: 'Erholungsausflug zum See', description: '07.10.2024', link: '/page7' }, + { id: 8, value: 0, x: 12, imageUrl: 'https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png', title: 'Kleine Wochenendsfeier', description: '08.10.2024', link: '/page8' }, + { id: 9, value: 3.1, x: 14, imageUrl: 'https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png', title: 'Hochzeit von Cousine Tatjana', description: '09.10.2024', link: '/page9' }, + { id: 10, value: -1.8, x: 16, imageUrl: 'https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png', title: 'Erster Tag im neuen Job', description: '10.10.2024', link: '/page10' }, + { id: 11, value: 1.2, x: 18, imageUrl: 'https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png', title: 'Klassentreffen nach vielen Jahren', description: '11.10.2024', link: '/page11' }, + { id: 12, value: -0.6, x: 20, imageUrl: 'https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png', title: 'Familienabendessen', description: '12.10.2024', link: '/page12' }, + { id: 13, value: 2.7, x: 22, imageUrl: 'https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png', title: 'Kinobesuch mit der ganzen Familie', description: '13.10.2024', link: '/page13' }, + { id: 14, value: 0.8, x: 24, imageUrl: 'https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png', title: 'Entspannung', description: '14.10.2024', link: '/page14' }, + { id: 15, value: -2.9, x: 26, imageUrl: 'https://picsum.photos/200/150?random=6', title: 'Geruhsamer Sonntag', description: '15.10.2024', link: '/page15' }, + { id: 16, value: 1.5, x: 28, imageUrl: 'https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png', title: 'Kindergeburtstag', description: '16.10.2024', link: '/page16' }, + { id: 17, value: 0, x: 30, imageUrl: 'https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png', title: 'Spaziergang mit der Familie', description: '17.10.2024', link: '/page17' }, + { id: 18, value: 2.1, x: 32, imageUrl: 'https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png', title: 'Familienfeier bei den Großeltern', description: '18.10.2024', link: '/page18' } +]; + +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization('scroll-container', sampleDots, { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150 + }); + + // Handle window resize + window.addEventListener('resize', () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector('.scroll-container'); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener('mousedown', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('mouseleave', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mouseup', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('mousemove', (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener('touchstart', (e) => { + isDown = true; + scrollContainer.classList.add('active'); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener('touchend', () => { + isDown = false; + scrollContainer.classList.remove('active'); + }); + + scrollContainer.addEventListener('touchmove', (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + +}); diff --git a/dot-line-system/.history/src/main_20250522131101.ts b/dot-line-system/.history/src/main_20250522131101.ts new file mode 100644 index 0000000..270f968 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131101.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value: 2.0, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -2.9, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 200, + tooltipHeight: 150, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131108.ts b/dot-line-system/.history/src/main_20250522131108.ts new file mode 100644 index 0000000..f078f8f --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131108.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value: 2.0, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -2.9, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 150, + tooltipHeight: 150, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131112.ts b/dot-line-system/.history/src/main_20250522131112.ts new file mode 100644 index 0000000..61965c2 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131112.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value: 2.0, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -2.9, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 100, + tooltipHeight: 150, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131119.ts b/dot-line-system/.history/src/main_20250522131119.ts new file mode 100644 index 0000000..e27abb7 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131119.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value: 2.0, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -2.9, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + tooltipWidth: 100, + tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131131.ts b/dot-line-system/.history/src/main_20250522131131.ts new file mode 100644 index 0000000..aeadf5e --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131131.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value: 2.0, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -2.9, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131843.ts b/dot-line-system/.history/src/main_20250522131843.ts new file mode 100644 index 0000000..f90a07a --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131843.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value: 3.0, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -2.9, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131848.ts b/dot-line-system/.history/src/main_20250522131848.ts new file mode 100644 index 0000000..63a29af --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131848.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value: 1.0, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -2.9, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131851.ts b/dot-line-system/.history/src/main_20250522131851.ts new file mode 100644 index 0000000..0298937 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131851.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value: 1.5, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -2.9, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131901.ts b/dot-line-system/.history/src/main_20250522131901.ts new file mode 100644 index 0000000..871a5d2 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131901.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value: 1.5, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131929.ts b/dot-line-system/.history/src/main_20250522131929.ts new file mode 100644 index 0000000..4af2f9a --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131929.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131937.ts b/dot-line-system/.history/src/main_20250522131937.ts new file mode 100644 index 0000000..4ae58e1 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131937.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:2.5, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522131944.ts b/dot-line-system/.history/src/main_20250522131944.ts new file mode 100644 index 0000000..4af2f9a --- /dev/null +++ b/dot-line-system/.history/src/main_20250522131944.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 0.8, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522132110.ts b/dot-line-system/.history/src/main_20250522132110.ts new file mode 100644 index 0000000..dfe864b --- /dev/null +++ b/dot-line-system/.history/src/main_20250522132110.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 3, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522132123.ts b/dot-line-system/.history/src/main_20250522132123.ts new file mode 100644 index 0000000..6f68913 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522132123.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:0, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 3, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522132137.ts b/dot-line-system/.history/src/main_20250522132137.ts new file mode 100644 index 0000000..d85f61d --- /dev/null +++ b/dot-line-system/.history/src/main_20250522132137.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522132218.ts b/dot-line-system/.history/src/main_20250522132218.ts new file mode 100644 index 0000000..1bde57f --- /dev/null +++ b/dot-line-system/.history/src/main_20250522132218.ts @@ -0,0 +1,283 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522132225.ts b/dot-line-system/.history/src/main_20250522132225.ts new file mode 100644 index 0000000..d85f61d --- /dev/null +++ b/dot-line-system/.history/src/main_20250522132225.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522132350.ts b/dot-line-system/.history/src/main_20250522132350.ts new file mode 100644 index 0000000..9b3867a --- /dev/null +++ b/dot-line-system/.history/src/main_20250522132350.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + // imageUrl: + // "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522132401.ts b/dot-line-system/.history/src/main_20250522132401.ts new file mode 100644 index 0000000..d85f61d --- /dev/null +++ b/dot-line-system/.history/src/main_20250522132401.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522132525.ts b/dot-line-system/.history/src/main_20250522132525.ts new file mode 100644 index 0000000..9b3867a --- /dev/null +++ b/dot-line-system/.history/src/main_20250522132525.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + // imageUrl: + // "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522132532.ts b/dot-line-system/.history/src/main_20250522132532.ts new file mode 100644 index 0000000..d85f61d --- /dev/null +++ b/dot-line-system/.history/src/main_20250522132532.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: -1.8, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: 1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522133155.ts b/dot-line-system/.history/src/main_20250522133155.ts new file mode 100644 index 0000000..3d3060e --- /dev/null +++ b/dot-line-system/.history/src/main_20250522133155.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.8, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522133219.ts b/dot-line-system/.history/src/main_20250522133219.ts new file mode 100644 index 0000000..7f3b729 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522133219.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0.5, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522133230.ts b/dot-line-system/.history/src/main_20250522133230.ts new file mode 100644 index 0000000..62d08a6 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522133230.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3.1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522150734.ts b/dot-line-system/.history/src/main_20250522150734.ts new file mode 100644 index 0000000..b5569bc --- /dev/null +++ b/dot-line-system/.history/src/main_20250522150734.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 2, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522150803.ts b/dot-line-system/.history/src/main_20250522150803.ts new file mode 100644 index 0000000..79e4936 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522150803.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 1, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522150809.ts b/dot-line-system/.history/src/main_20250522150809.ts new file mode 100644 index 0000000..9cb0d02 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522150809.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522222205.ts b/dot-line-system/.history/src/main_20250522222205.ts new file mode 100644 index 0000000..9cb0d02 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522222205.ts @@ -0,0 +1,284 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522222757.ts b/dot-line-system/.history/src/main_20250522222757.ts new file mode 100644 index 0000000..116a6f9 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522222757.ts @@ -0,0 +1,286 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "https://cdn.midjourney.com/07911cec-cc5a-478a-b580-ac1bb80bad94/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "https://cdn.midjourney.com/57370e4b-e0c3-4271-bf8f-69adbdb416cc/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522223705.ts b/dot-line-system/.history/src/main_20250522223705.ts new file mode 100644 index 0000000..e89cece --- /dev/null +++ b/dot-line-system/.history/src/main_20250522223705.ts @@ -0,0 +1,286 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "src/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "src/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224138.ts b/dot-line-system/.history/src/main_20250522224138.ts new file mode 100644 index 0000000..7c16f57 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224138.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224154.ts b/dot-line-system/.history/src/main_20250522224154.ts new file mode 100644 index 0000000..7c16f57 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224154.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "https://cdn.midjourney.com/876aa1b2-48c5-4d19-9c2b-297271d17a51/0_3.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "https://cdn.midjourney.com/8b61487f-bdc6-4550-99b8-52802fa55fe8/0_1.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224417.ts b/dot-line-system/.history/src/main_20250522224417.ts new file mode 100644 index 0000000..50bdd2f --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224417.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/discopferd.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224419.ts b/dot-line-system/.history/src/main_20250522224419.ts new file mode 100644 index 0000000..25f33ad --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224419.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "https://cdn.midjourney.com/8eafbeb0-35c8-4139-97b4-26b0298c64f6/0_2.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224450.ts b/dot-line-system/.history/src/main_20250522224450.ts new file mode 100644 index 0000000..9606a8e --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224450.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "https://cdn.midjourney.com/5419f43d-2d7e-474c-b672-82f671f410d9/0_3.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224516.ts b/dot-line-system/.history/src/main_20250522224516.ts new file mode 100644 index 0000000..34b0304 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224516.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_0.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224540.ts b/dot-line-system/.history/src/main_20250522224540.ts new file mode 100644 index 0000000..6ef77a8 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224540.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "https://cdn.midjourney.com/a30dd9b5-beae-4d2d-8ba8-e58b474b69f1/0_1.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224607.ts b/dot-line-system/.history/src/main_20250522224607.ts new file mode 100644 index 0000000..7b03f12 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224607.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "https://cdn.midjourney.com/2683889b-38af-49bd-954e-defe3c4ca659/0_0.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224641.ts b/dot-line-system/.history/src/main_20250522224641.ts new file mode 100644 index 0000000..e20d6b5 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224641.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "https://cdn.midjourney.com/92b34bda-d03c-4ec6-9e9f-b23f27f454d6/0_3.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224710.ts b/dot-line-system/.history/src/main_20250522224710.ts new file mode 100644 index 0000000..2b7c777 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224710.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "https://cdn.midjourney.com/83e928f3-27c2-4a74-8553-ce767be2d1d9/0_3.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224733.ts b/dot-line-system/.history/src/main_20250522224733.ts new file mode 100644 index 0000000..aca3d92 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224733.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "https://cdn.midjourney.com/4588970f-748f-4c1e-89aa-f3a34c4ef73d/0_1.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224743.ts b/dot-line-system/.history/src/main_20250522224743.ts new file mode 100644 index 0000000..e45726a --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224743.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "https://cdn.midjourney.com/2ca30890-cf87-4a0c-9a13-0fc4ec6a6b1c/0_2.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224813.ts b/dot-line-system/.history/src/main_20250522224813.ts new file mode 100644 index 0000000..59b3222 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224813.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224826.ts b/dot-line-system/.history/src/main_20250522224826.ts new file mode 100644 index 0000000..971d1d4 --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224826.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "https://cdn.midjourney.com/9ebf9d61-8ede-46de-9336-10211aebaef7/0_0.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522224845.ts b/dot-line-system/.history/src/main_20250522224845.ts new file mode 100644 index 0000000..b548ebd --- /dev/null +++ b/dot-line-system/.history/src/main_20250522224845.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "https://cdn.midjourney.com/86652ad5-d67c-4ddb-bb50-a651387a4e5b/0_3.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "https://cdn.midjourney.com/ac154c04-7cc9-4926-ae8d-4f21c18bcf8d/0_2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "https://cdn.midjourney.com/0782c375-5ea9-4de5-b174-0046a65ce8ce/0_3.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522225100.ts b/dot-line-system/.history/src/main_20250522225100.ts new file mode 100644 index 0000000..7f42ebe --- /dev/null +++ b/dot-line-system/.history/src/main_20250522225100.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522225110.ts b/dot-line-system/.history/src/main_20250522225110.ts new file mode 100644 index 0000000..7f42ebe --- /dev/null +++ b/dot-line-system/.history/src/main_20250522225110.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "https://picsum.photos/200/150?random=6", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522225139.ts b/dot-line-system/.history/src/main_20250522225139.ts new file mode 100644 index 0000000..43c9e6d --- /dev/null +++ b/dot-line-system/.history/src/main_20250522225139.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "/images/sonntag.png", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250522231822.ts b/dot-line-system/.history/src/main_20250522231822.ts new file mode 100644 index 0000000..43c9e6d --- /dev/null +++ b/dot-line-system/.history/src/main_20250522231822.ts @@ -0,0 +1,303 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "/images/sonntag.png", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + let scrollLeft; + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250523080429.ts b/dot-line-system/.history/src/main_20250523080429.ts new file mode 100644 index 0000000..bcc5dfe --- /dev/null +++ b/dot-line-system/.history/src/main_20250523080429.ts @@ -0,0 +1,305 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "/images/sonntag.png", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX; + // let scrollLeft; + let scrollLeft: number = scrollContainer.scrollLeft; // Initialize and type scrollLeft + + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250523080510.ts b/dot-line-system/.history/src/main_20250523080510.ts new file mode 100644 index 0000000..8fc9dd2 --- /dev/null +++ b/dot-line-system/.history/src/main_20250523080510.ts @@ -0,0 +1,321 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "/images/sonntag.png", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + +// Add a null check to ensure scrollContainer is not null +if (scrollContainer) { + // Cast scrollContainer as HTMLElement if necessary + const container = scrollContainer as HTMLElement; + + // Your event listener logic here + container.addEventListener('mousemove', (e: MouseEvent) => { + // It's safe to use offsetLeft and scrollLeft because we've checked for null + const x = e.pageX - container.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + container.scrollLeft -= walk; + }); +} else { + console.error("Scroll container not found. Please check the element ID."); +} + + let isDown = false; + let startX; + // let scrollLeft; + let scrollLeft: number = scrollContainer.scrollLeft; // Initialize and type scrollLeft + + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250523080542.ts b/dot-line-system/.history/src/main_20250523080542.ts new file mode 100644 index 0000000..8dd1ba0 --- /dev/null +++ b/dot-line-system/.history/src/main_20250523080542.ts @@ -0,0 +1,321 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "/images/sonntag.png", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + +// Add a null check to ensure scrollContainer is not null +if (scrollContainer) { + // Cast scrollContainer as HTMLElement if necessary + const container = scrollContainer as HTMLElement; + + // Your event listener logic here + container.addEventListener('mousemove', (e: MouseEvent) => { + // It's safe to use offsetLeft and scrollLeft because we've checked for null + const x = e.pageX - container.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + container.scrollLeft -= walk; + }); +} else { + console.error("Scroll container not found. Please check the element ID."); +} + + let isDown = false; + let startX: number; + // let scrollLeft; + let scrollLeft: number = scrollContainer.scrollLeft; // Initialize and type scrollLeft + + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250523080609.ts b/dot-line-system/.history/src/main_20250523080609.ts new file mode 100644 index 0000000..f018b74 --- /dev/null +++ b/dot-line-system/.history/src/main_20250523080609.ts @@ -0,0 +1,305 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "/images/sonntag.png", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container"); + + let isDown = false; + let startX: number; + // let scrollLeft; + let scrollLeft: number = scrollContainer.scrollLeft; // Initialize and type scrollLeft + + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("mouseleave", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mouseup", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + // Use the first touch point for calculations + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + }); + + scrollContainer.addEventListener("touchend", () => { + isDown = false; + scrollContainer.classList.remove("active"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + // Use the first touch point for calculations + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/main_20250523080934.ts b/dot-line-system/.history/src/main_20250523080934.ts new file mode 100644 index 0000000..cac6f1b --- /dev/null +++ b/dot-line-system/.history/src/main_20250523080934.ts @@ -0,0 +1,320 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "/images/sonntag.png", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container") as HTMLElement; + + let isDown = false; + let startX: number; + let scrollLeft: number; + + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + + // Remove smooth scrolling while dragging + scrollContainer.classList.remove("smooth-scroll"); + }); + + scrollContainer.addEventListener("mouseleave", () => { + if (!isDown) return; + isDown = false; + scrollContainer.classList.remove("active"); + + // Add smooth scrolling after dragging + scrollContainer.classList.add("smooth-scroll"); + }); + + scrollContainer.addEventListener("mouseup", () => { + if (!isDown) return; + isDown = false; + scrollContainer.classList.remove("active"); + + // Add smooth scrolling after dragging + scrollContainer.classList.add("smooth-scroll"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + + // Remove smooth scrolling while dragging + scrollContainer.classList.remove("smooth-scroll"); + }); + + scrollContainer.addEventListener("touchend", () => { + if (!isDown) return; + isDown = false; + scrollContainer.classList.remove("active"); + + // Add smooth scrolling after dragging + scrollContainer.classList.add("smooth-scroll"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/.history/src/style_20250515080205.css b/dot-line-system/.history/src/style_20250515080205.css new file mode 100644 index 0000000..e69de29 diff --git a/dot-line-system/.history/src/style_20250522085955.css b/dot-line-system/.history/src/style_20250522085955.css new file mode 100644 index 0000000..4da039c --- /dev/null +++ b/dot-line-system/.history/src/style_20250522085955.css @@ -0,0 +1,67 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090021.css b/dot-line-system/.history/src/style_20250522090021.css new file mode 100644 index 0000000..2e644ca --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090021.css @@ -0,0 +1,68 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090641.css b/dot-line-system/.history/src/style_20250522090641.css new file mode 100644 index 0000000..a4c667e --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090641.css @@ -0,0 +1,74 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + height: 1px; + width: 100%; + background-color: #fff; +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090651.css b/dot-line-system/.history/src/style_20250522090651.css new file mode 100644 index 0000000..1d47c53 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090651.css @@ -0,0 +1,74 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + height: 1px; + width: 100%; + background-color: red; +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090747.css b/dot-line-system/.history/src/style_20250522090747.css new file mode 100644 index 0000000..b3d6fd3 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090747.css @@ -0,0 +1,77 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + display: flex; +justify-items: center; + + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + height: 1px; + width: 100%; + background-color: red; +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090807.css b/dot-line-system/.history/src/style_20250522090807.css new file mode 100644 index 0000000..d12bb43 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090807.css @@ -0,0 +1,74 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + height: 1px; + width: 100%; + background-color: red; +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090836.css b/dot-line-system/.history/src/style_20250522090836.css new file mode 100644 index 0000000..1274256 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090836.css @@ -0,0 +1,76 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + height: 1px; + width: 100%; + background-color: red; +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090841.css b/dot-line-system/.history/src/style_20250522090841.css new file mode 100644 index 0000000..31dd748 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090841.css @@ -0,0 +1,77 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + background-color: red; +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090850.css b/dot-line-system/.history/src/style_20250522090850.css new file mode 100644 index 0000000..b987a9b --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090850.css @@ -0,0 +1,77 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + background-color: white; +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090854.css b/dot-line-system/.history/src/style_20250522090854.css new file mode 100644 index 0000000..29255e3 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090854.css @@ -0,0 +1,77 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 2px; + width: 100%; + background-color: white; +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090906.css b/dot-line-system/.history/src/style_20250522090906.css new file mode 100644 index 0000000..b987a9b --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090906.css @@ -0,0 +1,77 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + background-color: white; +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090923.css b/dot-line-system/.history/src/style_20250522090923.css new file mode 100644 index 0000000..f070a59 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090923.css @@ -0,0 +1,77 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + background-color: rgba(255, 255, 255, 0.5); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090926.css b/dot-line-system/.history/src/style_20250522090926.css new file mode 100644 index 0000000..c53bbb9 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090926.css @@ -0,0 +1,77 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + background-color: rgba(255, 255, 255, 0.2); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090936.css b/dot-line-system/.history/src/style_20250522090936.css new file mode 100644 index 0000000..e5b0ed4 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090936.css @@ -0,0 +1,78 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.2); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522090943.css b/dot-line-system/.history/src/style_20250522090943.css new file mode 100644 index 0000000..e051081 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522090943.css @@ -0,0 +1,78 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522093837.css b/dot-line-system/.history/src/style_20250522093837.css new file mode 100644 index 0000000..2b1739d --- /dev/null +++ b/dot-line-system/.history/src/style_20250522093837.css @@ -0,0 +1,112 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 50px; + height: 50px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: bold; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522093851.css b/dot-line-system/.history/src/style_20250522093851.css new file mode 100644 index 0000000..2ca9fff --- /dev/null +++ b/dot-line-system/.history/src/style_20250522093851.css @@ -0,0 +1,112 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: bold; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522093915.css b/dot-line-system/.history/src/style_20250522093915.css new file mode 100644 index 0000000..53d6c1c --- /dev/null +++ b/dot-line-system/.history/src/style_20250522093915.css @@ -0,0 +1,113 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: bold; + margin-top: 4px; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522093919.css b/dot-line-system/.history/src/style_20250522093919.css new file mode 100644 index 0000000..eb5587f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522093919.css @@ -0,0 +1,113 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: bold; + margin-top: 8px; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522093925.css b/dot-line-system/.history/src/style_20250522093925.css new file mode 100644 index 0000000..65db527 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522093925.css @@ -0,0 +1,113 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: bold; + margin-top: 8px; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095000.css b/dot-line-system/.history/src/style_20250522095000.css new file mode 100644 index 0000000..f67af40 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095000.css @@ -0,0 +1,114 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: bold; + margin-top: 8px; + text-align: center; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095014.css b/dot-line-system/.history/src/style_20250522095014.css new file mode 100644 index 0000000..34ee22a --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095014.css @@ -0,0 +1,115 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: bold; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095017.css b/dot-line-system/.history/src/style_20250522095017.css new file mode 100644 index 0000000..34ee22a --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095017.css @@ -0,0 +1,115 @@ + body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: bold; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095208.css b/dot-line-system/.history/src/style_20250522095208.css new file mode 100644 index 0000000..08ba4b8 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095208.css @@ -0,0 +1,119 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: bold; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095223.css b/dot-line-system/.history/src/style_20250522095223.css new file mode 100644 index 0000000..dbdb377 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095223.css @@ -0,0 +1,119 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095304.css b/dot-line-system/.history/src/style_20250522095304.css new file mode 100644 index 0000000..64a4e78 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095304.css @@ -0,0 +1,119 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 10px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095307.css b/dot-line-system/.history/src/style_20250522095307.css new file mode 100644 index 0000000..dbdb377 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095307.css @@ -0,0 +1,119 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095311.css b/dot-line-system/.history/src/style_20250522095311.css new file mode 100644 index 0000000..5a007d4 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095311.css @@ -0,0 +1,119 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095317.css b/dot-line-system/.history/src/style_20250522095317.css new file mode 100644 index 0000000..77640c3 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095317.css @@ -0,0 +1,119 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 14px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095407.css b/dot-line-system/.history/src/style_20250522095407.css new file mode 100644 index 0000000..f18c8fe --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095407.css @@ -0,0 +1,120 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 14px; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095421.css b/dot-line-system/.history/src/style_20250522095421.css new file mode 100644 index 0000000..3b30efd --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095421.css @@ -0,0 +1,121 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 14px; + font-weight: thin; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095426.css b/dot-line-system/.history/src/style_20250522095426.css new file mode 100644 index 0000000..f40981b --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095426.css @@ -0,0 +1,121 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: thin; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095430.css b/dot-line-system/.history/src/style_20250522095430.css new file mode 100644 index 0000000..5326195 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095430.css @@ -0,0 +1,121 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 16px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: thin; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095436.css b/dot-line-system/.history/src/style_20250522095436.css new file mode 100644 index 0000000..1d8faee --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095436.css @@ -0,0 +1,121 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: thin; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095552.css b/dot-line-system/.history/src/style_20250522095552.css new file mode 100644 index 0000000..145fa00 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095552.css @@ -0,0 +1,122 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: thin; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095554.css b/dot-line-system/.history/src/style_20250522095554.css new file mode 100644 index 0000000..145fa00 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095554.css @@ -0,0 +1,122 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: thin; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095604.css b/dot-line-system/.history/src/style_20250522095604.css new file mode 100644 index 0000000..529c2d9 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095604.css @@ -0,0 +1,123 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; + hyphens: auto; + white-space: wrap; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: thin; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522095617.css b/dot-line-system/.history/src/style_20250522095617.css new file mode 100644 index 0000000..145fa00 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522095617.css @@ -0,0 +1,122 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: thin; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522100913.css b/dot-line-system/.history/src/style_20250522100913.css new file mode 100644 index 0000000..318d2c7 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522100913.css @@ -0,0 +1,122 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: light; + margin-top: 8px; + text-align: center; + text-wrap: balanced; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: thin; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522100924.css b/dot-line-system/.history/src/style_20250522100924.css new file mode 100644 index 0000000..d86863d --- /dev/null +++ b/dot-line-system/.history/src/style_20250522100924.css @@ -0,0 +1,122 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-top: 8px; + text-align: center; + text-wrap: balanced; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522100934.css b/dot-line-system/.history/src/style_20250522100934.css new file mode 100644 index 0000000..7358538 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522100934.css @@ -0,0 +1,122 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balanced; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522100943.css b/dot-line-system/.history/src/style_20250522100943.css new file mode 100644 index 0000000..7ee9f9f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522100943.css @@ -0,0 +1,122 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balance; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522101311.css b/dot-line-system/.history/src/style_20250522101311.css new file mode 100644 index 0000000..ebeb77c --- /dev/null +++ b/dot-line-system/.history/src/style_20250522101311.css @@ -0,0 +1,127 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: 80px; + height: 80px; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balance; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; +} +.dot-tooltip .image_container{ + width: 80px; + height: 80px; + overflow: hidden; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522101328.css b/dot-line-system/.history/src/style_20250522101328.css new file mode 100644 index 0000000..6cb0457 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522101328.css @@ -0,0 +1,127 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: auto; + height: 100%; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balance; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; +} +.dot-tooltip .image_container{ + width: 80px; + height: 80px; + overflow: hidden; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522101336.css b/dot-line-system/.history/src/style_20250522101336.css new file mode 100644 index 0000000..15eae64 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522101336.css @@ -0,0 +1,128 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); +} + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + +.median{ + position: fixed; + top:50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); +} + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); +} + +.dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically */ + align-items: center; /* Center horizontally */ + width: 100%; + height: 100%; + color: white; /* Text color */ +} + +.dot-tooltip .tooltip-image { + width: auto; + height: 100%; + +} + +.dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balance; + hyphens: auto; +} + +.dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; +} +.dot-tooltip .image_container{ + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; /* Circular image */ + border: 2px solid white; +} + +.dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); +} diff --git a/dot-line-system/.history/src/style_20250522101350.css b/dot-line-system/.history/src/style_20250522101350.css new file mode 100644 index 0000000..3195d8e --- /dev/null +++ b/dot-line-system/.history/src/style_20250522101350.css @@ -0,0 +1,132 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + /* Circular image */ + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522102626.css b/dot-line-system/.history/src/style_20250522102626.css new file mode 100644 index 0000000..f78cbe0 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522102626.css @@ -0,0 +1,133 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balance; + hyphens: auto; + max-width: 100%; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + /* Circular image */ + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522102642.css b/dot-line-system/.history/src/style_20250522102642.css new file mode 100644 index 0000000..3195d8e --- /dev/null +++ b/dot-line-system/.history/src/style_20250522102642.css @@ -0,0 +1,132 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + /* Circular image */ + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103246.css b/dot-line-system/.history/src/style_20250522103246.css new file mode 100644 index 0000000..6800536 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103246.css @@ -0,0 +1,133 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + margin-top: 4px; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + /* Circular image */ + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103250.css b/dot-line-system/.history/src/style_20250522103250.css new file mode 100644 index 0000000..6800536 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103250.css @@ -0,0 +1,133 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + margin-top: 4px; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-top: 8px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + margin-bottom: 4px; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + /* Circular image */ + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103312.css b/dot-line-system/.history/src/style_20250522103312.css new file mode 100644 index 0000000..2738b91 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103312.css @@ -0,0 +1,132 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + margin-top: 4px; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 4px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + /* Circular image */ + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103342.css b/dot-line-system/.history/src/style_20250522103342.css new file mode 100644 index 0000000..36da931 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103342.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + + + .dot-tooltip .image_container { + margin-top: 8px; + + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 4px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + /* Circular image */ + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103350.css b/dot-line-system/.history/src/style_20250522103350.css new file mode 100644 index 0000000..22f9087 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103350.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + + + .dot-tooltip .image_container { + margin-top: 8px; + + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + /* Circular image */ + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103407.css b/dot-line-system/.history/src/style_20250522103407.css new file mode 100644 index 0000000..7291bd9 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103407.css @@ -0,0 +1,134 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(0, 0, 0, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103422.css b/dot-line-system/.history/src/style_20250522103422.css new file mode 100644 index 0000000..282e4b1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103422.css @@ -0,0 +1,135 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(0, 0, 0, 0.8); */ + fill: rgba(255, 255, 255, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103426.css b/dot-line-system/.history/src/style_20250522103426.css new file mode 100644 index 0000000..8e2cc8c --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103426.css @@ -0,0 +1,134 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.8); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(255, 255, 255, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103431.css b/dot-line-system/.history/src/style_20250522103431.css new file mode 100644 index 0000000..f8f50b2 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103431.css @@ -0,0 +1,134 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + /* fill: rgba(0, 0, 0, 0.8); */ + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(255, 255, 255, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522103440.css b/dot-line-system/.history/src/style_20250522103440.css new file mode 100644 index 0000000..c6ad579 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522103440.css @@ -0,0 +1,135 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(255, 255, 255, 0.8); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522104922.css b/dot-line-system/.history/src/style_20250522104922.css new file mode 100644 index 0000000..ff58dd6 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522104922.css @@ -0,0 +1,147 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + fill: rgba(255, 255, 255, 1); + } + + .arrow { + width: 100px; /* Adjust the length of the arrow as needed */ + height: 1px; + background: linear-gradient(to right, transparent, black, transparent); + position: relative; /* Allows positioning if needed */ +} + +/* Optional: To rotate or position the arrow */ +.arrow-vertical { + transform: rotate(90deg); /* Rotates the arrow to vertical */ +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522105207.css b/dot-line-system/.history/src/style_20250522105207.css new file mode 100644 index 0000000..f5a302f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522105207.css @@ -0,0 +1,148 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + fill: linear-gradient(to right, transparent, black, transparent);; + } + + .arrow { + width: 100px; /* Adjust the length of the arrow as needed */ + height: 1px; + background: linear-gradient(to right, transparent, black, transparent); + position: relative; /* Allows positioning if needed */ +} + +/* Optional: To rotate or position the arrow */ +.arrow-vertical { + transform: rotate(90deg); /* Rotates the arrow to vertical */ +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522105209.css b/dot-line-system/.history/src/style_20250522105209.css new file mode 100644 index 0000000..8b92c3a --- /dev/null +++ b/dot-line-system/.history/src/style_20250522105209.css @@ -0,0 +1,148 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + fill: linear-gradient(to right, transparent, black, transparent); + } + + .arrow { + width: 100px; /* Adjust the length of the arrow as needed */ + height: 1px; + background: linear-gradient(to right, transparent, black, transparent); + position: relative; /* Allows positioning if needed */ +} + +/* Optional: To rotate or position the arrow */ +.arrow-vertical { + transform: rotate(90deg); /* Rotates the arrow to vertical */ +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522105222.css b/dot-line-system/.history/src/style_20250522105222.css new file mode 100644 index 0000000..05e6203 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522105222.css @@ -0,0 +1,136 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + fill: linear-gradient(to right, transparent, black, transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522110436.css b/dot-line-system/.history/src/style_20250522110436.css new file mode 100644 index 0000000..05e6203 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522110436.css @@ -0,0 +1,136 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + fill: linear-gradient(to right, transparent, black, transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112314.css b/dot-line-system/.history/src/style_20250522112314.css new file mode 100644 index 0000000..8f3be83 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112314.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 1px; + height: 20px; + background: linear-gradient(to right, transparent, black, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112321.css b/dot-line-system/.history/src/style_20250522112321.css new file mode 100644 index 0000000..681916f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112321.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 2px; + height: 20px; + background: linear-gradient(to right, transparent, black, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112533.css b/dot-line-system/.history/src/style_20250522112533.css new file mode 100644 index 0000000..231076b --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112533.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 2px; + height: 20px; + background: linear-gradient(to right, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112544.css b/dot-line-system/.history/src/style_20250522112544.css new file mode 100644 index 0000000..2acccb8 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112544.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 2px; + height: 20px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112548.css b/dot-line-system/.history/src/style_20250522112548.css new file mode 100644 index 0000000..f3de22d --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112548.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 2px; + height: 30px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112553.css b/dot-line-system/.history/src/style_20250522112553.css new file mode 100644 index 0000000..ed5e837 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112553.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112559.css b/dot-line-system/.history/src/style_20250522112559.css new file mode 100644 index 0000000..ba447d1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112559.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 1px; + height: 40px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112603.css b/dot-line-system/.history/src/style_20250522112603.css new file mode 100644 index 0000000..ed5e837 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112603.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112612.css b/dot-line-system/.history/src/style_20250522112612.css new file mode 100644 index 0000000..74ae135 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112612.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 1px; + height: 20px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112618.css b/dot-line-system/.history/src/style_20250522112618.css new file mode 100644 index 0000000..ed5e837 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112618.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + /* fill: rgba(255, 255, 255, 1); */ + /* fill: linear-gradient(to right, transparent, black, transparent); */ + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112638.css b/dot-line-system/.history/src/style_20250522112638.css new file mode 100644 index 0000000..c2edfc1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112638.css @@ -0,0 +1,138 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112644.css b/dot-line-system/.history/src/style_20250522112644.css new file mode 100644 index 0000000..3eae7bd --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112644.css @@ -0,0 +1,138 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.5); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112711.css b/dot-line-system/.history/src/style_20250522112711.css new file mode 100644 index 0000000..c2edfc1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112711.css @@ -0,0 +1,138 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, white, transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522112732.css b/dot-line-system/.history/src/style_20250522112732.css new file mode 100644 index 0000000..0ee5c86 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522112732.css @@ -0,0 +1,138 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522113350.css b/dot-line-system/.history/src/style_20250522113350.css new file mode 100644 index 0000000..8ff1312 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522113350.css @@ -0,0 +1,140 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522113403.css b/dot-line-system/.history/src/style_20250522113403.css new file mode 100644 index 0000000..64594ca --- /dev/null +++ b/dot-line-system/.history/src/style_20250522113403.css @@ -0,0 +1,141 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522113810.css b/dot-line-system/.history/src/style_20250522113810.css new file mode 100644 index 0000000..3ce3551 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522113810.css @@ -0,0 +1,142 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114215.css b/dot-line-system/.history/src/style_20250522114215.css new file mode 100644 index 0000000..95101fc --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114215.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + +.dynamic-gradient-bg { + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98); + background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; +} + +@keyframes gradientAnimation { + 0% { + background-position: 0% 0%; /* Start Links Mitte */ + } + 25% { + background-position: 100% 0%; /* Oben Rechts */ + } + 50% { + background-position: 100% 100%; /* Unten Rechts */ + } + 75% { + background-position: 0% 100%; /* Unten Links */ + } + 100% { + background-position: 0% 0%; /* Zurück zum Start Links Mitte */ + } +} + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114227.css b/dot-line-system/.history/src/style_20250522114227.css new file mode 100644 index 0000000..649bee8 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114227.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + +.gradient-bg { + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98); + background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; +} + +@keyframes gradientAnimation { + 0% { + background-position: 0% 0%; /* Start Links Mitte */ + } + 25% { + background-position: 100% 0%; /* Oben Rechts */ + } + 50% { + background-position: 100% 100%; /* Unten Rechts */ + } + 75% { + background-position: 0% 100%; /* Unten Links */ + } + 100% { + background-position: 0% 0%; /* Zurück zum Start Links Mitte */ + } +} + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114320.css b/dot-line-system/.history/src/style_20250522114320.css new file mode 100644 index 0000000..649bee8 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114320.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + +.gradient-bg { + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98); + background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; +} + +@keyframes gradientAnimation { + 0% { + background-position: 0% 0%; /* Start Links Mitte */ + } + 25% { + background-position: 100% 0%; /* Oben Rechts */ + } + 50% { + background-position: 100% 100%; /* Unten Rechts */ + } + 75% { + background-position: 0% 100%; /* Unten Links */ + } + 100% { + background-position: 0% 0%; /* Zurück zum Start Links Mitte */ + } +} + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114407.css b/dot-line-system/.history/src/style_20250522114407.css new file mode 100644 index 0000000..95101fc --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114407.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + +.dynamic-gradient-bg { + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98); + background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; +} + +@keyframes gradientAnimation { + 0% { + background-position: 0% 0%; /* Start Links Mitte */ + } + 25% { + background-position: 100% 0%; /* Oben Rechts */ + } + 50% { + background-position: 100% 100%; /* Unten Rechts */ + } + 75% { + background-position: 0% 100%; /* Unten Links */ + } + 100% { + background-position: 0% 0%; /* Zurück zum Start Links Mitte */ + } +} + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114517.css b/dot-line-system/.history/src/style_20250522114517.css new file mode 100644 index 0000000..340da50 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114517.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + +.dynamic-gradient-bg { + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98); + background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; +} + +@keyframes gradientAnimation { + 0% { + background-position: 0% 0%; /* Start Links Mitte */ + } + 25% { + background-position: 100% 0%; /* Oben Rechts */ + } + 50% { + background-position: 100% 100%; /* Unten Rechts */ + } + 75% { + background-position: 0% 100%; /* Unten Links */ + } + 100% { + background-position: 0% 0%; /* Zurück zum Start Links Mitte */ + } +} + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } +/* + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); + z-index: -1; + } */ + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114533.css b/dot-line-system/.history/src/style_20250522114533.css new file mode 100644 index 0000000..257a13b --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114533.css @@ -0,0 +1,171 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + + +@keyframes gradientAnimation { + 0% { + background-position: 0% 0%; /* Start Links Mitte */ + } + 25% { + background-position: 100% 0%; /* Oben Rechts */ + } + 50% { + background-position: 100% 100%; /* Unten Rechts */ + } + 75% { + background-position: 0% 100%; /* Unten Links */ + } + 100% { + background-position: 0% 0%; /* Zurück zum Start Links Mitte */ + } +} + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + z-index: -1; + } + + .dynamic-gradient-bg { + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98); + background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; +} + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114538.css b/dot-line-system/.history/src/style_20250522114538.css new file mode 100644 index 0000000..fe18e47 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114538.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + + +@keyframes gradientAnimation { + 0% { + background-position: 0% 0%; /* Start Links Mitte */ + } + 25% { + background-position: 100% 0%; /* Oben Rechts */ + } + 50% { + background-position: 100% 100%; /* Unten Rechts */ + } + 75% { + background-position: 0% 100%; /* Unten Links */ + } + 100% { + background-position: 0% 0%; /* Zurück zum Start Links Mitte */ + } +} + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + z-index: -1; + + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98); + background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; +} + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114616.css b/dot-line-system/.history/src/style_20250522114616.css new file mode 100644 index 0000000..20c0bb6 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114616.css @@ -0,0 +1,168 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + + +@keyframes gradientAnimation { + 0% { + background-position: 0% 0%; /* Start Links Mitte */ + } + 25% { + background-position: 100% 0%; /* Oben Rechts */ + } + 50% { + background-position: 100% 100%; /* Unten Rechts */ + } + 75% { + background-position: 0% 100%; /* Unten Links */ + } + 100% { + background-position: 0% 0%; /* Zurück zum Start Links Mitte */ + } +} + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /*background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; +} + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.5), transparent); + +} \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114639.css b/dot-line-system/.history/src/style_20250522114639.css new file mode 100644 index 0000000..3381220 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114639.css @@ -0,0 +1,173 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /*background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + /* Start Links Mitte */ + } + + 25% { + background-position: 100% 0%; + /* Oben Rechts */ + } + + 50% { + background-position: 100% 100%; + /* Unten Rechts */ + } + + 75% { + background-position: 0% 100%; + /* Unten Links */ + } + + 100% { + background-position: 0% 0%; + /* Zurück zum Start Links Mitte */ + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114649.css b/dot-line-system/.history/src/style_20250522114649.css new file mode 100644 index 0000000..8701e91 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114649.css @@ -0,0 +1,168 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /*background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /*background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114653.css b/dot-line-system/.history/src/style_20250522114653.css new file mode 100644 index 0000000..5a22146 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114653.css @@ -0,0 +1,168 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114816.css b/dot-line-system/.history/src/style_20250522114816.css new file mode 100644 index 0000000..4c97910 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114816.css @@ -0,0 +1,171 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; /* Allows vertical scrolling if needed */ + + + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114848.css b/dot-line-system/.history/src/style_20250522114848.css new file mode 100644 index 0000000..484b475 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114848.css @@ -0,0 +1,167 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114856.css b/dot-line-system/.history/src/style_20250522114856.css new file mode 100644 index 0000000..f9fcf4d --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114856.css @@ -0,0 +1,167 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: auto; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114917.css b/dot-line-system/.history/src/style_20250522114917.css new file mode 100644 index 0000000..484b475 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114917.css @@ -0,0 +1,167 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + overflow-x: hidden; + overflow-y: hidden; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114920.css b/dot-line-system/.history/src/style_20250522114920.css new file mode 100644 index 0000000..b870002 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114920.css @@ -0,0 +1,167 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114934.css b/dot-line-system/.history/src/style_20250522114934.css new file mode 100644 index 0000000..401e1a9 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114934.css @@ -0,0 +1,167 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: auto; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522114947.css b/dot-line-system/.history/src/style_20250522114947.css new file mode 100644 index 0000000..2ebc79e --- /dev/null +++ b/dot-line-system/.history/src/style_20250522114947.css @@ -0,0 +1,167 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115024.css b/dot-line-system/.history/src/style_20250522115024.css new file mode 100644 index 0000000..86784e8 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115024.css @@ -0,0 +1,168 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115045.css b/dot-line-system/.history/src/style_20250522115045.css new file mode 100644 index 0000000..48ee122 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115045.css @@ -0,0 +1,168 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115052.css b/dot-line-system/.history/src/style_20250522115052.css new file mode 100644 index 0000000..a975cf0 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115052.css @@ -0,0 +1,168 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 1); + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115107.css b/dot-line-system/.history/src/style_20250522115107.css new file mode 100644 index 0000000..0649aeb --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115107.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 1); + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115113.css b/dot-line-system/.history/src/style_20250522115113.css new file mode 100644 index 0000000..ada98ed --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115113.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 1); + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115119.css b/dot-line-system/.history/src/style_20250522115119.css new file mode 100644 index 0000000..a124083 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115119.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2); + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115124.css b/dot-line-system/.history/src/style_20250522115124.css new file mode 100644 index 0000000..cdf2715 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115124.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.4); + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115129.css b/dot-line-system/.history/src/style_20250522115129.css new file mode 100644 index 0000000..bf180ab --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115129.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 300; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115145.css b/dot-line-system/.history/src/style_20250522115145.css new file mode 100644 index 0000000..2270003 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115145.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 200; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115153.css b/dot-line-system/.history/src/style_20250522115153.css new file mode 100644 index 0000000..8361746 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115153.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115156.css b/dot-line-system/.history/src/style_20250522115156.css new file mode 100644 index 0000000..6000a74 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115156.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 11px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115158.css b/dot-line-system/.history/src/style_20250522115158.css new file mode 100644 index 0000000..af235c3 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115158.css @@ -0,0 +1,169 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115353.css b/dot-line-system/.history/src/style_20250522115353.css new file mode 100644 index 0000000..2f680bc --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115353.css @@ -0,0 +1,177 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); + transition: box-shadow 0.5s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.5s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115410.css b/dot-line-system/.history/src/style_20250522115410.css new file mode 100644 index 0000000..d044926 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115410.css @@ -0,0 +1,177 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.5); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115441.css b/dot-line-system/.history/src/style_20250522115441.css new file mode 100644 index 0000000..b84fe27 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115441.css @@ -0,0 +1,178 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + /* box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.5);*/ + box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115453.css b/dot-line-system/.history/src/style_20250522115453.css new file mode 100644 index 0000000..f78929c --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115453.css @@ -0,0 +1,178 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.5); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115508.css b/dot-line-system/.history/src/style_20250522115508.css new file mode 100644 index 0000000..e0bec63 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115508.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.5); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115517.css b/dot-line-system/.history/src/style_20250522115517.css new file mode 100644 index 0000000..68ef695 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115517.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115552.css b/dot-line-system/.history/src/style_20250522115552.css new file mode 100644 index 0000000..317ea64 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115552.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 2px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115602.css b/dot-line-system/.history/src/style_20250522115602.css new file mode 100644 index 0000000..68ef695 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115602.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115619.css b/dot-line-system/.history/src/style_20250522115619.css new file mode 100644 index 0000000..30f1a14 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115619.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115746.css b/dot-line-system/.history/src/style_20250522115746.css new file mode 100644 index 0000000..b4696d1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115746.css @@ -0,0 +1,181 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522115820.css b/dot-line-system/.history/src/style_20250522115820.css new file mode 100644 index 0000000..b4696d1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522115820.css @@ -0,0 +1,181 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130235.css b/dot-line-system/.history/src/style_20250522130235.css new file mode 100644 index 0000000..b4696d1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130235.css @@ -0,0 +1,181 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 30s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130252.css b/dot-line-system/.history/src/style_20250522130252.css new file mode 100644 index 0000000..efe0cbd --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130252.css @@ -0,0 +1,181 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 10s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130310.css b/dot-line-system/.history/src/style_20250522130310.css new file mode 100644 index 0000000..efe0cbd --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130310.css @@ -0,0 +1,181 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + /* overflow-x: hidden; + overflow-y: hidden; */ + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 10s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130323.css b/dot-line-system/.history/src/style_20250522130323.css new file mode 100644 index 0000000..b32fb0a --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130323.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + animation: gradientAnimation 10s ease infinite; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130438.css b/dot-line-system/.history/src/style_20250522130438.css new file mode 100644 index 0000000..fc1b096 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130438.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130450.css b/dot-line-system/.history/src/style_20250522130450.css new file mode 100644 index 0000000..a2f8aa9 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130450.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 1s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130457.css b/dot-line-system/.history/src/style_20250522130457.css new file mode 100644 index 0000000..a2f8aa9 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130457.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 1s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130520.css b/dot-line-system/.history/src/style_20250522130520.css new file mode 100644 index 0000000..a2f8aa9 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130520.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 1s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130537.css b/dot-line-system/.history/src/style_20250522130537.css new file mode 100644 index 0000000..f621b52 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130537.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 1s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130552.css b/dot-line-system/.history/src/style_20250522130552.css new file mode 100644 index 0000000..cc9d34e --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130552.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130614.css b/dot-line-system/.history/src/style_20250522130614.css new file mode 100644 index 0000000..cc9d34e --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130614.css @@ -0,0 +1,179 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522130959.css b/dot-line-system/.history/src/style_20250522130959.css new file mode 100644 index 0000000..0b7920b --- /dev/null +++ b/dot-line-system/.history/src/style_20250522130959.css @@ -0,0 +1,198 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522131003.css b/dot-line-system/.history/src/style_20250522131003.css new file mode 100644 index 0000000..85730c0 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522131003.css @@ -0,0 +1,204 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + padding-top: 2rem; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + /* Apply transition to box-shadow */ + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + /* Apply transition to box-shadow */ + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522131623.css b/dot-line-system/.history/src/style_20250522131623.css new file mode 100644 index 0000000..cbf179e --- /dev/null +++ b/dot-line-system/.history/src/style_20250522131623.css @@ -0,0 +1,202 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.05); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522132147.css b/dot-line-system/.history/src/style_20250522132147.css new file mode 100644 index 0000000..f0c6d1c --- /dev/null +++ b/dot-line-system/.history/src/style_20250522132147.css @@ -0,0 +1,202 @@ + body { + font-family: "Montserrat", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522132926.css b/dot-line-system/.history/src/style_20250522132926.css new file mode 100644 index 0000000..bf3fcc3 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522132926.css @@ -0,0 +1,202 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522132930.css b/dot-line-system/.history/src/style_20250522132930.css new file mode 100644 index 0000000..4adefe1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522132930.css @@ -0,0 +1,202 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 12px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522132942.css b/dot-line-system/.history/src/style_20250522132942.css new file mode 100644 index 0000000..4ed56f0 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522132942.css @@ -0,0 +1,202 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 16px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 10px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522132949.css b/dot-line-system/.history/src/style_20250522132949.css new file mode 100644 index 0000000..d21ff7f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522132949.css @@ -0,0 +1,202 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 16px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522132956.css b/dot-line-system/.history/src/style_20250522132956.css new file mode 100644 index 0000000..8fe6d99 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522132956.css @@ -0,0 +1,202 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133003.css b/dot-line-system/.history/src/style_20250522133003.css new file mode 100644 index 0000000..63c3fbf --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133003.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.2; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133006.css b/dot-line-system/.history/src/style_20250522133006.css new file mode 100644 index 0000000..82e19a5 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133006.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133030.css b/dot-line-system/.history/src/style_20250522133030.css new file mode 100644 index 0000000..a1d2e8f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133030.css @@ -0,0 +1,204 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + /* text-wrap: balance; */ + text-align: justify; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133046.css b/dot-line-system/.history/src/style_20250522133046.css new file mode 100644 index 0000000..82e19a5 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133046.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 50%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133236.css b/dot-line-system/.history/src/style_20250522133236.css new file mode 100644 index 0000000..90f3454 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133236.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 60%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133241.css b/dot-line-system/.history/src/style_20250522133241.css new file mode 100644 index 0000000..427dfa5 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133241.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 62.5%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133243.css b/dot-line-system/.history/src/style_20250522133243.css new file mode 100644 index 0000000..4af0f56 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133243.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 62%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133246.css b/dot-line-system/.history/src/style_20250522133246.css new file mode 100644 index 0000000..fc23a61 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133246.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133248.css b/dot-line-system/.history/src/style_20250522133248.css new file mode 100644 index 0000000..1ebdd19 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133248.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133254.css b/dot-line-system/.history/src/style_20250522133254.css new file mode 100644 index 0000000..e002d7f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133254.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.75%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133352.css b/dot-line-system/.history/src/style_20250522133352.css new file mode 100644 index 0000000..e002d7f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133352.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + /* background: linear-gradient(to bottom, #4f46e5, #60a5fa, #4ade80); */ + /* background: linear-gradient(45deg, #541ba2, #d4890b, #fc1c98);*/ + /* background: linear-gradient(45deg, #7317c3, #d2348b, #31e6cb); */ + /* background: linear-gradient(-45deg, #a0197c, #fe4374, #541ba2, #a0197c);*/ + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.75%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133401.css b/dot-line-system/.history/src/style_20250522133401.css new file mode 100644 index 0000000..52edf24 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133401.css @@ -0,0 +1,199 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 200% 200%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.75%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133408.css b/dot-line-system/.history/src/style_20250522133408.css new file mode 100644 index 0000000..e41bb61 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133408.css @@ -0,0 +1,199 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 300% 300%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.75%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522133412.css b/dot-line-system/.history/src/style_20250522133412.css new file mode 100644 index 0000000..4919966 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522133412.css @@ -0,0 +1,199 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.75%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522153124.css b/dot-line-system/.history/src/style_20250522153124.css new file mode 100644 index 0000000..a11c985 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522153124.css @@ -0,0 +1,199 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.75%; + left: 0; + height: 2px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.1); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522153129.css b/dot-line-system/.history/src/style_20250522153129.css new file mode 100644 index 0000000..e8970db --- /dev/null +++ b/dot-line-system/.history/src/style_20250522153129.css @@ -0,0 +1,199 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.75%; + left: 0; + height: 2px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.8); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522153134.css b/dot-line-system/.history/src/style_20250522153134.css new file mode 100644 index 0000000..2119044 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522153134.css @@ -0,0 +1,199 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.75%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.8); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522153141.css b/dot-line-system/.history/src/style_20250522153141.css new file mode 100644 index 0000000..30f0809 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522153141.css @@ -0,0 +1,199 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.8); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522153151.css b/dot-line-system/.history/src/style_20250522153151.css new file mode 100644 index 0000000..173b4d9 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522153151.css @@ -0,0 +1,199 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 0, 0, 0.8); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522153532.css b/dot-line-system/.history/src/style_20250522153532.css new file mode 100644 index 0000000..2f057f0 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522153532.css @@ -0,0 +1,199 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.2); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522153557.css b/dot-line-system/.history/src/style_20250522153557.css new file mode 100644 index 0000000..02476a1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522153557.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed white; + width: 100%; + z-index: -1; + background-color: rgba(255, 255, 255, 0.2); + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522153600.css b/dot-line-system/.history/src/style_20250522153600.css new file mode 100644 index 0000000..c1a966d --- /dev/null +++ b/dot-line-system/.history/src/style_20250522153600.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed white; + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522223323.css b/dot-line-system/.history/src/style_20250522223323.css new file mode 100644 index 0000000..a8e5ad4 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522223323.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: auto; + height: 100%; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522230441.css b/dot-line-system/.history/src/style_20250522230441.css new file mode 100644 index 0000000..545d443 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522230441.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: -1; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232137.css b/dot-line-system/.history/src/style_20250522232137.css new file mode 100644 index 0000000..93e075d --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232137.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: 0; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232205.css b/dot-line-system/.history/src/style_20250522232205.css new file mode 100644 index 0000000..93e075d --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232205.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: 0; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232242.css b/dot-line-system/.history/src/style_20250522232242.css new file mode 100644 index 0000000..c35fa4f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232242.css @@ -0,0 +1,205 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-width: 100%; + z-index: 0; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + .gradient-bg { + border: 2px solid red; /* Temporary for debugging */ +} + + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232300.css b/dot-line-system/.history/src/style_20250522232300.css new file mode 100644 index 0000000..201161d --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232300.css @@ -0,0 +1,205 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + .gradient-bg { + border: 2px solid red; /* Temporary for debugging */ +} + + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232311.css b/dot-line-system/.history/src/style_20250522232311.css new file mode 100644 index 0000000..0c0b176 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232311.css @@ -0,0 +1,205 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background-size: 200% 200%; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + .gradient-bg { + border: 2px solid green; /* Temporary for debugging */ +} + + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232332.css b/dot-line-system/.history/src/style_20250522232332.css new file mode 100644 index 0000000..487866f --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232332.css @@ -0,0 +1,205 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background-size: 200% 200%; + background-color: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + animation: gradientAnimation 10s ease infinite; + } + + .gradient-bg { + border: 2px solid green; /* Temporary for debugging */ +} + + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232342.css b/dot-line-system/.history/src/style_20250522232342.css new file mode 100644 index 0000000..eedaebd --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232342.css @@ -0,0 +1,205 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 10s ease infinite; + } + + .gradient-bg { + border: 2px solid green; /* Temporary for debugging */ +} + + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232352.css b/dot-line-system/.history/src/style_20250522232352.css new file mode 100644 index 0000000..27bc2e2 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232352.css @@ -0,0 +1,205 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 30s ease infinite; + } + + .gradient-bg { + border: 2px solid green; /* Temporary for debugging */ +} + + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232356.css b/dot-line-system/.history/src/style_20250522232356.css new file mode 100644 index 0000000..30c2087 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232356.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 30s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232403.css b/dot-line-system/.history/src/style_20250522232403.css new file mode 100644 index 0000000..45fc6a6 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232403.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232543.css b/dot-line-system/.history/src/style_20250522232543.css new file mode 100644 index 0000000..9e6d4c1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232543.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: 0; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232557.css b/dot-line-system/.history/src/style_20250522232557.css new file mode 100644 index 0000000..b7c9577 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232557.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: 1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232605.css b/dot-line-system/.history/src/style_20250522232605.css new file mode 100644 index 0000000..9e6d4c1 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232605.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: 0; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: 0; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232609.css b/dot-line-system/.history/src/style_20250522232609.css new file mode 100644 index 0000000..73b62df --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232609.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: -1; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: 0; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250522232619.css b/dot-line-system/.history/src/style_20250522232619.css new file mode 100644 index 0000000..dcc3e88 --- /dev/null +++ b/dot-line-system/.history/src/style_20250522232619.css @@ -0,0 +1,200 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: -1; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + /* padding-top: 2rem; */ + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250523080846.css b/dot-line-system/.history/src/style_20250523080846.css new file mode 100644 index 0000000..266c32c --- /dev/null +++ b/dot-line-system/.history/src/style_20250523080846.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: -1; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .smooth-scroll { + transition: scroll-left 0.3s ease-out; /* Add easing on scroll-left */ +} + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250523080941.css b/dot-line-system/.history/src/style_20250523080941.css new file mode 100644 index 0000000..266c32c --- /dev/null +++ b/dot-line-system/.history/src/style_20250523080941.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: -1; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .smooth-scroll { + transition: scroll-left 0.3s ease-out; /* Add easing on scroll-left */ +} + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250523081026.css b/dot-line-system/.history/src/style_20250523081026.css new file mode 100644 index 0000000..2b05fcd --- /dev/null +++ b/dot-line-system/.history/src/style_20250523081026.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: -1; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .smooth-scroll { + transition: scroll-left 1s ease-out; /* Add easing on scroll-left */ +} + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/.history/src/style_20250523081040.css b/dot-line-system/.history/src/style_20250523081040.css new file mode 100644 index 0000000..9d13415 --- /dev/null +++ b/dot-line-system/.history/src/style_20250523081040.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: -1; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .smooth-scroll { + transition: scroll-left 0.5s ease-out; /* Add easing on scroll-left */ +} + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/bun.lock b/dot-line-system/bun.lock new file mode 100644 index 0000000..c575191 --- /dev/null +++ b/dot-line-system/bun.lock @@ -0,0 +1,128 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "dot-line-system", + "dependencies": { + "gsap": "./gsap-bonus.tgz", + }, + "devDependencies": { + "typescript": "~5.7.2", + "vite": "^6.2.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.2", "", { "os": "android", "cpu": "arm" }, "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.2", "", { "os": "android", "cpu": "x64" }, "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.2", "", { "os": "linux", "cpu": "arm" }, "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.2", "", { "os": "linux", "cpu": "x64" }, "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.2", "", { "os": "none", "cpu": "arm64" }, "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.2", "", { "os": "none", "cpu": "x64" }, "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.39.0", "", { "os": "android", "cpu": "arm" }, "sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.39.0", "", { "os": "android", "cpu": "arm64" }, "sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.39.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.39.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.39.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.39.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.39.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.39.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.39.0", "", { "os": "linux", "cpu": "none" }, "sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.39.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.39.0", "", { "os": "linux", "cpu": "none" }, "sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.39.0", "", { "os": "linux", "cpu": "none" }, "sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.39.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.39.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.39.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug=="], + + "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + + "esbuild": ["esbuild@0.25.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.2", "@esbuild/android-arm": "0.25.2", "@esbuild/android-arm64": "0.25.2", "@esbuild/android-x64": "0.25.2", "@esbuild/darwin-arm64": "0.25.2", "@esbuild/darwin-x64": "0.25.2", "@esbuild/freebsd-arm64": "0.25.2", "@esbuild/freebsd-x64": "0.25.2", "@esbuild/linux-arm": "0.25.2", "@esbuild/linux-arm64": "0.25.2", "@esbuild/linux-ia32": "0.25.2", "@esbuild/linux-loong64": "0.25.2", "@esbuild/linux-mips64el": "0.25.2", "@esbuild/linux-ppc64": "0.25.2", "@esbuild/linux-riscv64": "0.25.2", "@esbuild/linux-s390x": "0.25.2", "@esbuild/linux-x64": "0.25.2", "@esbuild/netbsd-arm64": "0.25.2", "@esbuild/netbsd-x64": "0.25.2", "@esbuild/openbsd-arm64": "0.25.2", "@esbuild/openbsd-x64": "0.25.2", "@esbuild/sunos-x64": "0.25.2", "@esbuild/win32-arm64": "0.25.2", "@esbuild/win32-ia32": "0.25.2", "@esbuild/win32-x64": "0.25.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "gsap": ["gsap@./gsap-bonus.tgz", {}], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], + + "rollup": ["rollup@4.39.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.39.0", "@rollup/rollup-android-arm64": "4.39.0", "@rollup/rollup-darwin-arm64": "4.39.0", "@rollup/rollup-darwin-x64": "4.39.0", "@rollup/rollup-freebsd-arm64": "4.39.0", "@rollup/rollup-freebsd-x64": "4.39.0", "@rollup/rollup-linux-arm-gnueabihf": "4.39.0", "@rollup/rollup-linux-arm-musleabihf": "4.39.0", "@rollup/rollup-linux-arm64-gnu": "4.39.0", "@rollup/rollup-linux-arm64-musl": "4.39.0", "@rollup/rollup-linux-loongarch64-gnu": "4.39.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.39.0", "@rollup/rollup-linux-riscv64-gnu": "4.39.0", "@rollup/rollup-linux-riscv64-musl": "4.39.0", "@rollup/rollup-linux-s390x-gnu": "4.39.0", "@rollup/rollup-linux-x64-gnu": "4.39.0", "@rollup/rollup-linux-x64-musl": "4.39.0", "@rollup/rollup-win32-arm64-msvc": "4.39.0", "@rollup/rollup-win32-ia32-msvc": "4.39.0", "@rollup/rollup-win32-x64-msvc": "4.39.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "vite": ["vite@6.2.5", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA=="], + } +} diff --git a/dot-line-system/index.html b/dot-line-system/index.html new file mode 100644 index 0000000..415baa6 --- /dev/null +++ b/dot-line-system/index.html @@ -0,0 +1,25 @@ + + + + + + + + Life Line + + + + + + + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/dot-line-system/package-lock.json b/dot-line-system/package-lock.json new file mode 100644 index 0000000..139d2ca --- /dev/null +++ b/dot-line-system/package-lock.json @@ -0,0 +1,1006 @@ +{ + "name": "dot-line-system", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dot-line-system", + "version": "0.0.0", + "devDependencies": { + "typescript": "~5.7.2", + "vite": "^6.3.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/dot-line-system/package.json b/dot-line-system/package.json new file mode 100644 index 0000000..882780f --- /dev/null +++ b/dot-line-system/package.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/public/ScrollTrigger.min.js b/dot-line-system/public/ScrollTrigger.min.js new file mode 100644 index 0000000..01c8533 --- /dev/null +++ b/dot-line-system/public/ScrollTrigger.min.js @@ -0,0 +1,11 @@ +/*! + * ScrollTrigger 3.12.7 + * https://gsap.com + * + * @license Copyright 2025, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license or for Club GSAP members, the agreement issued with that membership. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function _defineProperties(e,t){for(var r=0;r=Math.abs(r)?t:r}function O(){(Ae=Te.core.globals().ScrollTrigger)&&Ae.core&&function _integrate(){var e=Ae.core,r=e.bridge||{},t=e._scrollers,n=e._proxies;t.push.apply(t,qe),n.push.apply(n,Ie),qe=t,Ie=n,o=function _bridge(e,t){return r[e](t)}}()}function P(e){return Te=e||r(),!Ce&&Te&&"undefined"!=typeof document&&document.body&&(Se=window,Ee=(ke=document).documentElement,Pe=ke.body,t=[Se,ke,Ee,Pe],Te.utils.clamp,Re=Te.core.context||function(){},Oe="onpointerenter"in Pe?"pointer":"mouse",Me=k.isTouch=Se.matchMedia&&Se.matchMedia("(hover: none), (pointer: coarse)").matches?1:"ontouchstart"in Se||0=i,n=Math.abs(t)>=i;S&&(r||n)&&S(se,e,t,me,ye),r&&(m&&0Math.abs(t)?"x":"y",oe=!0),"y"!==ae&&(me[2]+=e,se._vx.update(e,!0)),"x"!==ae&&(ye[2]+=t,se._vy.update(t,!0)),n?ee=ee||requestAnimationFrame(ff):ff()}function jf(e){if(!df(e,1)){var t=(e=M(e,s)).clientX,r=e.clientY,n=t-se.x,i=r-se.y,o=se.isDragging;se.x=t,se.y=r,(o||(n||i)&&(Math.abs(se.startX-t)>=a||Math.abs(se.startY-r)>=a))&&(re=o?2:1,o||(se.isDragging=!0),hf(n,i))}}function mf(e){return e.touches&&1=e)return a[n];return a[n-1]}for(n=a.length,e+=r;n--;)if(a[n]<=e)return a[n];return a[0]}:function(e,t,r){void 0===r&&(r=.001);var n=o(e);return!t||Math.abs(n-e)r&&(n*=t/100),e=e.substr(0,r-1)),e=n+(e in H?H[e]*t:~e.indexOf("%")?parseFloat(e)*t/100:parseFloat(e)||0)}return e}function Db(e,t,r,n,i,o,a,s){var l=i.startColor,c=i.endColor,u=i.fontSize,f=i.indent,d=i.fontWeight,p=Xe.createElement("div"),g=La(r)||"fixed"===z(r,"pinType"),h=-1!==e.indexOf("scroller"),v=g?Je:r,b=-1!==e.indexOf("start"),m=b?l:c,y="border-color:"+m+";font-size:"+u+";color:"+m+";font-weight:"+d+";pointer-events:none;white-space:nowrap;font-family:sans-serif,Arial;z-index:1000;padding:4px 8px;border-width:0;border-style:solid;";return y+="position:"+((h||s)&&g?"fixed;":"absolute;"),!h&&!s&&g||(y+=(n===ze?q:I)+":"+(o+parseFloat(f))+"px;"),a&&(y+="box-sizing:border-box;text-align:left;width:"+a.offsetWidth+"px;"),p._isStart=b,p.setAttribute("class","gsap-marker-"+e+(t?" marker-"+t:"")),p.style.cssText=y,p.innerText=t||0===t?e+"-"+t:e,v.children[0]?v.insertBefore(p,v.children[0]):v.appendChild(p),p._offset=p["offset"+n.op.d2],X(p,0,n,b),p}function Ib(){return 34Je.clientWidth)||(qe.cache++,v?D=D||requestAnimationFrame(Z):Z(),st||U("scrollStart"),st=at())}function Kb(){y=Ne.innerWidth,m=Ne.innerHeight}function Lb(e){qe.cache++,!0!==e&&(Ke||h||Xe.fullscreenElement||Xe.webkitFullscreenElement||b&&y===Ne.innerWidth&&!(Math.abs(Ne.innerHeight-m)>.25*Ne.innerHeight))||c.restart(!0)}function Ob(){return xb(ne,"scrollEnd",Ob)||Et(!0)}function Rb(e){for(var t=0;tt,n=e._startClamp&&e.start>=t;(r||n)&&e.setPositions(n?t-1:e.start,r?Math.max(n?t:e.start+1,t):e.end,!0)}),Zb(!1),et=0,r.forEach(function(e){return e&&e.render&&e.render(-1)}),qe.forEach(function(e){Ta(e)&&(e.smooth&&requestAnimationFrame(function(){return e.target.style.scrollBehavior="smooth"}),e.rec&&e(e.rec))}),Tb(w,1),c.pause(),kt++,Z(rt=2),Ct.forEach(function(e){return Ta(e.vars.onRefresh)&&e.vars.onRefresh(e)}),rt=ne.isRefreshing=!1,U("refresh")}else wb(ne,"scrollEnd",Ob)},Q=0,Pt=1,Z=function _updateAll(e){if(2===e||!rt&&!S){ne.isUpdating=!0,it&&it.update(0);var t=Ct.length,r=at(),n=50<=r-R,i=t&&Ct[0].scroll();if(Pt=i=Qa(be,he)){if(oe&&Ae()&&!de)for(o=oe.parentNode;o&&o!==Je;)o._pinOffset&&(R-=o._pinOffset,L-=o._pinOffset),o=o.parentNode}else i=mb(ae),s=he===ze,a=Ae(),j=parseFloat(G(he.a))+_,!y&&1=L})},Ce.update=function(e,t,r){if(!de||r||e){var n,i,o,a,s,l,c,u=!0===rt?re:Ce.scroll(),f=e?0:(u-R)/N,d=f<0?0:1u+(u-B)/(at()-Ge)*P&&(d=.9999)),d!==p&&Ce.enabled){if(a=(s=(n=Ce.isActive=!!d&&d<1)!=(!!p&&p<1))||!!d!=!!p,Ce.direction=p=Qa(be,he),fe)if(e||!n&&!l)oc(ae,U);else{var g=wt(ae,!0),h=u-R;oc(ae,Je,g.top+(he===ze?h:0)+xt,g.left+(he===ze?0:h)+xt)}Mt(n||l?W:V),$&&d<1&&n||b(j+(1!==d||l?0:Q))}}else b(Ia(j+Q*d));!ue||A.tween||Ke||ot||te.restart(!0),S&&(s||ce&&d&&(d<1||!tt))&&Ve(S.targets).forEach(function(e){return e.classList[n||ce?"add":"remove"](S.className)}),!C||ve||e||C(Ce),a&&!Ke?(ve&&(c&&("complete"===o?O.pause().totalProgress(1):"reset"===o?O.restart(!0).pause():"restart"===o?O.restart(!0):O[o]()),C&&C(Ce)),!s&&tt||(k&&s&&Xa(Ce,k),xe[i]&&Xa(Ce,xe[i]),ce&&(1===d?Ce.kill(!1,1):xe[i]=0),s||xe[i=1===d?1:3]&&Xa(Ce,xe[i])),pe&&!n&&Math.abs(Ce.getVelocity())>(Ua(pe)?pe:2500)&&(Wa(Ce.callbackAnimation),ee?ee.progress(1):Wa(O,"reverse"===o?1:!d,1))):ve&&C&&!Ke&&C(Ce)}if(x){var v=de?u/de.duration()*(de._caScrollDist||0):u;y(v+(Y._isFlipped?1:0)),x(v)}T&&T(-u/de.duration()*(de._caScrollDist||0))}},Ce.enable=function(e,t){Ce.enabled||(Ce.enabled=!0,wb(be,"resize",Lb),me||wb(be,"scroll",Jb),Se&&wb(ScrollTrigger,"refreshInit",Se),!1!==e&&(Ce.progress=Oe=0,D=B=Pe=Ae()),!1!==t&&Ce.refresh())},Ce.getTween=function(e){return e&&A?A.tween:ee},Ce.setPositions=function(e,t,r,n){if(de){var i=de.scrollTrigger,o=de.duration(),a=i.end-i.start;e=i.start+a*e/o,t=i.start+a*t/o}Ce.refresh(!1,!1,{start:Da(e,r&&!!Ce._startClamp),end:Da(t,r&&!!Ce._endClamp)},n),Ce.update()},Ce.adjustPinSpacing=function(e){if(Z&&e){var t=Z.indexOf(he.d)+1;Z[t]=parseFloat(Z[t])+e+xt,Z[1]=parseFloat(Z[1])+e+xt,Mt(Z)}},Ce.disable=function(e,t){if(Ce.enabled&&(!1!==e&&Ce.revert(!0,!0),Ce.enabled=Ce.isActive=!1,t||ee&&ee.pause(),re=0,n&&(n.uncache=1),Se&&xb(ScrollTrigger,"refreshInit",Se),te&&(te.pause(),A.tween&&A.tween.kill()&&(A.tween=0)),!me)){for(var r=Ct.length;r--;)if(Ct[r].scroller===be&&Ct[r]!==Ce)return;xb(be,"resize",Lb),me||xb(be,"scroll",Jb)}},Ce.kill=function(e,t){Ce.disable(e,t),ee&&!t&&ee.kill(),a&&delete St[a];var r=Ct.indexOf(Ce);0<=r&&Ct.splice(r,1),r===Qe&&0o&&(b()>o?a.progress(1)&&b(o):a.resetTo("scrollY",o))}Va(e)||(e={}),e.preventDefault=e.isNormalizer=e.allowClicks=!0,e.type||(e.type="wheel,touch"),e.debounce=!!e.debounce,e.id=e.id||"normalizer";var n,o,l,i,a,c,u,s,f=e.normalizeScrollX,t=e.momentum,r=e.allowNestedScroll,d=e.onRelease,p=J(e.target)||We,g=He.core.globals().ScrollSmoother,h=g&&g.get(),v=E&&(e.content&&J(e.content)||h&&!1!==e.content&&!h.smooth()&&h.content()),b=K(p,ze),m=K(p,Fe),y=1,x=(k.isTouch&&Ne.visualViewport?Ne.visualViewport.scale*Ne.visualViewport.width:Ne.outerWidth)/Ne.innerWidth,w=0,_=Ta(t)?function(){return t(n)}:function(){return t||2.8},T=xc(p,e.type,!0,r),C=Ha,S=Ha;return v&&He.set(v,{y:"+=0"}),e.ignoreCheck=function(e){return E&&"touchmove"===e.type&&function ignoreDrag(){if(i){requestAnimationFrame(Bq);var e=Ia(n.deltaY/2),t=S(b.v-e);if(v&&t!==b.v+b.offset){b.offset=t-b.v;var r=Ia((parseFloat(v&&v._gsap.y)||0)-b.offset);v.style.transform="matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, "+r+", 0, 1)",v._gsap.y=r+"px",b.cacheID=qe.cache,Z()}return!0}b.offset&&Fq(),i=!0}()||1.05=o||o-1<=r)&&He.to({},{onUpdate:Lq,duration:i})}else s.restart(!0);d&&d(e)},e.onWheel=function(){a._ts&&a.pause(),1e3 gsap || (typeof(window) !== \"undefined\" && (gsap = window.gsap) && gsap.registerPlugin && gsap),\n\t_startup = 1,\n\t_observers = [],\n\t_scrollers = [],\n\t_proxies = [],\n\t_getTime = Date.now,\n\t_bridge = (name, value) => value,\n\t_integrate = () => {\n\t\tlet core = ScrollTrigger.core,\n\t\t\tdata = core.bridge || {},\n\t\t\tscrollers = core._scrollers,\n\t\t\tproxies = core._proxies;\n\t\tscrollers.push(..._scrollers);\n\t\tproxies.push(..._proxies);\n\t\t_scrollers = scrollers;\n\t\t_proxies = proxies;\n\t\t_bridge = (name, value) => data[name](value);\n\t},\n\t_getProxyProp = (element, property) => ~_proxies.indexOf(element) && _proxies[_proxies.indexOf(element) + 1][property],\n\t_isViewport = el => !!~_root.indexOf(el),\n\t_addListener = (element, type, func, passive, capture) => element.addEventListener(type, func, {passive: passive !== false, capture: !!capture}),\n\t_removeListener = (element, type, func, capture) => element.removeEventListener(type, func, !!capture),\n\t_scrollLeft = \"scrollLeft\",\n\t_scrollTop = \"scrollTop\",\n\t_onScroll = () => (_normalizer && _normalizer.isPressed) || _scrollers.cache++,\n\t_scrollCacheFunc = (f, doNotCache) => {\n\t\tlet cachingFunc = value => { // since reading the scrollTop/scrollLeft/pageOffsetY/pageOffsetX can trigger a layout, this function allows us to cache the value so it only gets read fresh after a \"scroll\" event fires (or while we're refreshing because that can lengthen the page and alter the scroll position). when \"soft\" is true, that means don't actually set the scroll, but cache the new value instead (useful in ScrollSmoother)\n\t\t\tif (value || value === 0) {\n\t\t\t\t_startup && (_win.history.scrollRestoration = \"manual\"); // otherwise the new position will get overwritten by the browser onload.\n\t\t\t\tlet isNormalizing = _normalizer && _normalizer.isPressed;\n\t\t\t\tvalue = cachingFunc.v = Math.round(value) || (_normalizer && _normalizer.iOS ? 1 : 0); //TODO: iOS Bug: if you allow it to go to 0, Safari can start to report super strange (wildly inaccurate) touch positions!\n\t\t\t\tf(value);\n\t\t\t\tcachingFunc.cacheID = _scrollers.cache;\n\t\t\t\tisNormalizing && _bridge(\"ss\", value); // set scroll (notify ScrollTrigger so it can dispatch a \"scrollStart\" event if necessary\n\t\t\t} else if (doNotCache || _scrollers.cache !== cachingFunc.cacheID || _bridge(\"ref\")) {\n\t\t\t\tcachingFunc.cacheID = _scrollers.cache;\n\t\t\t\tcachingFunc.v = f();\n\t\t\t}\n\t\t\treturn cachingFunc.v + cachingFunc.offset;\n\t\t};\n\t\tcachingFunc.offset = 0;\n\t\treturn f && cachingFunc;\n\t},\n\t_horizontal = {s: _scrollLeft, p: \"left\", p2: \"Left\", os: \"right\", os2: \"Right\", d: \"width\", d2: \"Width\", a: \"x\", sc: _scrollCacheFunc(function(value) { return arguments.length ? _win.scrollTo(value, _vertical.sc()) : _win.pageXOffset || _doc[_scrollLeft] || _docEl[_scrollLeft] || _body[_scrollLeft] || 0})},\n\t_vertical = {s: _scrollTop, p: \"top\", p2: \"Top\", os: \"bottom\", os2: \"Bottom\", d: \"height\", d2: \"Height\", a: \"y\", op: _horizontal, sc: _scrollCacheFunc(function(value) { return arguments.length ? _win.scrollTo(_horizontal.sc(), value) : _win.pageYOffset || _doc[_scrollTop] || _docEl[_scrollTop] || _body[_scrollTop] || 0})},\n\t_getTarget = (t, self) => ((self && self._ctx && self._ctx.selector) || gsap.utils.toArray)(t)[0] || (typeof(t) === \"string\" && gsap.config().nullTargetWarn !== false ? console.warn(\"Element not found:\", t) : null),\n\n\t_getScrollFunc = (element, {s, sc}) => { // we store the scroller functions in an alternating sequenced Array like [element, verticalScrollFunc, horizontalScrollFunc, ...] so that we can minimize memory, maximize performance, and we also record the last position as a \".rec\" property in order to revert to that after refreshing to ensure things don't shift around.\n\t\t_isViewport(element) && (element = _doc.scrollingElement || _docEl);\n\t\tlet i = _scrollers.indexOf(element),\n\t\t\toffset = sc === _vertical.sc ? 1 : 2;\n\t\t!~i && (i = _scrollers.push(element) - 1);\n\t\t_scrollers[i + offset] || _addListener(element, \"scroll\", _onScroll); // clear the cache when a scroll occurs\n\t\tlet prev = _scrollers[i + offset],\n\t\t\tfunc = prev || (_scrollers[i + offset] = _scrollCacheFunc(_getProxyProp(element, s), true) || (_isViewport(element) ? sc : _scrollCacheFunc(function(value) { return arguments.length ? (element[s] = value) : element[s]; })));\n\t\tfunc.target = element;\n\t\tprev || (func.smooth = gsap.getProperty(element, \"scrollBehavior\") === \"smooth\"); // only set it the first time (don't reset every time a scrollFunc is requested because perhaps it happens during a refresh() when it's disabled in ScrollTrigger.\n\t\treturn func;\n\t},\n\t_getVelocityProp = (value, minTimeRefresh, useDelta) => {\n\t\tlet v1 = value,\n\t\t\tv2 = value,\n\t\t\tt1 = _getTime(),\n\t\t\tt2 = t1,\n\t\t\tmin = minTimeRefresh || 50,\n\t\t\tdropToZeroTime = Math.max(500, min * 3),\n\t\t\tupdate = (value, force) => {\n\t\t\t\tlet t = _getTime();\n\t\t\t\tif (force || t - t1 > min) {\n\t\t\t\t\tv2 = v1;\n\t\t\t\t\tv1 = value;\n\t\t\t\t\tt2 = t1;\n\t\t\t\t\tt1 = t;\n\t\t\t\t} else if (useDelta) {\n\t\t\t\t\tv1 += value;\n\t\t\t\t} else { // not totally necessary, but makes it a bit more accurate by adjusting the v1 value according to the new slope. This way we're not just ignoring the incoming data. Removing for now because it doesn't seem to make much practical difference and it's probably not worth the kb.\n\t\t\t\t\tv1 = v2 + (value - v2) / (t - t2) * (t1 - t2);\n\t\t\t\t}\n\t\t\t},\n\t\t\treset = () => { v2 = v1 = useDelta ? 0 : v1; t2 = t1 = 0; },\n\t\t\tgetVelocity = latestValue => {\n\t\t\t\tlet tOld = t2,\n\t\t\t\t\tvOld = v2,\n\t\t\t\t\tt = _getTime();\n\t\t\t\t(latestValue || latestValue === 0) && latestValue !== v1 && update(latestValue);\n\t\t\t\treturn (t1 === t2 || t - t2 > dropToZeroTime) ? 0 : (v1 + (useDelta ? vOld : -vOld)) / ((useDelta ? t : t1) - tOld) * 1000;\n\t\t\t};\n\t\treturn {update, reset, getVelocity};\n\t},\n\t_getEvent = (e, preventDefault) => {\n\t\tpreventDefault && !e._gsapAllow && e.preventDefault();\n\t\treturn e.changedTouches ? e.changedTouches[0] : e;\n\t},\n\t_getAbsoluteMax = a => {\n\t\tlet max = Math.max(...a),\n\t\t\tmin = Math.min(...a);\n\t\treturn Math.abs(max) >= Math.abs(min) ? max : min;\n\t},\n\t_setScrollTrigger = () => {\n\t\tScrollTrigger = gsap.core.globals().ScrollTrigger;\n\t\tScrollTrigger && ScrollTrigger.core && _integrate();\n\t},\n\t_initCore = core => {\n\t\tgsap = core || _getGSAP();\n\t\tif (!_coreInitted && gsap && typeof(document) !== \"undefined\" && document.body) {\n\t\t\t_win = window;\n\t\t\t_doc = document;\n\t\t\t_docEl = _doc.documentElement;\n\t\t\t_body = _doc.body;\n\t\t\t_root = [_win, _doc, _docEl, _body];\n\t\t\t_clamp = gsap.utils.clamp;\n\t\t\t_context = gsap.core.context || function() {};\n\t\t\t_pointerType = \"onpointerenter\" in _body ? \"pointer\" : \"mouse\";\n\t\t\t// isTouch is 0 if no touch, 1 if ONLY touch, and 2 if it can accommodate touch but also other types like mouse/pointer.\n\t\t\t_isTouch = Observer.isTouch = _win.matchMedia && _win.matchMedia(\"(hover: none), (pointer: coarse)\").matches ? 1 : (\"ontouchstart\" in _win || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0) ? 2 : 0;\n\t\t\t_eventTypes = Observer.eventTypes = (\"ontouchstart\" in _docEl ? \"touchstart,touchmove,touchcancel,touchend\" : !(\"onpointerdown\" in _docEl) ? \"mousedown,mousemove,mouseup,mouseup\" : \"pointerdown,pointermove,pointercancel,pointerup\").split(\",\");\n\t\t\tsetTimeout(() => _startup = 0, 500);\n\t\t\t_setScrollTrigger();\n\t\t\t_coreInitted = 1;\n\t\t}\n\t\treturn _coreInitted;\n\t};\n\n_horizontal.op = _vertical;\n_scrollers.cache = 0;\n\nexport class Observer {\n\tconstructor(vars) {\n\t\tthis.init(vars);\n\t}\n\n\tinit(vars) {\n\t\t_coreInitted || _initCore(gsap) || console.warn(\"Please gsap.registerPlugin(Observer)\");\n\t\tScrollTrigger || _setScrollTrigger();\n\t\tlet {tolerance, dragMinimum, type, target, lineHeight, debounce, preventDefault, onStop, onStopDelay, ignore, wheelSpeed, event, onDragStart, onDragEnd, onDrag, onPress, onRelease, onRight, onLeft, onUp, onDown, onChangeX, onChangeY, onChange, onToggleX, onToggleY, onHover, onHoverEnd, onMove, ignoreCheck, isNormalizer, onGestureStart, onGestureEnd, onWheel, onEnable, onDisable, onClick, scrollSpeed, capture, allowClicks, lockAxis, onLockAxis} = vars;\n\t\tthis.target = target = _getTarget(target) || _docEl;\n\t\tthis.vars = vars;\n\t\tignore && (ignore = gsap.utils.toArray(ignore));\n\t\ttolerance = tolerance || 1e-9;\n\t\tdragMinimum = dragMinimum || 0;\n\t\twheelSpeed = wheelSpeed || 1;\n\t\tscrollSpeed = scrollSpeed || 1;\n\t\ttype = type || \"wheel,touch,pointer\";\n\t\tdebounce = debounce !== false;\n\t\tlineHeight || (lineHeight = parseFloat(_win.getComputedStyle(_body).lineHeight) || 22); // note: browser may report \"normal\", so default to 22.\n\t\tlet id, onStopDelayedCall, dragged, moved, wheeled, locked, axis,\n\t\t\tself = this,\n\t\t\tprevDeltaX = 0,\n\t\t\tprevDeltaY = 0,\n\t\t\tpassive = vars.passive || (!preventDefault && vars.passive !== false),\n\t\t\tscrollFuncX = _getScrollFunc(target, _horizontal),\n\t\t\tscrollFuncY = _getScrollFunc(target, _vertical),\n\t\t\tscrollX = scrollFuncX(),\n\t\t\tscrollY = scrollFuncY(),\n\t\t\tlimitToTouch = ~type.indexOf(\"touch\") && !~type.indexOf(\"pointer\") && _eventTypes[0] === \"pointerdown\", // for devices that accommodate mouse events and touch events, we need to distinguish.\n\t\t\tisViewport = _isViewport(target),\n\t\t\townerDoc = target.ownerDocument || _doc,\n\t\t\tdeltaX = [0, 0, 0], // wheel, scroll, pointer/touch\n\t\t\tdeltaY = [0, 0, 0],\n\t\t\tonClickTime = 0,\n\t\t\tclickCapture = () => onClickTime = _getTime(),\n\t\t\t_ignoreCheck = (e, isPointerOrTouch) => (self.event = e) && (ignore && ~ignore.indexOf(e.target)) || (isPointerOrTouch && limitToTouch && e.pointerType !== \"touch\") || (ignoreCheck && ignoreCheck(e, isPointerOrTouch)),\n\t\t\tonStopFunc = () => {\n\t\t\t\tself._vx.reset();\n\t\t\t\tself._vy.reset();\n\t\t\t\tonStopDelayedCall.pause();\n\t\t\t\tonStop && onStop(self);\n\t\t\t},\n\t\t\tupdate = () => {\n\t\t\t\tlet dx = self.deltaX = _getAbsoluteMax(deltaX),\n\t\t\t\t\tdy = self.deltaY = _getAbsoluteMax(deltaY),\n\t\t\t\t\tchangedX = Math.abs(dx) >= tolerance,\n\t\t\t\t\tchangedY = Math.abs(dy) >= tolerance;\n\t\t\t\tonChange && (changedX || changedY) && onChange(self, dx, dy, deltaX, deltaY); // in ScrollTrigger.normalizeScroll(), we need to know if it was touch/pointer so we need access to the deltaX/deltaY Arrays before we clear them out.\n\t\t\t\tif (changedX) {\n\t\t\t\t\tonRight && self.deltaX > 0 && onRight(self);\n\t\t\t\t\tonLeft && self.deltaX < 0 && onLeft(self);\n\t\t\t\t\tonChangeX && onChangeX(self);\n\t\t\t\t\tonToggleX && ((self.deltaX < 0) !== (prevDeltaX < 0)) && onToggleX(self);\n\t\t\t\t\tprevDeltaX = self.deltaX;\n\t\t\t\t\tdeltaX[0] = deltaX[1] = deltaX[2] = 0\n\t\t\t\t}\n\t\t\t\tif (changedY) {\n\t\t\t\t\tonDown && self.deltaY > 0 && onDown(self);\n\t\t\t\t\tonUp && self.deltaY < 0 && onUp(self);\n\t\t\t\t\tonChangeY && onChangeY(self);\n\t\t\t\t\tonToggleY && ((self.deltaY < 0) !== (prevDeltaY < 0)) && onToggleY(self);\n\t\t\t\t\tprevDeltaY = self.deltaY;\n\t\t\t\t\tdeltaY[0] = deltaY[1] = deltaY[2] = 0\n\t\t\t\t}\n\t\t\t\tif (moved || dragged) {\n\t\t\t\t\tonMove && onMove(self);\n\t\t\t\t\tif (dragged) {\n\t\t\t\t\t\tonDragStart && dragged === 1 && onDragStart(self);\n\t\t\t\t\t\tonDrag && onDrag(self);\n\t\t\t\t\t\tdragged = 0;\n\t\t\t\t\t}\n\t\t\t\t\tmoved = false;\n\t\t\t\t}\n\t\t\t\tlocked && !(locked = false) && onLockAxis && onLockAxis(self);\n\t\t\t\tif (wheeled) {\n\t\t\t\t\tonWheel(self);\n\t\t\t\t\twheeled = false;\n\t\t\t\t}\n\t\t\t\tid = 0;\n\t\t\t},\n\t\t\tonDelta = (x, y, index) => {\n\t\t\t\tdeltaX[index] += x;\n\t\t\t\tdeltaY[index] += y;\n\t\t\t\tself._vx.update(x);\n\t\t\t\tself._vy.update(y);\n\t\t\t\tdebounce ? id || (id = requestAnimationFrame(update)) : update();\n\t\t\t},\n\t\t\tonTouchOrPointerDelta = (x, y) => {\n\t\t\t\tif (lockAxis && !axis) {\n\t\t\t\t\tself.axis = axis = Math.abs(x) > Math.abs(y) ? \"x\" : \"y\";\n\t\t\t\t\tlocked = true;\n\t\t\t\t}\n\t\t\t\tif (axis !== \"y\") {\n\t\t\t\t\tdeltaX[2] += x;\n\t\t\t\t\tself._vx.update(x, true); // update the velocity as frequently as possible instead of in the debounced function so that very quick touch-scrolls (flicks) feel natural. If it's the mouse/touch/pointer, force it so that we get snappy/accurate momentum scroll.\n\t\t\t\t}\n\t\t\t\tif (axis !== \"x\") {\n\t\t\t\t\tdeltaY[2] += y;\n\t\t\t\t\tself._vy.update(y, true);\n\t\t\t\t}\n\t\t\t\tdebounce ? id || (id = requestAnimationFrame(update)) : update();\n\t\t\t},\n\t\t\t_onDrag = e => {\n\t\t\t\tif (_ignoreCheck(e, 1)) {return;}\n\t\t\t\te = _getEvent(e, preventDefault);\n\t\t\t\tlet x = e.clientX,\n\t\t\t\t\ty = e.clientY,\n\t\t\t\t\tdx = x - self.x,\n\t\t\t\t\tdy = y - self.y,\n\t\t\t\t\tisDragging = self.isDragging;\n\t\t\t\tself.x = x;\n\t\t\t\tself.y = y;\n\t\t\t\tif (isDragging || ((dx || dy) && (Math.abs(self.startX - x) >= dragMinimum || Math.abs(self.startY - y) >= dragMinimum))) {\n\t\t\t\t\tdragged = isDragging ? 2 : 1; // dragged: 0 = not dragging, 1 = first drag, 2 = normal drag\n\t\t\t\t\tisDragging || (self.isDragging = true);\n\t\t\t\t\tonTouchOrPointerDelta(dx, dy);\n\t\t\t\t}\n\t\t\t},\n\t\t\t_onPress = self.onPress = e => {\n\t\t\t\tif (_ignoreCheck(e, 1) || (e && e.button)) {return;}\n\t\t\t\tself.axis = axis = null;\n\t\t\t\tonStopDelayedCall.pause();\n\t\t\t\tself.isPressed = true;\n\t\t\t\te = _getEvent(e); // note: may need to preventDefault(?) Won't side-scroll on iOS Safari if we do, though.\n\t\t\t\tprevDeltaX = prevDeltaY = 0;\n\t\t\t\tself.startX = self.x = e.clientX;\n\t\t\t\tself.startY = self.y = e.clientY;\n\t\t\t\tself._vx.reset(); // otherwise the t2 may be stale if the user touches and flicks super fast and releases in less than 2 requestAnimationFrame ticks, causing velocity to be 0.\n\t\t\t\tself._vy.reset();\n\t\t\t\t_addListener(isNormalizer ? target : ownerDoc, _eventTypes[1], _onDrag, passive, true);\n\t\t\t\tself.deltaX = self.deltaY = 0;\n\t\t\t\tonPress && onPress(self);\n\t\t\t},\n\t\t\t_onRelease = self.onRelease = e => {\n\t\t\t\tif (_ignoreCheck(e, 1)) {return;}\n\t\t\t\t_removeListener(isNormalizer ? target : ownerDoc, _eventTypes[1], _onDrag, true);\n\t\t\t\tlet isTrackingDrag = !isNaN(self.y - self.startY),\n\t\t\t\t\twasDragging = self.isDragging,\n\t\t\t\t\tisDragNotClick = wasDragging && (Math.abs(self.x - self.startX) > 3 || Math.abs(self.y - self.startY) > 3), // some touch devices need some wiggle room in terms of sensing clicks - the finger may move a few pixels.\n\t\t\t\t\teventData = _getEvent(e);\n\t\t\t\tif (!isDragNotClick && isTrackingDrag) {\n\t\t\t\t\tself._vx.reset();\n\t\t\t\t\tself._vy.reset();\n\t\t\t\t\t//if (preventDefault && allowClicks && self.isPressed) { // check isPressed because in a rare edge case, the inputObserver in ScrollTrigger may stopPropagation() on the press/drag, so the onRelease may get fired without the onPress/onDrag ever getting called, thus it could trigger a click to occur on a link after scroll-dragging it.\n\t\t\t\t\tif (preventDefault && allowClicks) {\n\t\t\t\t\t\tgsap.delayedCall(0.08, () => { // some browsers (like Firefox) won't trust script-generated clicks, so if the user tries to click on a video to play it, for example, it simply won't work. Since a regular \"click\" event will most likely be generated anyway (one that has its isTrusted flag set to true), we must slightly delay our script-generated click so that the \"real\"/trusted one is prioritized. Remember, when there are duplicate events in quick succession, we suppress all but the first one. Some browsers don't even trigger the \"real\" one at all, so our synthetic one is a safety valve that ensures that no matter what, a click event does get dispatched.\n\t\t\t\t\t\t\tif (_getTime() - onClickTime > 300 && !e.defaultPrevented) {\n\t\t\t\t\t\t\t\tif (e.target.click) { //some browsers (like mobile Safari) don't properly trigger the click event\n\t\t\t\t\t\t\t\t\te.target.click();\n\t\t\t\t\t\t\t\t} else if (ownerDoc.createEvent) {\n\t\t\t\t\t\t\t\t\tlet syntheticEvent = ownerDoc.createEvent(\"MouseEvents\");\n\t\t\t\t\t\t\t\t\tsyntheticEvent.initMouseEvent(\"click\", true, true, _win, 1, eventData.screenX, eventData.screenY, eventData.clientX, eventData.clientY, false, false, false, false, 0, null);\n\t\t\t\t\t\t\t\t\te.target.dispatchEvent(syntheticEvent);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tself.isDragging = self.isGesturing = self.isPressed = false;\n\t\t\t\tonStop && wasDragging && !isNormalizer && onStopDelayedCall.restart(true);\n\t\t\t\tdragged && update(); // in case debouncing, we don't want onDrag to fire AFTER onDragEnd().\n\t\t\t\tonDragEnd && wasDragging && onDragEnd(self);\n\t\t\t\tonRelease && onRelease(self, isDragNotClick);\n\t\t\t},\n\t\t\t_onGestureStart = e => e.touches && e.touches.length > 1 && (self.isGesturing = true) && onGestureStart(e, self.isDragging),\n\t\t\t_onGestureEnd = () => (self.isGesturing = false) || onGestureEnd(self),\n\t\t\tonScroll = e => {\n\t\t\t\tif (_ignoreCheck(e)) {return;}\n\t\t\t\tlet x = scrollFuncX(),\n\t\t\t\t\ty = scrollFuncY();\n\t\t\t\tonDelta((x - scrollX) * scrollSpeed, (y - scrollY) * scrollSpeed, 1);\n\t\t\t\tscrollX = x;\n\t\t\t\tscrollY = y;\n\t\t\t\tonStop && onStopDelayedCall.restart(true);\n\t\t\t},\n\t\t\t_onWheel = e => {\n\t\t\t\tif (_ignoreCheck(e)) {return;}\n\t\t\t\te = _getEvent(e, preventDefault);\n\t\t\t\tonWheel && (wheeled = true);\n\t\t\t\tlet multiplier = (e.deltaMode === 1 ? lineHeight : e.deltaMode === 2 ? _win.innerHeight : 1) * wheelSpeed;\n\t\t\t\tonDelta(e.deltaX * multiplier, e.deltaY * multiplier, 0);\n\t\t\t\tonStop && !isNormalizer && onStopDelayedCall.restart(true);\n\t\t\t},\n\t\t\t_onMove = e => {\n\t\t\t\tif (_ignoreCheck(e)) {return;}\n\t\t\t\tlet x = e.clientX,\n\t\t\t\t\ty = e.clientY,\n\t\t\t\t\tdx = x - self.x,\n\t\t\t\t\tdy = y - self.y;\n\t\t\t\tself.x = x;\n\t\t\t\tself.y = y;\n\t\t\t\tmoved = true;\n\t\t\t\tonStop && onStopDelayedCall.restart(true);\n\t\t\t\t(dx || dy) && onTouchOrPointerDelta(dx, dy);\n\t\t\t},\n\t\t\t_onHover = e => {self.event = e; onHover(self);},\n\t\t\t_onHoverEnd = e => {self.event = e; onHoverEnd(self);},\n\t\t\t_onClick = e => _ignoreCheck(e) || (_getEvent(e, preventDefault) && onClick(self));\n\n\t\tonStopDelayedCall = self._dc = gsap.delayedCall(onStopDelay || 0.25, onStopFunc).pause();\n\n\t\tself.deltaX = self.deltaY = 0;\n\t\tself._vx = _getVelocityProp(0, 50, true);\n\t\tself._vy = _getVelocityProp(0, 50, true);\n\t\tself.scrollX = scrollFuncX;\n\t\tself.scrollY = scrollFuncY;\n\t\tself.isDragging = self.isGesturing = self.isPressed = false;\n\t\t_context(this);\n\t\tself.enable = e => {\n\t\t\tif (!self.isEnabled) {\n\t\t\t\t_addListener(isViewport ? ownerDoc : target, \"scroll\", _onScroll);\n\t\t\t\ttype.indexOf(\"scroll\") >= 0 && _addListener(isViewport ? ownerDoc : target, \"scroll\", onScroll, passive, capture);\n\t\t\t\ttype.indexOf(\"wheel\") >= 0 && _addListener(target, \"wheel\", _onWheel, passive, capture);\n\t\t\t\tif ((type.indexOf(\"touch\") >= 0 && _isTouch) || type.indexOf(\"pointer\") >= 0) {\n\t\t\t\t\t_addListener(target, _eventTypes[0], _onPress, passive, capture);\n\t\t\t\t\t_addListener(ownerDoc, _eventTypes[2], _onRelease);\n\t\t\t\t\t_addListener(ownerDoc, _eventTypes[3], _onRelease);\n\t\t\t\t\tallowClicks && _addListener(target, \"click\", clickCapture, true, true);\n\t\t\t\t\tonClick && _addListener(target, \"click\", _onClick);\n\t\t\t\t\tonGestureStart && _addListener(ownerDoc, \"gesturestart\", _onGestureStart);\n\t\t\t\t\tonGestureEnd && _addListener(ownerDoc, \"gestureend\", _onGestureEnd);\n\t\t\t\t\tonHover && _addListener(target, _pointerType + \"enter\", _onHover);\n\t\t\t\t\tonHoverEnd && _addListener(target, _pointerType + \"leave\", _onHoverEnd);\n\t\t\t\t\tonMove && _addListener(target, _pointerType + \"move\", _onMove);\n\t\t\t\t}\n\t\t\t\tself.isEnabled = true;\n\t\t\t\tself.isDragging = self.isGesturing = self.isPressed = moved = dragged = false;\n\t\t\t\tself._vx.reset();\n\t\t\t\tself._vy.reset();\n\t\t\t\tscrollX = scrollFuncX();\n\t\t\t\tscrollY = scrollFuncY();\n\t\t\t\te && e.type && _onPress(e);\n\t\t\t\tonEnable && onEnable(self);\n\t\t\t}\n\t\t\treturn self;\n\t\t};\n\t\tself.disable = () => {\n\t\t\tif (self.isEnabled) {\n\t\t\t\t// only remove the _onScroll listener if there aren't any others that rely on the functionality.\n\t\t\t\t_observers.filter(o => o !== self && _isViewport(o.target)).length || _removeListener(isViewport ? ownerDoc : target, \"scroll\", _onScroll);\n\t\t\t\tif (self.isPressed) {\n\t\t\t\t\tself._vx.reset();\n\t\t\t\t\tself._vy.reset();\n\t\t\t\t\t_removeListener(isNormalizer ? target : ownerDoc, _eventTypes[1], _onDrag, true);\n\t\t\t\t}\n\t\t\t\t_removeListener(isViewport ? ownerDoc : target, \"scroll\", onScroll, capture);\n\t\t\t\t_removeListener(target, \"wheel\", _onWheel, capture);\n\t\t\t\t_removeListener(target, _eventTypes[0], _onPress, capture);\n\t\t\t\t_removeListener(ownerDoc, _eventTypes[2], _onRelease);\n\t\t\t\t_removeListener(ownerDoc, _eventTypes[3], _onRelease);\n\t\t\t\t_removeListener(target, \"click\", clickCapture, true);\n\t\t\t\t_removeListener(target, \"click\", _onClick);\n\t\t\t\t_removeListener(ownerDoc, \"gesturestart\", _onGestureStart);\n\t\t\t\t_removeListener(ownerDoc, \"gestureend\", _onGestureEnd);\n\t\t\t\t_removeListener(target, _pointerType + \"enter\", _onHover);\n\t\t\t\t_removeListener(target, _pointerType + \"leave\", _onHoverEnd);\n\t\t\t\t_removeListener(target, _pointerType + \"move\", _onMove);\n\t\t\t\tself.isEnabled = self.isPressed = self.isDragging = false;\n\t\t\t\tonDisable && onDisable(self);\n\t\t\t}\n\t\t};\n\n\t\tself.kill = self.revert = () => {\n\t\t\tself.disable();\n\t\t\tlet i = _observers.indexOf(self);\n\t\t\ti >= 0 && _observers.splice(i, 1);\n\t\t\t_normalizer === self && (_normalizer = 0);\n\t\t}\n\n\t\t_observers.push(self);\n\t\tisNormalizer && _isViewport(target) && (_normalizer = self);\n\n\t\tself.enable(event);\n\t}\n\n\tget velocityX() {\n\t\treturn this._vx.getVelocity();\n\t}\n\tget velocityY() {\n\t\treturn this._vy.getVelocity();\n\t}\n\n}\n\nObserver.version = \"3.12.7\";\nObserver.create = vars => new Observer(vars);\nObserver.register = _initCore;\nObserver.getAll = () => _observers.slice();\nObserver.getById = id => _observers.filter(o => o.vars.id === id)[0];\n\n_getGSAP() && gsap.registerPlugin(Observer);\n\nexport { Observer as default, _isViewport, _scrollers, _getScrollFunc, _getProxyProp, _proxies, _getVelocityProp, _vertical, _horizontal, _getTarget };","/*!\n * ScrollTrigger 3.12.7\n * https://gsap.com\n *\n * @license Copyright 2008-2025, GreenSock. All rights reserved.\n * Subject to the terms at https://gsap.com/standard-license or for\n * Club GSAP members, the agreement issued with that membership.\n * @author: Jack Doyle, jack@greensock.com\n*/\n/* eslint-disable */\n\nimport { Observer, _getTarget, _vertical, _horizontal, _scrollers, _proxies, _getScrollFunc, _getProxyProp, _getVelocityProp } from \"./Observer.js\";\n\nlet gsap, _coreInitted, _win, _doc, _docEl, _body, _root, _resizeDelay, _toArray, _clamp, _time2, _syncInterval, _refreshing, _pointerIsDown, _transformProp, _i, _prevWidth, _prevHeight, _autoRefresh, _sort, _suppressOverwrites, _ignoreResize, _normalizer, _ignoreMobileResize, _baseScreenHeight, _baseScreenWidth, _fixIOSBug, _context, _scrollRestoration, _div100vh, _100vh, _isReverted, _clampingMax,\n\t_limitCallbacks, // if true, we'll only trigger callbacks if the active state toggles, so if you scroll immediately past both the start and end positions of a ScrollTrigger (thus inactive to inactive), neither its onEnter nor onLeave will be called. This is useful during startup.\n\t_startup = 1,\n\t_getTime = Date.now,\n\t_time1 = _getTime(),\n\t_lastScrollTime = 0,\n\t_enabled = 0,\n\t_parseClamp = (value, type, self) => {\n\t\tlet clamp = (_isString(value) && (value.substr(0, 6) === \"clamp(\" || value.indexOf(\"max\") > -1));\n\t\tself[\"_\" + type + \"Clamp\"] = clamp;\n\t\treturn clamp ? value.substr(6, value.length - 7) : value;\n\t},\n\t_keepClamp = (value, clamp) => clamp && (!_isString(value) || value.substr(0, 6) !== \"clamp(\") ? \"clamp(\" + value + \")\" : value,\n\t_rafBugFix = () => _enabled && requestAnimationFrame(_rafBugFix), // in some browsers (like Firefox), screen repaints weren't consistent unless we had SOMETHING queued up in requestAnimationFrame()! So this just creates a super simple loop to keep it alive and smooth out repaints.\n\t_pointerDownHandler = () => _pointerIsDown = 1,\n\t_pointerUpHandler = () => _pointerIsDown = 0,\n\t_passThrough = v => v,\n\t_round = value => Math.round(value * 100000) / 100000 || 0,\n\t_windowExists = () => typeof(window) !== \"undefined\",\n\t_getGSAP = () => gsap || (_windowExists() && (gsap = window.gsap) && gsap.registerPlugin && gsap),\n\t_isViewport = e => !!~_root.indexOf(e),\n\t_getViewportDimension = dimensionProperty => (dimensionProperty === \"Height\" ? _100vh : _win[\"inner\" + dimensionProperty]) || _docEl[\"client\" + dimensionProperty] || _body[\"client\" + dimensionProperty],\n\t_getBoundsFunc = element => _getProxyProp(element, \"getBoundingClientRect\") || (_isViewport(element) ? () => {_winOffsets.width = _win.innerWidth; _winOffsets.height = _100vh; return _winOffsets;} : () => _getBounds(element)),\n\t_getSizeFunc = (scroller, isViewport, {d, d2, a}) => (a = _getProxyProp(scroller, \"getBoundingClientRect\")) ? () => a()[d] : () => (isViewport ? _getViewportDimension(d2) : scroller[\"client\" + d2]) || 0,\n\t_getOffsetsFunc = (element, isViewport) => !isViewport || ~_proxies.indexOf(element) ? _getBoundsFunc(element) : () => _winOffsets,\n\t_maxScroll = (element, {s, d2, d, a}) => Math.max(0, (s = \"scroll\" + d2) && (a = _getProxyProp(element, s)) ? a() - _getBoundsFunc(element)()[d] : _isViewport(element) ? (_docEl[s] || _body[s]) - _getViewportDimension(d2) : element[s] - element[\"offset\" + d2]),\n\t_iterateAutoRefresh = (func, events) => {\n\t\tfor (let i = 0; i < _autoRefresh.length; i += 3) {\n\t\t\t(!events || ~events.indexOf(_autoRefresh[i+1])) && func(_autoRefresh[i], _autoRefresh[i+1], _autoRefresh[i+2]);\n\t\t}\n\t},\n\t_isString = value => typeof(value) === \"string\",\n\t_isFunction = value => typeof(value) === \"function\",\n\t_isNumber = value => typeof(value) === \"number\",\n\t_isObject = value => typeof(value) === \"object\",\n\t_endAnimation = (animation, reversed, pause) => animation && animation.progress(reversed ? 0 : 1) && pause && animation.pause(),\n\t_callback = (self, func) => {\n\t\tif (self.enabled) {\n\t\t\tlet result = self._ctx ? self._ctx.add(() => func(self)) : func(self);\n\t\t\tresult && result.totalTime && (self.callbackAnimation = result);\n\t\t}\n\t},\n\t_abs = Math.abs,\n\t_left = \"left\",\n\t_top = \"top\",\n\t_right = \"right\",\n\t_bottom = \"bottom\",\n\t_width = \"width\",\n\t_height = \"height\",\n\t_Right = \"Right\",\n\t_Left = \"Left\",\n\t_Top = \"Top\",\n\t_Bottom = \"Bottom\",\n\t_padding = \"padding\",\n\t_margin = \"margin\",\n\t_Width = \"Width\",\n\t_Height = \"Height\",\n\t_px = \"px\",\n\t_getComputedStyle = element => _win.getComputedStyle(element),\n\t_makePositionable = element => { // if the element already has position: absolute or fixed, leave that, otherwise make it position: relative\n\t\tlet position = _getComputedStyle(element).position;\n\t\telement.style.position = (position === \"absolute\" || position === \"fixed\") ? position : \"relative\";\n\t},\n\t_setDefaults = (obj, defaults) => {\n\t\tfor (let p in defaults) {\n\t\t\t(p in obj) || (obj[p] = defaults[p]);\n\t\t}\n\t\treturn obj;\n\t},\n\t_getBounds = (element, withoutTransforms) => {\n\t\tlet tween = withoutTransforms && _getComputedStyle(element)[_transformProp] !== \"matrix(1, 0, 0, 1, 0, 0)\" && gsap.to(element, {x: 0, y: 0, xPercent: 0, yPercent: 0, rotation: 0, rotationX: 0, rotationY: 0, scale: 1, skewX: 0, skewY: 0}).progress(1),\n\t\t\tbounds = element.getBoundingClientRect();\n\t\ttween && tween.progress(0).kill();\n\t\treturn bounds;\n\t},\n\t_getSize = (element, {d2}) => element[\"offset\" + d2] || element[\"client\" + d2] || 0,\n\t_getLabelRatioArray = timeline => {\n\t\tlet a = [],\n\t\t\tlabels = timeline.labels,\n\t\t\tduration = timeline.duration(),\n\t\t\tp;\n\t\tfor (p in labels) {\n\t\t\ta.push(labels[p] / duration);\n\t\t}\n\t\treturn a;\n\t},\n\t_getClosestLabel = animation => value => gsap.utils.snap(_getLabelRatioArray(animation), value),\n\t_snapDirectional = snapIncrementOrArray => {\n\t\tlet snap = gsap.utils.snap(snapIncrementOrArray),\n\t\t\ta = Array.isArray(snapIncrementOrArray) && snapIncrementOrArray.slice(0).sort((a, b) => a - b);\n\t\treturn a ? (value, direction, threshold= 1e-3) => {\n\t\t\tlet i;\n\t\t\tif (!direction) {\n\t\t\t\treturn snap(value);\n\t\t\t}\n\t\t\tif (direction > 0) {\n\t\t\t\tvalue -= threshold; // to avoid rounding errors. If we're too strict, it might snap forward, then immediately again, and again.\n\t\t\t\tfor (i = 0; i < a.length; i++) {\n\t\t\t\t\tif (a[i] >= value) {\n\t\t\t\t\t\treturn a[i];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn a[i-1];\n\t\t\t} else {\n\t\t\t\ti = a.length;\n\t\t\t\tvalue += threshold;\n\t\t\t\twhile (i--) {\n\t\t\t\t\tif (a[i] <= value) {\n\t\t\t\t\t\treturn a[i];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn a[0];\n\t\t} : (value, direction, threshold= 1e-3) => {\n\t\t\tlet snapped = snap(value);\n\t\t\treturn !direction || Math.abs(snapped - value) < threshold || ((snapped - value < 0) === direction < 0) ? snapped : snap(direction < 0 ? value - snapIncrementOrArray : value + snapIncrementOrArray);\n\t\t};\n\t},\n\t_getLabelAtDirection = timeline => (value, st) => _snapDirectional(_getLabelRatioArray(timeline))(value, st.direction),\n\t_multiListener = (func, element, types, callback) => types.split(\",\").forEach(type => func(element, type, callback)),\n\t_addListener = (element, type, func, nonPassive, capture) => element.addEventListener(type, func, {passive: !nonPassive, capture: !!capture}),\n\t_removeListener = (element, type, func, capture) => element.removeEventListener(type, func, !!capture),\n\t_wheelListener = (func, el, scrollFunc) => {\n\t\tscrollFunc = scrollFunc && scrollFunc.wheelHandler\n\t\tif (scrollFunc) {\n\t\t\tfunc(el, \"wheel\", scrollFunc);\n\t\t\tfunc(el, \"touchmove\", scrollFunc);\n\t\t}\n\t},\n\t_markerDefaults = {startColor: \"green\", endColor: \"red\", indent: 0, fontSize: \"16px\", fontWeight:\"normal\"},\n\t_defaults = {toggleActions: \"play\", anticipatePin: 0},\n\t_keywords = {top: 0, left: 0, center: 0.5, bottom: 1, right: 1},\n\t_offsetToPx = (value, size) => {\n\t\tif (_isString(value)) {\n\t\t\tlet eqIndex = value.indexOf(\"=\"),\n\t\t\t\trelative = ~eqIndex ? +(value.charAt(eqIndex-1) + 1) * parseFloat(value.substr(eqIndex + 1)) : 0;\n\t\t\tif (~eqIndex) {\n\t\t\t\t(value.indexOf(\"%\") > eqIndex) && (relative *= size / 100);\n\t\t\t\tvalue = value.substr(0, eqIndex-1);\n\t\t\t}\n\t\t\tvalue = relative + ((value in _keywords) ? _keywords[value] * size : ~value.indexOf(\"%\") ? parseFloat(value) * size / 100 : parseFloat(value) || 0);\n\t\t}\n\t\treturn value;\n\t},\n\t_createMarker = (type, name, container, direction, {startColor, endColor, fontSize, indent, fontWeight}, offset, matchWidthEl, containerAnimation) => {\n\t\tlet e = _doc.createElement(\"div\"),\n\t\t\tuseFixedPosition = _isViewport(container) || _getProxyProp(container, \"pinType\") === \"fixed\",\n\t\t\tisScroller = type.indexOf(\"scroller\") !== -1,\n\t\t\tparent = useFixedPosition ? _body : container,\n\t\t\tisStart = type.indexOf(\"start\") !== -1,\n\t\t\tcolor = isStart ? startColor : endColor,\n\t\t\tcss = \"border-color:\" + color + \";font-size:\" + fontSize + \";color:\" + color + \";font-weight:\" + fontWeight + \";pointer-events:none;white-space:nowrap;font-family:sans-serif,Arial;z-index:1000;padding:4px 8px;border-width:0;border-style:solid;\";\n\t\tcss += \"position:\" + ((isScroller || containerAnimation) && useFixedPosition ? \"fixed;\" : \"absolute;\");\n\t\t(isScroller || containerAnimation || !useFixedPosition) && (css += (direction === _vertical ? _right : _bottom) + \":\" + (offset + parseFloat(indent)) + \"px;\");\n\t\tmatchWidthEl && (css += \"box-sizing:border-box;text-align:left;width:\" + matchWidthEl.offsetWidth + \"px;\");\n\t\te._isStart = isStart;\n\t\te.setAttribute(\"class\", \"gsap-marker-\" + type + (name ? \" marker-\" + name : \"\"));\n\t\te.style.cssText = css;\n\t\te.innerText = name || name === 0 ? type + \"-\" + name : type;\n\t\tparent.children[0] ? parent.insertBefore(e, parent.children[0]) : parent.appendChild(e);\n\t\te._offset = e[\"offset\" + direction.op.d2];\n\t\t_positionMarker(e, 0, direction, isStart);\n\t\treturn e;\n\t},\n\t_positionMarker = (marker, start, direction, flipped) => {\n\t\tlet vars = {display: \"block\"},\n\t\t\tside = direction[flipped ? \"os2\" : \"p2\"],\n\t\t\toppositeSide = direction[flipped ? \"p2\" : \"os2\"];\n\t\tmarker._isFlipped = flipped;\n\t\tvars[direction.a + \"Percent\"] = flipped ? -100 : 0;\n\t\tvars[direction.a] = flipped ? \"1px\" : 0;\n\t\tvars[\"border\" + side + _Width] = 1;\n\t\tvars[\"border\" + oppositeSide + _Width] = 0;\n\t\tvars[direction.p] = start + \"px\";\n\t\tgsap.set(marker, vars);\n\t},\n\t_triggers = [],\n\t_ids = {},\n\t_rafID,\n\t_sync = () => _getTime() - _lastScrollTime > 34 && (_rafID || (_rafID = requestAnimationFrame(_updateAll))),\n\t_onScroll = () => { // previously, we tried to optimize performance by batching/deferring to the next requestAnimationFrame(), but discovered that Safari has a few bugs that make this unworkable (especially on iOS). See https://codepen.io/GreenSock/pen/16c435b12ef09c38125204818e7b45fc?editors=0010 and https://codepen.io/GreenSock/pen/JjOxYpQ/3dd65ccec5a60f1d862c355d84d14562?editors=0010 and https://codepen.io/GreenSock/pen/ExbrPNa/087cef197dc35445a0951e8935c41503?editors=0010\n\t\tif (!_normalizer || !_normalizer.isPressed || _normalizer.startX > _body.clientWidth) { // if the user is dragging the scrollbar, allow it.\n\t\t\t_scrollers.cache++;\n\t\t\tif (_normalizer) {\n\t\t\t\t_rafID || (_rafID = requestAnimationFrame(_updateAll));\n\t\t\t} else {\n\t\t\t\t_updateAll(); // Safari in particular (on desktop) NEEDS the immediate update rather than waiting for a requestAnimationFrame() whereas iOS seems to benefit from waiting for the requestAnimationFrame() tick, at least when normalizing. See https://codepen.io/GreenSock/pen/qBYozqO?editors=0110\n\t\t\t}\n\t\t\t_lastScrollTime || _dispatch(\"scrollStart\");\n\t\t\t_lastScrollTime = _getTime();\n\t\t}\n\t},\n\t_setBaseDimensions = () => {\n\t\t_baseScreenWidth = _win.innerWidth;\n\t\t_baseScreenHeight = _win.innerHeight;\n\t},\n\t_onResize = (force) => {\n\t\t_scrollers.cache++;\n\t\t(force === true || (!_refreshing && !_ignoreResize && !_doc.fullscreenElement && !_doc.webkitFullscreenElement && (!_ignoreMobileResize || _baseScreenWidth !== _win.innerWidth || Math.abs(_win.innerHeight - _baseScreenHeight) > _win.innerHeight * 0.25))) && _resizeDelay.restart(true);\n\t}, // ignore resizes triggered by refresh()\n\t_listeners = {},\n\t_emptyArray = [],\n\t_softRefresh = () => _removeListener(ScrollTrigger, \"scrollEnd\", _softRefresh) || _refreshAll(true),\n\t_dispatch = type => (_listeners[type] && _listeners[type].map(f => f())) || _emptyArray,\n\t_savedStyles = [], // when ScrollTrigger.saveStyles() is called, the inline styles are recorded in this Array in a sequential format like [element, cssText, gsCache, media]. This keeps it very memory-efficient and fast to iterate through.\n\t_revertRecorded = media => {\n\t\tfor (let i = 0; i < _savedStyles.length; i+=5) {\n\t\t\tif (!media || _savedStyles[i+4] && _savedStyles[i+4].query === media) {\n\t\t\t\t_savedStyles[i].style.cssText = _savedStyles[i+1];\n\t\t\t\t_savedStyles[i].getBBox && _savedStyles[i].setAttribute(\"transform\", _savedStyles[i+2] || \"\");\n\t\t\t\t_savedStyles[i+3].uncache = 1;\n\t\t\t}\n\t\t}\n\t},\n\t_revertAll = (kill, media) => {\n\t\tlet trigger;\n\t\tfor (_i = 0; _i < _triggers.length; _i++) {\n\t\t\ttrigger = _triggers[_i];\n\t\t\tif (trigger && (!media || trigger._ctx === media)) {\n\t\t\t\tif (kill) {\n\t\t\t\t\ttrigger.kill(1);\n\t\t\t\t} else {\n\t\t\t\t\ttrigger.revert(true, true);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t_isReverted = true;\n\t\tmedia && _revertRecorded(media);\n\t\tmedia || _dispatch(\"revert\");\n\t},\n\t_clearScrollMemory = (scrollRestoration, force) => { // zero-out all the recorded scroll positions. Don't use _triggers because if, for example, .matchMedia() is used to create some ScrollTriggers and then the user resizes and it removes ALL ScrollTriggers, and then go back to a size where there are ScrollTriggers, it would have kept the position(s) saved from the initial state.\n\t\t_scrollers.cache++;\n\t\t(force || !_refreshingAll) && _scrollers.forEach(obj => _isFunction(obj) && obj.cacheID++ && (obj.rec = 0));\n\t\t_isString(scrollRestoration) && (_win.history.scrollRestoration = _scrollRestoration = scrollRestoration);\n\t},\n\t_refreshingAll,\n\t_refreshID = 0,\n\t_queueRefreshID,\n\t_queueRefreshAll = () => { // we don't want to call _refreshAll() every time we create a new ScrollTrigger (for performance reasons) - it's better to batch them. Some frameworks dynamically load content and we can't rely on the window's \"load\" or \"DOMContentLoaded\" events to trigger it.\n\t\tif (_queueRefreshID !== _refreshID) {\n\t\t\tlet id = _queueRefreshID = _refreshID;\n\t\t\trequestAnimationFrame(() => id === _refreshID && _refreshAll(true));\n\t\t}\n\t},\n\t_refresh100vh = () => {\n\t\t_body.appendChild(_div100vh);\n\t\t_100vh = (!_normalizer && _div100vh.offsetHeight) || _win.innerHeight;\n\t\t_body.removeChild(_div100vh);\n\t},\n\t_hideAllMarkers = hide => _toArray(\".gsap-marker-start, .gsap-marker-end, .gsap-marker-scroller-start, .gsap-marker-scroller-end\").forEach(el => el.style.display = hide ? \"none\" : \"block\"),\n\t_refreshAll = (force, skipRevert) => {\n\t\t_docEl = _doc.documentElement; // some frameworks like Astro may cache the and replace it during routing, so we'll just re-record the _docEl and _body for safety (otherwise, the markers may not get added properly).\n\t\t_body = _doc.body;\n\t\t_root = [_win, _doc, _docEl, _body];\n\t\tif (_lastScrollTime && !force && !_isReverted) {\n\t\t\t_addListener(ScrollTrigger, \"scrollEnd\", _softRefresh);\n\t\t\treturn;\n\t\t}\n\t\t_refresh100vh();\n\t\t_refreshingAll = ScrollTrigger.isRefreshing = true;\n\t\t_scrollers.forEach(obj => _isFunction(obj) && ++obj.cacheID && (obj.rec = obj())); // force the clearing of the cache because some browsers take a little while to dispatch the \"scroll\" event and the user may have changed the scroll position and then called ScrollTrigger.refresh() right away\n\t\tlet refreshInits = _dispatch(\"refreshInit\");\n\t\t_sort && ScrollTrigger.sort();\n\t\tskipRevert || _revertAll();\n\t\t_scrollers.forEach(obj => {\n\t\t\tif (_isFunction(obj)) {\n\t\t\t\tobj.smooth && (obj.target.style.scrollBehavior = \"auto\"); // smooth scrolling interferes\n\t\t\t\tobj(0);\n\t\t\t}\n\t\t});\n\t\t_triggers.slice(0).forEach(t => t.refresh()) // don't loop with _i because during a refresh() someone could call ScrollTrigger.update() which would iterate through _i resulting in a skip.\n\t\t_isReverted = false;\n\t\t_triggers.forEach((t) => { // nested pins (pinnedContainer) with pinSpacing may expand the container, so we must accommodate that here.\n\t\t\tif (t._subPinOffset && t.pin) {\n\t\t\t\tlet prop = t.vars.horizontal ? \"offsetWidth\" : \"offsetHeight\",\n\t\t\t\t\toriginal = t.pin[prop];\n\t\t\t\tt.revert(true, 1);\n\t\t\t\tt.adjustPinSpacing(t.pin[prop] - original);\n\t\t\t\tt.refresh();\n\t\t\t}\n\t\t});\n\t\t_clampingMax = 1; // pinSpacing might be propping a page open, thus when we .setPositions() to clamp a ScrollTrigger's end we should leave the pinSpacing alone. That's what this flag is for.\n\t\t_hideAllMarkers(true);\n\t\t_triggers.forEach(t => { // the scroller's max scroll position may change after all the ScrollTriggers refreshed (like pinning could push it down), so we need to loop back and correct any with end: \"max\". Same for anything with a clamped end\n\t\t\tlet max = _maxScroll(t.scroller, t._dir),\n\t\t\t\tendClamp = t.vars.end === \"max\" || (t._endClamp && t.end > max),\n\t\t\t\tstartClamp = t._startClamp && t.start >= max;\n\t\t\t(endClamp || startClamp) && t.setPositions(startClamp ? max - 1 : t.start, endClamp ? Math.max(startClamp ? max : t.start + 1, max) : t.end, true);\n\t\t});\n\t\t_hideAllMarkers(false);\n\t\t_clampingMax = 0;\n\t\trefreshInits.forEach(result => result && result.render && result.render(-1)); // if the onRefreshInit() returns an animation (typically a gsap.set()), revert it. This makes it easy to put things in a certain spot before refreshing for measurement purposes, and then put things back.\n\t\t_scrollers.forEach(obj => {\n\t\t\tif (_isFunction(obj)) {\n\t\t\t\tobj.smooth && requestAnimationFrame(() => obj.target.style.scrollBehavior = \"smooth\");\n\t\t\t\tobj.rec && obj(obj.rec);\n\t\t\t}\n\t\t});\n\t\t_clearScrollMemory(_scrollRestoration, 1);\n\t\t_resizeDelay.pause();\n\t\t_refreshID++;\n\t\t_refreshingAll = 2;\n\t\t_updateAll(2);\n\t\t_triggers.forEach(t => _isFunction(t.vars.onRefresh) && t.vars.onRefresh(t));\n\t\t_refreshingAll = ScrollTrigger.isRefreshing = false;\n\t\t_dispatch(\"refresh\");\n\t},\n\t_lastScroll = 0,\n\t_direction = 1,\n\t_primary,\n\t_updateAll = (force) => {\n\t\tif (force === 2 || (!_refreshingAll && !_isReverted)) { // _isReverted could be true if, for example, a matchMedia() is in the process of executing. We don't want to update during the time everything is reverted.\n\t\t\tScrollTrigger.isUpdating = true;\n\t\t\t_primary && _primary.update(0); // ScrollSmoother uses refreshPriority -9999 to become the primary that gets updated before all others because it affects the scroll position.\n\t\t\tlet l = _triggers.length,\n\t\t\t\ttime = _getTime(),\n\t\t\t\trecordVelocity = time - _time1 >= 50,\n\t\t\t\tscroll = l && _triggers[0].scroll();\n\t\t\t_direction = _lastScroll > scroll ? -1 : 1;\n\t\t\t_refreshingAll || (_lastScroll = scroll);\n\t\t\tif (recordVelocity) {\n\t\t\t\tif (_lastScrollTime && !_pointerIsDown && time - _lastScrollTime > 200) {\n\t\t\t\t\t_lastScrollTime = 0;\n\t\t\t\t\t_dispatch(\"scrollEnd\");\n\t\t\t\t}\n\t\t\t\t_time2 = _time1;\n\t\t\t\t_time1 = time;\n\t\t\t}\n\t\t\tif (_direction < 0) {\n\t\t\t\t_i = l;\n\t\t\t\twhile (_i-- > 0) {\n\t\t\t\t\t_triggers[_i] && _triggers[_i].update(0, recordVelocity);\n\t\t\t\t}\n\t\t\t\t_direction = 1;\n\t\t\t} else {\n\t\t\t\tfor (_i = 0; _i < l; _i++) {\n\t\t\t\t\t_triggers[_i] && _triggers[_i].update(0, recordVelocity);\n\t\t\t\t}\n\t\t\t}\n\t\t\tScrollTrigger.isUpdating = false;\n\t\t}\n\t\t_rafID = 0;\n\t},\n\t_propNamesToCopy = [_left, _top, _bottom, _right, _margin + _Bottom, _margin + _Right, _margin + _Top, _margin + _Left, \"display\", \"flexShrink\", \"float\", \"zIndex\", \"gridColumnStart\", \"gridColumnEnd\", \"gridRowStart\", \"gridRowEnd\", \"gridArea\", \"justifySelf\", \"alignSelf\", \"placeSelf\", \"order\"],\n\t_stateProps = _propNamesToCopy.concat([_width, _height, \"boxSizing\", \"max\" + _Width, \"max\" + _Height, \"position\", _margin, _padding, _padding + _Top, _padding + _Right, _padding + _Bottom, _padding + _Left]),\n\t_swapPinOut = (pin, spacer, state) => {\n\t\t_setState(state);\n\t\tlet cache = pin._gsap;\n\t\tif (cache.spacerIsNative) {\n\t\t\t_setState(cache.spacerState);\n\t\t} else if (pin._gsap.swappedIn) {\n\t\t\tlet parent = spacer.parentNode;\n\t\t\tif (parent) {\n\t\t\t\tparent.insertBefore(pin, spacer);\n\t\t\t\tparent.removeChild(spacer);\n\t\t\t}\n\t\t}\n\t\tpin._gsap.swappedIn = false;\n\t},\n\t_swapPinIn = (pin, spacer, cs, spacerState) => {\n\t\tif (!pin._gsap.swappedIn) {\n\t\t\tlet i = _propNamesToCopy.length,\n\t\t\t\tspacerStyle = spacer.style,\n\t\t\t\tpinStyle = pin.style,\n\t\t\t\tp;\n\t\t\twhile (i--) {\n\t\t\t\tp = _propNamesToCopy[i];\n\t\t\t\tspacerStyle[p] = cs[p];\n\t\t\t}\n\t\t\tspacerStyle.position = cs.position === \"absolute\" ? \"absolute\" : \"relative\";\n\t\t\t(cs.display === \"inline\") && (spacerStyle.display = \"inline-block\");\n\t\t\tpinStyle[_bottom] = pinStyle[_right] = \"auto\";\n\t\t\tspacerStyle.flexBasis = cs.flexBasis || \"auto\";\n\t\t\tspacerStyle.overflow = \"visible\";\n\t\t\tspacerStyle.boxSizing = \"border-box\";\n\t\t\tspacerStyle[_width] = _getSize(pin, _horizontal) + _px;\n\t\t\tspacerStyle[_height] = _getSize(pin, _vertical) + _px;\n\t\t\tspacerStyle[_padding] = pinStyle[_margin] = pinStyle[_top] = pinStyle[_left] = \"0\";\n\t\t\t_setState(spacerState);\n\t\t\tpinStyle[_width] = pinStyle[\"max\" + _Width] = cs[_width];\n\t\t\tpinStyle[_height] = pinStyle[\"max\" + _Height] = cs[_height];\n\t\t\tpinStyle[_padding] = cs[_padding];\n\t\t\tif (pin.parentNode !== spacer) {\n\t\t\t\tpin.parentNode.insertBefore(spacer, pin);\n\t\t\t\tspacer.appendChild(pin);\n\t\t\t}\n\t\t\tpin._gsap.swappedIn = true;\n\t\t}\n\t},\n\t_capsExp = /([A-Z])/g,\n\t_setState = state => {\n\t\tif (state) {\n\t\t\tlet style = state.t.style,\n\t\t\t\tl = state.length,\n\t\t\t\ti = 0,\n\t\t\t\tp, value;\n\t\t\t(state.t._gsap || gsap.core.getCache(state.t)).uncache = 1; // otherwise transforms may be off\n\t\t\tfor (; i < l; i +=2) {\n\t\t\t\tvalue = state[i+1];\n\t\t\t\tp = state[i];\n\t\t\t\tif (value) {\n\t\t\t\t\tstyle[p] = value;\n\t\t\t\t} else if (style[p]) {\n\t\t\t\t\tstyle.removeProperty(p.replace(_capsExp, \"-$1\").toLowerCase());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t_getState = element => { // returns an Array with alternating values like [property, value, property, value] and a \"t\" property pointing to the target (element). Makes it fast and cheap.\n\t\tlet l = _stateProps.length,\n\t\t\tstyle = element.style,\n\t\t\tstate = [],\n\t\t\ti = 0;\n\t\tfor (; i < l; i++) {\n\t\t\tstate.push(_stateProps[i], style[_stateProps[i]]);\n\t\t}\n\t\tstate.t = element;\n\t\treturn state;\n\t},\n\t_copyState = (state, override, omitOffsets) => {\n\t\tlet result = [],\n\t\t\tl = state.length,\n\t\t\ti = omitOffsets ? 8 : 0, // skip top, left, right, bottom if omitOffsets is true\n\t\t\tp;\n\t\tfor (; i < l; i += 2) {\n\t\t\tp = state[i];\n\t\t\tresult.push(p, (p in override) ? override[p] : state[i+1]);\n\t\t}\n\t\tresult.t = state.t;\n\t\treturn result;\n\t},\n\t_winOffsets = {left:0, top:0},\n\t// // potential future feature (?) Allow users to calculate where a trigger hits (scroll position) like getScrollPosition(\"#id\", \"top bottom\")\n\t// _getScrollPosition = (trigger, position, {scroller, containerAnimation, horizontal}) => {\n\t// \tscroller = _getTarget(scroller || _win);\n\t// \tlet direction = horizontal ? _horizontal : _vertical,\n\t// \t\tisViewport = _isViewport(scroller);\n\t// \t_getSizeFunc(scroller, isViewport, direction);\n\t// \treturn _parsePosition(position, _getTarget(trigger), _getSizeFunc(scroller, isViewport, direction)(), direction, _getScrollFunc(scroller, direction)(), 0, 0, 0, _getOffsetsFunc(scroller, isViewport)(), isViewport ? 0 : parseFloat(_getComputedStyle(scroller)[\"border\" + direction.p2 + _Width]) || 0, 0, containerAnimation ? containerAnimation.duration() : _maxScroll(scroller), containerAnimation);\n\t// },\n\t_parsePosition = (value, trigger, scrollerSize, direction, scroll, marker, markerScroller, self, scrollerBounds, borderWidth, useFixedPosition, scrollerMax, containerAnimation, clampZeroProp) => {\n\t\t_isFunction(value) && (value = value(self));\n\t\tif (_isString(value) && value.substr(0,3) === \"max\") {\n\t\t\tvalue = scrollerMax + (value.charAt(4) === \"=\" ? _offsetToPx(\"0\" + value.substr(3), scrollerSize) : 0);\n\t\t}\n\t\tlet time = containerAnimation ? containerAnimation.time() : 0,\n\t\t\tp1, p2, element;\n\t\tcontainerAnimation && containerAnimation.seek(0);\n\t\tisNaN(value) || (value = +value); // convert a string number like \"45\" to an actual number\n\t\tif (!_isNumber(value)) {\n\t\t\t_isFunction(trigger) && (trigger = trigger(self));\n\t\t\tlet offsets = (value || \"0\").split(\" \"),\n\t\t\t\tbounds, localOffset, globalOffset, display;\n\t\t\telement = _getTarget(trigger, self) || _body;\n\t\t\tbounds = _getBounds(element) || {};\n\t\t\tif ((!bounds || (!bounds.left && !bounds.top)) && _getComputedStyle(element).display === \"none\") { // if display is \"none\", it won't report getBoundingClientRect() properly\n\t\t\t\tdisplay = element.style.display;\n\t\t\t\telement.style.display = \"block\";\n\t\t\t\tbounds = _getBounds(element);\n\t\t\t\tdisplay ? (element.style.display = display) : element.style.removeProperty(\"display\");\n\t\t\t}\n\t\t\tlocalOffset = _offsetToPx(offsets[0], bounds[direction.d]);\n\t\t\tglobalOffset = _offsetToPx(offsets[1] || \"0\", scrollerSize);\n\t\t\tvalue = bounds[direction.p] - scrollerBounds[direction.p] - borderWidth + localOffset + scroll - globalOffset;\n\t\t\tmarkerScroller && _positionMarker(markerScroller, globalOffset, direction, (scrollerSize - globalOffset < 20 || (markerScroller._isStart && globalOffset > 20)));\n\t\t\tscrollerSize -= scrollerSize - globalOffset; // adjust for the marker\n\t\t} else {\n\t\t\tcontainerAnimation && (value = gsap.utils.mapRange(containerAnimation.scrollTrigger.start, containerAnimation.scrollTrigger.end, 0, scrollerMax, value));\n\t\t\tmarkerScroller && _positionMarker(markerScroller, scrollerSize, direction, true);\n\t\t}\n\t\tif (clampZeroProp) {\n\t\t\tself[clampZeroProp] = value || -0.001;\n\t\t\tvalue < 0 && (value = 0);\n\t\t}\n\t\tif (marker) {\n\t\t\tlet position = value + scrollerSize,\n\t\t\t\tisStart = marker._isStart;\n\t\t\tp1 = \"scroll\" + direction.d2;\n\t\t\t_positionMarker(marker, position, direction, (isStart && position > 20) || (!isStart && (useFixedPosition ? Math.max(_body[p1], _docEl[p1]) : marker.parentNode[p1]) <= position + 1));\n\t\t\tif (useFixedPosition) {\n\t\t\t\tscrollerBounds = _getBounds(markerScroller);\n\t\t\t\tuseFixedPosition && (marker.style[direction.op.p] = (scrollerBounds[direction.op.p] - direction.op.m - marker._offset) + _px);\n\t\t\t}\n\t\t}\n\t\tif (containerAnimation && element) {\n\t\t\tp1 = _getBounds(element);\n\t\t\tcontainerAnimation.seek(scrollerMax);\n\t\t\tp2 = _getBounds(element);\n\t\t\tcontainerAnimation._caScrollDist = p1[direction.p] - p2[direction.p];\n\t\t\tvalue = value / (containerAnimation._caScrollDist) * scrollerMax;\n\t\t}\n\t\tcontainerAnimation && containerAnimation.seek(time);\n\t\treturn containerAnimation ? value : Math.round(value);\n\t},\n\t_prefixExp = /(webkit|moz|length|cssText|inset)/i,\n\t_reparent = (element, parent, top, left) => {\n\t\tif (element.parentNode !== parent) {\n\t\t\tlet style = element.style,\n\t\t\t\tp, cs;\n\t\t\tif (parent === _body) {\n\t\t\t\telement._stOrig = style.cssText; // record original inline styles so we can revert them later\n\t\t\t\tcs = _getComputedStyle(element);\n\t\t\t\tfor (p in cs) { // must copy all relevant styles to ensure that nothing changes visually when we reparent to the . Skip the vendor prefixed ones.\n\t\t\t\t\tif (!+p && !_prefixExp.test(p) && cs[p] && typeof style[p] === \"string\" && p !== \"0\") {\n\t\t\t\t\t\tstyle[p] = cs[p];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstyle.top = top;\n\t\t\t\tstyle.left = left;\n\t\t\t} else {\n\t\t\t\tstyle.cssText = element._stOrig;\n\t\t\t}\n\t\t\tgsap.core.getCache(element).uncache = 1;\n\t\t\tparent.appendChild(element);\n\t\t}\n\t},\n\t_interruptionTracker = (getValueFunc, initialValue, onInterrupt) => {\n\t\tlet last1 = initialValue,\n\t\t\tlast2 = last1;\n\t\treturn value => {\n\t\t\tlet current = Math.round(getValueFunc()); // round because in some [very uncommon] Windows environments, scroll can get reported with decimals even though it was set without.\n\t\t\tif (current !== last1 && current !== last2 && Math.abs(current - last1) > 3 && Math.abs(current - last2) > 3) { // if the user scrolls, kill the tween. iOS Safari intermittently misreports the scroll position, it may be the most recently-set one or the one before that! When Safari is zoomed (CMD-+), it often misreports as 1 pixel off too! So if we set the scroll position to 125, for example, it'll actually report it as 124.\n\t\t\t\tvalue = current;\n\t\t\t\tonInterrupt && onInterrupt();\n\t\t\t}\n\t\t\tlast2 = last1;\n\t\t\tlast1 = Math.round(value);\n\t\t\treturn last1;\n\t\t};\n\t},\n\t_shiftMarker = (marker, direction, value) => {\n\t\tlet vars = {};\n\t\tvars[direction.p] = \"+=\" + value;\n\t\tgsap.set(marker, vars);\n\t},\n\t// _mergeAnimations = animations => {\n\t// \tlet tl = gsap.timeline({smoothChildTiming: true}).startTime(Math.min(...animations.map(a => a.globalTime(0))));\n\t// \tanimations.forEach(a => {let time = a.totalTime(); tl.add(a); a.totalTime(time); });\n\t// \ttl.smoothChildTiming = false;\n\t// \treturn tl;\n\t// },\n\n\t// returns a function that can be used to tween the scroll position in the direction provided, and when doing so it'll add a .tween property to the FUNCTION itself, and remove it when the tween completes or gets killed. This gives us a way to have multiple ScrollTriggers use a central function for any given scroller and see if there's a scroll tween running (which would affect if/how things get updated)\n\t_getTweenCreator = (scroller, direction) => {\n\t\tlet getScroll = _getScrollFunc(scroller, direction),\n\t\t\tprop = \"_scroll\" + direction.p2, // add a tweenable property to the scroller that's a getter/setter function, like _scrollTop or _scrollLeft. This way, if someone does gsap.killTweensOf(scroller) it'll kill the scroll tween.\n\t\t\tgetTween = (scrollTo, vars, initialValue, change1, change2) => {\n\t\t\t\tlet tween = getTween.tween,\n\t\t\t\t\tonComplete = vars.onComplete,\n\t\t\t\t\tmodifiers = {};\n\t\t\t\tinitialValue = initialValue || getScroll();\n\t\t\t\tlet checkForInterruption = _interruptionTracker(getScroll, initialValue, () => {\n\t\t\t\t\ttween.kill();\n\t\t\t\t\tgetTween.tween = 0;\n\t\t\t\t});\n\t\t\t\tchange2 = (change1 && change2) || 0; // if change1 is 0, we set that to the difference and ignore change2. Otherwise, there would be a compound effect.\n\t\t\t\tchange1 = change1 || (scrollTo - initialValue);\n\t\t\t\ttween && tween.kill();\n\t\t\t\tvars[prop] = scrollTo;\n\t\t\t\tvars.inherit = false;\n\t\t\t\tvars.modifiers = modifiers;\n\t\t\t\tmodifiers[prop] = () => checkForInterruption(initialValue + change1 * tween.ratio + change2 * tween.ratio * tween.ratio);\n\t\t\t\tvars.onUpdate = () => {\n\t\t\t\t\t_scrollers.cache++;\n\t\t\t\t\tgetTween.tween && _updateAll(); // if it was interrupted/killed, like in a context.revert(), don't force an updateAll()\n\t\t\t\t};\n\t\t\t\tvars.onComplete = () => {\n\t\t\t\t\tgetTween.tween = 0;\n\t\t\t\t\tonComplete && onComplete.call(tween);\n\t\t\t\t};\n\t\t\t\ttween = getTween.tween = gsap.to(scroller, vars);\n\t\t\t\treturn tween;\n\t\t\t};\n\t\tscroller[prop] = getScroll;\n\t\tgetScroll.wheelHandler = () => getTween.tween && getTween.tween.kill() && (getTween.tween = 0);\n\t\t_addListener(scroller, \"wheel\", getScroll.wheelHandler); // Windows machines handle mousewheel scrolling in chunks (like \"3 lines per scroll\") meaning the typical strategy for cancelling the scroll isn't as sensitive. It's much more likely to match one of the previous 2 scroll event positions. So we kill any snapping as soon as there's a wheel event.\n\t\tScrollTrigger.isTouch && _addListener(scroller, \"touchmove\", getScroll.wheelHandler);\n\t\treturn getTween;\n\t};\n\n\n\n\nexport class ScrollTrigger {\n\n\tconstructor(vars, animation) {\n\t\t_coreInitted || ScrollTrigger.register(gsap) || console.warn(\"Please gsap.registerPlugin(ScrollTrigger)\");\n\t\t_context(this);\n\t\tthis.init(vars, animation);\n\t}\n\n\tinit(vars, animation) {\n\t\tthis.progress = this.start = 0;\n\t\tthis.vars && this.kill(true, true); // in case it's being initted again\n\t\tif (!_enabled) {\n\t\t\tthis.update = this.refresh = this.kill = _passThrough;\n\t\t\treturn;\n\t\t}\n\t\tvars = _setDefaults((_isString(vars) || _isNumber(vars) || vars.nodeType) ? {trigger: vars} : vars, _defaults);\n\t\tlet {onUpdate, toggleClass, id, onToggle, onRefresh, scrub, trigger, pin, pinSpacing, invalidateOnRefresh, anticipatePin, onScrubComplete, onSnapComplete, once, snap, pinReparent, pinSpacer, containerAnimation, fastScrollEnd, preventOverlaps} = vars,\n\t\t\tdirection = vars.horizontal || (vars.containerAnimation && vars.horizontal !== false) ? _horizontal : _vertical,\n\t\t\tisToggle = !scrub && scrub !== 0,\n\t\t\tscroller = _getTarget(vars.scroller || _win),\n\t\t\tscrollerCache = gsap.core.getCache(scroller),\n\t\t\tisViewport = _isViewport(scroller),\n\t\t\tuseFixedPosition = (\"pinType\" in vars ? vars.pinType : _getProxyProp(scroller, \"pinType\") || (isViewport && \"fixed\")) === \"fixed\",\n\t\t\tcallbacks = [vars.onEnter, vars.onLeave, vars.onEnterBack, vars.onLeaveBack],\n\t\t\ttoggleActions = isToggle && vars.toggleActions.split(\" \"),\n\t\t\tmarkers = \"markers\" in vars ? vars.markers : _defaults.markers,\n\t\t\tborderWidth = isViewport ? 0 : parseFloat(_getComputedStyle(scroller)[\"border\" + direction.p2 + _Width]) || 0,\n\t\t\tself = this,\n\t\t\tonRefreshInit = vars.onRefreshInit && (() => vars.onRefreshInit(self)),\n\t\t\tgetScrollerSize = _getSizeFunc(scroller, isViewport, direction),\n\t\t\tgetScrollerOffsets = _getOffsetsFunc(scroller, isViewport),\n\t\t\tlastSnap = 0,\n\t\t\tlastRefresh = 0,\n\t\t\tprevProgress = 0,\n\t\t\tscrollFunc = _getScrollFunc(scroller, direction),\n\t\t\ttweenTo, pinCache, snapFunc, scroll1, scroll2, start, end, markerStart, markerEnd, markerStartTrigger, markerEndTrigger, markerVars, executingOnRefresh,\n\t\t\tchange, pinOriginalState, pinActiveState, pinState, spacer, offset, pinGetter, pinSetter, pinStart, pinChange, spacingStart, spacerState, markerStartSetter, pinMoves,\n\t\t\tmarkerEndSetter, cs, snap1, snap2, scrubTween, scrubSmooth, snapDurClamp, snapDelayedCall, prevScroll, prevAnimProgress, caMarkerSetter, customRevertReturn;\n\n\t\t// for the sake of efficiency, _startClamp/_endClamp serve like a truthy value indicating that clamping was enabled on the start/end, and ALSO store the actual pre-clamped numeric value. We tap into that in ScrollSmoother for speed effects. So for example, if start=\"clamp(top bottom)\" results in a start of -100 naturally, it would get clamped to 0 but -100 would be stored in _startClamp.\n\t\tself._startClamp = self._endClamp = false;\n\t\tself._dir = direction;\n\t\tanticipatePin *= 45;\n\t\tself.scroller = scroller;\n\t\tself.scroll = containerAnimation ? containerAnimation.time.bind(containerAnimation) : scrollFunc;\n\t\tscroll1 = scrollFunc();\n\t\tself.vars = vars;\n\t\tanimation = animation || vars.animation;\n\t\tif (\"refreshPriority\" in vars) {\n\t\t\t_sort = 1;\n\t\t\tvars.refreshPriority === -9999 && (_primary = self); // used by ScrollSmoother\n\t\t}\n\t\tscrollerCache.tweenScroll = scrollerCache.tweenScroll || {\n\t\t\ttop: _getTweenCreator(scroller, _vertical),\n\t\t\tleft: _getTweenCreator(scroller, _horizontal)\n\t\t};\n\t\tself.tweenTo = tweenTo = scrollerCache.tweenScroll[direction.p];\n\t\tself.scrubDuration = value => {\n\t\t\tscrubSmooth = _isNumber(value) && value;\n\t\t\tif (!scrubSmooth) {\n\t\t\t\tscrubTween && scrubTween.progress(1).kill();\n\t\t\t\tscrubTween = 0;\n\t\t\t} else {\n\t\t\t\tscrubTween ? scrubTween.duration(value) : (scrubTween = gsap.to(animation, {ease: \"expo\", totalProgress: \"+=0\", inherit: false, duration: scrubSmooth, paused: true, onComplete: () => onScrubComplete && onScrubComplete(self)}));\n\t\t\t}\n\t\t};\n\t\tif (animation) {\n\t\t\tanimation.vars.lazy = false;\n\t\t\t(animation._initted && !self.isReverted) || (animation.vars.immediateRender !== false && vars.immediateRender !== false && animation.duration() && animation.render(0, true, true)); // special case: if this ScrollTrigger gets re-initted, a from() tween with a stagger could get initted initially and then reverted on the re-init which means it'll need to get rendered again here to properly display things. Otherwise, See https://gsap.com/forums/topic/36777-scrollsmoother-splittext-nextjs/ and https://codepen.io/GreenSock/pen/eYPyPpd?editors=0010\n\t\t\tself.animation = animation.pause();\n\t\t\tanimation.scrollTrigger = self;\n\t\t\tself.scrubDuration(scrub);\n\t\t\tsnap1 = 0;\n\t\t\tid || (id = animation.vars.id);\n\t\t}\n\n\t\tif (snap) {\n\t\t\t// TODO: potential idea: use legitimate CSS scroll snapping by pushing invisible elements into the DOM that serve as snap positions, and toggle the document.scrollingElement.style.scrollSnapType onToggle. See https://codepen.io/GreenSock/pen/JjLrgWM for a quick proof of concept.\n\t\t\tif (!_isObject(snap) || snap.push) {\n\t\t\t\tsnap = {snapTo: snap};\n\t\t\t}\n\t\t\t(\"scrollBehavior\" in _body.style) && gsap.set(isViewport ? [_body, _docEl] : scroller, {scrollBehavior: \"auto\"}); // smooth scrolling doesn't work with snap.\n\t\t\t_scrollers.forEach(o => _isFunction(o) && o.target === (isViewport ? _doc.scrollingElement || _docEl : scroller) && (o.smooth = false)); // note: set smooth to false on both the vertical and horizontal scroll getters/setters\n\t\t\tsnapFunc = _isFunction(snap.snapTo) ? snap.snapTo : snap.snapTo === \"labels\" ? _getClosestLabel(animation) : snap.snapTo === \"labelsDirectional\" ? _getLabelAtDirection(animation) : snap.directional !== false ? (value, st) => _snapDirectional(snap.snapTo)(value, _getTime() - lastRefresh < 500 ? 0 : st.direction) : gsap.utils.snap(snap.snapTo);\n\t\t\tsnapDurClamp = snap.duration || {min: 0.1, max: 2};\n\t\t\tsnapDurClamp = _isObject(snapDurClamp) ? _clamp(snapDurClamp.min, snapDurClamp.max) : _clamp(snapDurClamp, snapDurClamp);\n\t\t\tsnapDelayedCall = gsap.delayedCall(snap.delay || (scrubSmooth / 2) || 0.1, () => {\n\t\t\t\tlet scroll = scrollFunc(),\n\t\t\t\t\trefreshedRecently = _getTime() - lastRefresh < 500,\n\t\t\t\t\ttween = tweenTo.tween;\n\t\t\t\tif ((refreshedRecently || Math.abs(self.getVelocity()) < 10) && !tween && !_pointerIsDown && lastSnap !== scroll) {\n\t\t\t\t\tlet progress = (scroll - start) / change, // don't use self.progress because this might run between the refresh() and when the scroll position updates and self.progress is set properly in the update() method.\n\t\t\t\t\t\ttotalProgress = animation && !isToggle ? animation.totalProgress() : progress,\n\t\t\t\t\t\tvelocity = refreshedRecently ? 0 : ((totalProgress - snap2) / (_getTime() - _time2) * 1000) || 0,\n\t\t\t\t\t\tchange1 = gsap.utils.clamp(-progress, 1 - progress, _abs(velocity / 2) * velocity / 0.185),\n\t\t\t\t\t\tnaturalEnd = progress + (snap.inertia === false ? 0 : change1),\n\t\t\t\t\t\tendValue, endScroll,\n\t\t\t\t\t\t{ onStart, onInterrupt, onComplete } = snap;\n\t\t\t\t\tendValue = snapFunc(naturalEnd, self);\n\t\t\t\t\t_isNumber(endValue) || (endValue = naturalEnd); // in case the function didn't return a number, fall back to using the naturalEnd\n\t\t\t\t\tendScroll = Math.max(0, Math.round(start + endValue * change));\n\t\t\t\t\tif (scroll <= end && scroll >= start && endScroll !== scroll) {\n\t\t\t\t\t\tif (tween && !tween._initted && tween.data <= _abs(endScroll - scroll)) { // there's an overlapping snap! So we must figure out which one is closer and let that tween live.\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (snap.inertia === false) {\n\t\t\t\t\t\t\tchange1 = endValue - progress;\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttweenTo(endScroll, {\n\t\t\t\t\t\t\tduration: snapDurClamp(_abs( (Math.max(_abs(naturalEnd - totalProgress), _abs(endValue - totalProgress)) * 0.185 / velocity / 0.05) || 0)),\n\t\t\t\t\t\t\tease: snap.ease || \"power3\",\n\t\t\t\t\t\t\tdata: _abs(endScroll - scroll), // record the distance so that if another snap tween occurs (conflict) we can prioritize the closest snap.\n\t\t\t\t\t\t\tonInterrupt: () => snapDelayedCall.restart(true) && onInterrupt && onInterrupt(self),\n\t\t\t\t\t\t\tonComplete() {\n\t\t\t\t\t\t\t\tself.update();\n\t\t\t\t\t\t\t\tlastSnap = scrollFunc();\n\t\t\t\t\t\t\t\tif (animation && !isToggle) { // the resolution of the scrollbar is limited, so we should correct the scrubbed animation's playhead at the end to match EXACTLY where it was supposed to snap\n\t\t\t\t\t\t\t\t\tscrubTween ? scrubTween.resetTo(\"totalProgress\", endValue, animation._tTime / animation._tDur) : animation.progress(endValue);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tsnap1 = snap2 = animation && !isToggle ? animation.totalProgress() : self.progress;\n\t\t\t\t\t\t\t\tonSnapComplete && onSnapComplete(self);\n\t\t\t\t\t\t\t\tonComplete && onComplete(self);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}, scroll, change1 * change, endScroll - scroll - change1 * change);\n\t\t\t\t\t\tonStart && onStart(self, tweenTo.tween);\n\t\t\t\t\t}\n\t\t\t\t} else if (self.isActive && lastSnap !== scroll) {\n\t\t\t\t\tsnapDelayedCall.restart(true);\n\t\t\t\t}\n\t\t\t}).pause();\n\t\t}\n\t\tid && (_ids[id] = self);\n\t\ttrigger = self.trigger = _getTarget(trigger || (pin !== true && pin));\n\n\t\t// if a trigger has some kind of scroll-related effect applied that could contaminate the \"y\" or \"x\" position (like a ScrollSmoother effect), we needed a way to temporarily revert it, so we use the stRevert property of the gsCache. It can return another function that we'll call at the end so it can return to its normal state.\n\t\tcustomRevertReturn = trigger && trigger._gsap && trigger._gsap.stRevert;\n\t\tcustomRevertReturn && (customRevertReturn = customRevertReturn(self));\n\n\t\tpin = pin === true ? trigger : _getTarget(pin);\n\t\t_isString(toggleClass) && (toggleClass = {targets: trigger, className: toggleClass});\n\t\tif (pin) {\n\t\t\t(pinSpacing === false || pinSpacing === _margin) || (pinSpacing = !pinSpacing && pin.parentNode && pin.parentNode.style && _getComputedStyle(pin.parentNode).display === \"flex\" ? false : _padding); // if the parent is display: flex, don't apply pinSpacing by default. We should check that pin.parentNode is an element (not shadow dom window)\n\t\t\tself.pin = pin;\n\t\t\tpinCache = gsap.core.getCache(pin);\n\t\t\tif (!pinCache.spacer) { // record the spacer and pinOriginalState on the cache in case someone tries pinning the same element with MULTIPLE ScrollTriggers - we don't want to have multiple spacers or record the \"original\" pin state after it has already been affected by another ScrollTrigger.\n\t\t\t\tif (pinSpacer) {\n\t\t\t\t\tpinSpacer = _getTarget(pinSpacer);\n\t\t\t\t\tpinSpacer && !pinSpacer.nodeType && (pinSpacer = pinSpacer.current || pinSpacer.nativeElement); // for React & Angular\n\t\t\t\t\tpinCache.spacerIsNative = !!pinSpacer;\n\t\t\t\t\tpinSpacer && (pinCache.spacerState = _getState(pinSpacer));\n\t\t\t\t}\n\t\t\t\tpinCache.spacer = spacer = pinSpacer || _doc.createElement(\"div\");\n\t\t\t\tspacer.classList.add(\"pin-spacer\");\n\t\t\t\tid && spacer.classList.add(\"pin-spacer-\" + id);\n\t\t\t\tpinCache.pinState = pinOriginalState = _getState(pin);\n\t\t\t} else {\n\t\t\t\tpinOriginalState = pinCache.pinState;\n\t\t\t}\n\t\t\tvars.force3D !== false && gsap.set(pin, {force3D: true});\n\t\t\tself.spacer = spacer = pinCache.spacer;\n\t\t\tcs = _getComputedStyle(pin);\n\t\t\tspacingStart = cs[pinSpacing + direction.os2];\n\t\t\tpinGetter = gsap.getProperty(pin);\n\t\t\tpinSetter = gsap.quickSetter(pin, direction.a, _px);\n\t\t\t// pin.firstChild && !_maxScroll(pin, direction) && (pin.style.overflow = \"hidden\"); // protects from collapsing margins, but can have unintended consequences as demonstrated here: https://codepen.io/GreenSock/pen/1e42c7a73bfa409d2cf1e184e7a4248d so it was removed in favor of just telling people to set up their CSS to avoid the collapsing margins (overflow: hidden | auto is just one option. Another is border-top: 1px solid transparent).\n\t\t\t_swapPinIn(pin, spacer, cs);\n\t\t\tpinState = _getState(pin);\n\t\t}\n\t\tif (markers) {\n\t\t\tmarkerVars = _isObject(markers) ? _setDefaults(markers, _markerDefaults) : _markerDefaults;\n\t\t\tmarkerStartTrigger = _createMarker(\"scroller-start\", id, scroller, direction, markerVars, 0);\n\t\t\tmarkerEndTrigger = _createMarker(\"scroller-end\", id, scroller, direction, markerVars, 0, markerStartTrigger);\n\t\t\toffset = markerStartTrigger[\"offset\" + direction.op.d2];\n\t\t\tlet content = _getTarget(_getProxyProp(scroller, \"content\") || scroller);\n\t\t\tmarkerStart = this.markerStart = _createMarker(\"start\", id, content, direction, markerVars, offset, 0, containerAnimation);\n\t\t\tmarkerEnd = this.markerEnd = _createMarker(\"end\", id, content, direction, markerVars, offset, 0, containerAnimation);\n\t\t\tcontainerAnimation && (caMarkerSetter = gsap.quickSetter([markerStart, markerEnd], direction.a, _px));\n\t\t\tif ((!useFixedPosition && !(_proxies.length && _getProxyProp(scroller, \"fixedMarkers\") === true))) {\n\t\t\t\t_makePositionable(isViewport ? _body : scroller);\n\t\t\t\tgsap.set([markerStartTrigger, markerEndTrigger], {force3D: true});\n\t\t\t\tmarkerStartSetter = gsap.quickSetter(markerStartTrigger, direction.a, _px);\n\t\t\t\tmarkerEndSetter = gsap.quickSetter(markerEndTrigger, direction.a, _px);\n\t\t\t}\n\t\t}\n\n\t\tif (containerAnimation) {\n\t\t\tlet oldOnUpdate = containerAnimation.vars.onUpdate,\n\t\t\t\toldParams = containerAnimation.vars.onUpdateParams;\n\t\t\tcontainerAnimation.eventCallback(\"onUpdate\", () => {\n\t\t\t\tself.update(0, 0, 1);\n\t\t\t\toldOnUpdate && oldOnUpdate.apply(containerAnimation, oldParams || []);\n\t\t\t});\n\t\t}\n\n\t\tself.previous = () => _triggers[_triggers.indexOf(self) - 1];\n\t\tself.next = () => _triggers[_triggers.indexOf(self) + 1];\n\n\t\tself.revert = (revert, temp) => {\n\t\t\tif (!temp) { return self.kill(true); } // for compatibility with gsap.context() and gsap.matchMedia() which call revert()\n\t\t\tlet r = revert !== false || !self.enabled,\n\t\t\t\tprevRefreshing = _refreshing;\n\t\t\tif (r !== self.isReverted) {\n\t\t\t\tif (r) {\n\t\t\t\t\tprevScroll = Math.max(scrollFunc(), self.scroll.rec || 0); // record the scroll so we can revert later (repositioning/pinning things can affect scroll position). In the static refresh() method, we first record all the scroll positions as a reference.\n\t\t\t\t\tprevProgress = self.progress;\n\t\t\t\t\tprevAnimProgress = animation && animation.progress();\n\t\t\t\t}\n\t\t\t\tmarkerStart && [markerStart, markerEnd, markerStartTrigger, markerEndTrigger].forEach(m => m.style.display = r ? \"none\" : \"block\");\n\t\t\t\tif (r) {\n\t\t\t\t\t_refreshing = self;\n\t\t\t\t\tself.update(r); // make sure the pin is back in its original position so that all the measurements are correct. do this BEFORE swapping the pin out\n\t\t\t\t}\n\t\t\t\tif (pin && (!pinReparent || !self.isActive)) {\n\t\t\t\t\tif (r) {\n\t\t\t\t\t\t_swapPinOut(pin, spacer, pinOriginalState);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t_swapPinIn(pin, spacer, _getComputedStyle(pin), spacerState);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tr || self.update(r); // when we're restoring, the update should run AFTER swapping the pin into its pin-spacer.\n\t\t\t\t_refreshing = prevRefreshing; // restore. We set it to true during the update() so that things fire properly in there.\n\t\t\t\tself.isReverted = r;\n\t\t\t}\n\t\t}\n\n\t\tself.refresh = (soft, force, position, pinOffset) => { // position is typically only defined if it's coming from setPositions() - it's a way to skip the normal parsing. pinOffset is also only from setPositions() and is mostly related to fancy stuff we need to do in ScrollSmoother with effects\n\t\t\tif ((_refreshing || !self.enabled) && !force) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (pin && soft && _lastScrollTime) {\n\t\t\t\t_addListener(ScrollTrigger, \"scrollEnd\", _softRefresh);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t!_refreshingAll && onRefreshInit && onRefreshInit(self);\n\t\t\t_refreshing = self;\n\t\t\tif (tweenTo.tween && !position) { // we skip this if a position is passed in because typically that's from .setPositions() and it's best to allow in-progress snapping to continue.\n\t\t\t\ttweenTo.tween.kill();\n\t\t\t\ttweenTo.tween = 0;\n\t\t\t}\n\t\t\tscrubTween && scrubTween.pause();\n\t\t\tinvalidateOnRefresh && animation && animation.revert({kill: false}).invalidate();\n\t\t\tself.isReverted || self.revert(true, true);\n\t\t\tself._subPinOffset = false; // we'll set this to true in the sub-pins if we find any\n\t\t\tlet size = getScrollerSize(),\n\t\t\t\tscrollerBounds = getScrollerOffsets(),\n\t\t\t\tmax = containerAnimation ? containerAnimation.duration() : _maxScroll(scroller, direction),\n\t\t\t\tisFirstRefresh = change <= 0.01,\n\t\t\t\toffset = 0,\n\t\t\t\totherPinOffset = pinOffset || 0,\n\t\t\t\tparsedEnd = _isObject(position) ? position.end : vars.end,\n\t\t\t\tparsedEndTrigger = vars.endTrigger || trigger,\n\t\t\t\tparsedStart = _isObject(position) ? position.start : (vars.start || (vars.start === 0 || !trigger ? 0 : (pin ? \"0 0\" : \"0 100%\"))),\n\t\t\t\tpinnedContainer = self.pinnedContainer = vars.pinnedContainer && _getTarget(vars.pinnedContainer, self),\n\t\t\t\ttriggerIndex = (trigger && Math.max(0, _triggers.indexOf(self))) || 0,\n\t\t\t\ti = triggerIndex,\n\t\t\t\tcs, bounds, scroll, isVertical, override, curTrigger, curPin, oppositeScroll, initted, revertedPins, forcedOverflow, markerStartOffset, markerEndOffset;\n\t\t\tif (markers && _isObject(position)) { // if we alter the start/end positions with .setPositions(), it generally feeds in absolute NUMBERS which don't convey information about where to line up the markers, so to keep it intuitive, we record how far the trigger positions shift after applying the new numbers and then offset by that much in the opposite direction. We do the same to the associated trigger markers too of course.\n\t\t\t\tmarkerStartOffset = gsap.getProperty(markerStartTrigger, direction.p);\n\t\t\t\tmarkerEndOffset = gsap.getProperty(markerEndTrigger, direction.p);\n\t\t\t}\n\t\t\twhile (i-- > 0) { // user might try to pin the same element more than once, so we must find any prior triggers with the same pin, revert them, and determine how long they're pinning so that we can offset things appropriately. Make sure we revert from last to first so that things \"rewind\" properly.\n\t\t\t\tcurTrigger = _triggers[i];\n\t\t\t\tcurTrigger.end || curTrigger.refresh(0, 1) || (_refreshing = self); // if it's a timeline-based trigger that hasn't been fully initialized yet because it's waiting for 1 tick, just force the refresh() here, otherwise if it contains a pin that's supposed to affect other ScrollTriggers further down the page, they won't be adjusted properly.\n\t\t\t\tcurPin = curTrigger.pin;\n\t\t\t\tif (curPin && (curPin === trigger || curPin === pin || curPin === pinnedContainer) && !curTrigger.isReverted) {\n\t\t\t\t\trevertedPins || (revertedPins = []);\n\t\t\t\t\trevertedPins.unshift(curTrigger); // we'll revert from first to last to make sure things reach their end state properly\n\t\t\t\t\tcurTrigger.revert(true, true);\n\t\t\t\t}\n\t\t\t\tif (curTrigger !== _triggers[i]) { // in case it got removed.\n\t\t\t\t\ttriggerIndex--;\n\t\t\t\t\ti--;\n\t\t\t\t}\n\t\t\t}\n\t\t\t_isFunction(parsedStart) && (parsedStart = parsedStart(self));\n\t\t\tparsedStart = _parseClamp(parsedStart, \"start\", self);\n\t\t\tstart = _parsePosition(parsedStart, trigger, size, direction, scrollFunc(), markerStart, markerStartTrigger, self, scrollerBounds, borderWidth, useFixedPosition, max, containerAnimation, self._startClamp && \"_startClamp\") || (pin ? -0.001 : 0);\n\t\t\t_isFunction(parsedEnd) && (parsedEnd = parsedEnd(self));\n\t\t\tif (_isString(parsedEnd) && !parsedEnd.indexOf(\"+=\")) {\n\t\t\t\tif (~parsedEnd.indexOf(\" \")) {\n\t\t\t\t\tparsedEnd = (_isString(parsedStart) ? parsedStart.split(\" \")[0] : \"\") + parsedEnd;\n\t\t\t\t} else {\n\t\t\t\t\toffset = _offsetToPx(parsedEnd.substr(2), size);\n\t\t\t\t\tparsedEnd = _isString(parsedStart) ? parsedStart : (containerAnimation ? gsap.utils.mapRange(0, containerAnimation.duration(), containerAnimation.scrollTrigger.start, containerAnimation.scrollTrigger.end, start) : start) + offset; // _parsePosition won't factor in the offset if the start is a number, so do it here.\n\t\t\t\t\tparsedEndTrigger = trigger;\n\t\t\t\t}\n\t\t\t}\n\t\t\tparsedEnd = _parseClamp(parsedEnd, \"end\", self);\n\t\t\tend = Math.max(start, _parsePosition(parsedEnd || (parsedEndTrigger ? \"100% 0\" : max), parsedEndTrigger, size, direction, scrollFunc() + offset, markerEnd, markerEndTrigger, self, scrollerBounds, borderWidth, useFixedPosition, max, containerAnimation, self._endClamp && \"_endClamp\")) || -0.001;\n\n\t\t\toffset = 0;\n\t\t\ti = triggerIndex;\n\t\t\twhile (i--) {\n\t\t\t\tcurTrigger = _triggers[i];\n\t\t\t\tcurPin = curTrigger.pin;\n\t\t\t\tif (curPin && curTrigger.start - curTrigger._pinPush <= start && !containerAnimation && curTrigger.end > 0) {\n\t\t\t\t\tcs = curTrigger.end - (self._startClamp ? Math.max(0, curTrigger.start) : curTrigger.start);\n\t\t\t\t\tif (((curPin === trigger && curTrigger.start - curTrigger._pinPush < start) || curPin === pinnedContainer) && isNaN(parsedStart)) { // numeric start values shouldn't be offset at all - treat them as absolute\n\t\t\t\t\t\toffset += cs * (1 - curTrigger.progress);\n\t\t\t\t\t}\n\t\t\t\t\tcurPin === pin && (otherPinOffset += cs);\n\t\t\t\t}\n\t\t\t}\n\t\t\tstart += offset;\n\t\t\tend += offset;\n\t\t\tself._startClamp && (self._startClamp += offset);\n\n\t\t\tif (self._endClamp && !_refreshingAll) {\n\t\t\t\tself._endClamp = end || -0.001;\n\t\t\t\tend = Math.min(end, _maxScroll(scroller, direction));\n\t\t\t}\n\t\t\tchange = (end - start) || ((start -= 0.01) && 0.001);\n\n\t\t\tif (isFirstRefresh) { // on the very first refresh(), the prevProgress couldn't have been accurate yet because the start/end were never calculated, so we set it here. Before 3.11.5, it could lead to an inaccurate scroll position restoration with snapping.\n\t\t\t\tprevProgress = gsap.utils.clamp(0, 1, gsap.utils.normalize(start, end, prevScroll));\n\t\t\t}\n\t\t\tself._pinPush = otherPinOffset;\n\t\t\tif (markerStart && offset) { // offset the markers if necessary\n\t\t\t\tcs = {};\n\t\t\t\tcs[direction.a] = \"+=\" + offset;\n\t\t\t\tpinnedContainer && (cs[direction.p] = \"-=\" + scrollFunc());\n\t\t\t\tgsap.set([markerStart, markerEnd], cs);\n\t\t\t}\n\n\t\t\tif (pin && !(_clampingMax && self.end >= _maxScroll(scroller, direction))) {\n\t\t\t\tcs = _getComputedStyle(pin);\n\t\t\t\tisVertical = direction === _vertical;\n\t\t\t\tscroll = scrollFunc(); // recalculate because the triggers can affect the scroll\n\t\t\t\tpinStart = parseFloat(pinGetter(direction.a)) + otherPinOffset;\n\t\t\t\tif (!max && end > 1) { // makes sure the scroller has a scrollbar, otherwise if something has width: 100%, for example, it would be too big (exclude the scrollbar). See https://gsap.com/forums/topic/25182-scrolltrigger-width-of-page-increase-where-markers-are-set-to-false/\n\t\t\t\t\tforcedOverflow = (isViewport ? (_doc.scrollingElement || _docEl) : scroller).style;\n\t\t\t\t\tforcedOverflow = {style: forcedOverflow, value: forcedOverflow[\"overflow\" + direction.a.toUpperCase()]};\n\t\t\t\t\tif (isViewport && _getComputedStyle(_body)[\"overflow\" + direction.a.toUpperCase()] !== \"scroll\") { // avoid an extra scrollbar if BOTH and have overflow set to \"scroll\"\n\t\t\t\t\t\tforcedOverflow.style[\"overflow\" + direction.a.toUpperCase()] = \"scroll\";\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t_swapPinIn(pin, spacer, cs);\n\t\t\t\tpinState = _getState(pin);\n\t\t\t\t// transforms will interfere with the top/left/right/bottom placement, so remove them temporarily. getBoundingClientRect() factors in transforms.\n\t\t\t\tbounds = _getBounds(pin, true);\n\t\t\t\toppositeScroll = useFixedPosition && _getScrollFunc(scroller, isVertical ? _horizontal : _vertical)();\n\t\t\t\tif (pinSpacing) {\n\t\t\t\t\tspacerState = [pinSpacing + direction.os2, change + otherPinOffset + _px];\n\t\t\t\t\tspacerState.t = spacer;\n\t\t\t\t\ti = (pinSpacing === _padding) ? _getSize(pin, direction) + change + otherPinOffset : 0;\n\t\t\t\t\tif (i) {\n\t\t\t\t\t\tspacerState.push(direction.d, i + _px); // for box-sizing: border-box (must include padding).\n\t\t\t\t\t\tspacer.style.flexBasis !== \"auto\" && (spacer.style.flexBasis = i + _px);\n\t\t\t\t\t}\n\t\t\t\t\t_setState(spacerState);\n\t\t\t\t\tif (pinnedContainer) { // in ScrollTrigger.refresh(), we need to re-evaluate the pinContainer's size because this pinSpacing may stretch it out, but we can't just add the exact distance because depending on layout, it may not push things down or it may only do so partially.\n\t\t\t\t\t\t_triggers.forEach(t => {\n\t\t\t\t\t\t\tif (t.pin === pinnedContainer && t.vars.pinSpacing !== false) {\n\t\t\t\t\t\t\t\tt._subPinOffset = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tuseFixedPosition && scrollFunc(prevScroll);\n\t\t\t\t} else {\n\t\t\t\t\ti = _getSize(pin, direction);\n\t\t\t\t\ti && spacer.style.flexBasis !== \"auto\" && (spacer.style.flexBasis = i + _px);\n\t\t\t\t}\n\t\t\t\tif (useFixedPosition) {\n\t\t\t\t\toverride = {\n\t\t\t\t\t\ttop: (bounds.top + (isVertical ? scroll - start : oppositeScroll)) + _px,\n\t\t\t\t\t\tleft: (bounds.left + (isVertical ? oppositeScroll : scroll - start)) + _px,\n\t\t\t\t\t\tboxSizing: \"border-box\",\n\t\t\t\t\t\tposition: \"fixed\"\n\t\t\t\t\t};\n\t\t\t\t\toverride[_width] = override[\"max\" + _Width] = Math.ceil(bounds.width) + _px;\n\t\t\t\t\toverride[_height] = override[\"max\" + _Height] = Math.ceil(bounds.height) + _px;\n\t\t\t\t\toverride[_margin] = override[_margin + _Top] = override[_margin + _Right] = override[_margin + _Bottom] = override[_margin + _Left] = \"0\";\n\t\t\t\t\toverride[_padding] = cs[_padding];\n\t\t\t\t\toverride[_padding + _Top] = cs[_padding + _Top];\n\t\t\t\t\toverride[_padding + _Right] = cs[_padding + _Right];\n\t\t\t\t\toverride[_padding + _Bottom] = cs[_padding + _Bottom];\n\t\t\t\t\toverride[_padding + _Left] = cs[_padding + _Left];\n\t\t\t\t\tpinActiveState = _copyState(pinOriginalState, override, pinReparent);\n\t\t\t\t\t_refreshingAll && scrollFunc(0);\n\t\t\t\t}\n\t\t\t\tif (animation) { // the animation might be affecting the transform, so we must jump to the end, check the value, and compensate accordingly. Otherwise, when it becomes unpinned, the pinSetter() will get set to a value that doesn't include whatever the animation did.\n\t\t\t\t\tinitted = animation._initted; // if not, we must invalidate() after this step, otherwise it could lock in starting values prematurely.\n\t\t\t\t\t_suppressOverwrites(1);\n\t\t\t\t\tanimation.render(animation.duration(), true, true);\n\t\t\t\t\tpinChange = pinGetter(direction.a) - pinStart + change + otherPinOffset;\n\t\t\t\t\tpinMoves = Math.abs(change - pinChange) > 1;\n\t\t\t\t\tuseFixedPosition && pinMoves && pinActiveState.splice(pinActiveState.length - 2, 2); // transform is the last property/value set in the state Array. Since the animation is controlling that, we should omit it.\n\t\t\t\t\tanimation.render(0, true, true);\n\t\t\t\t\tinitted || animation.invalidate(true);\n\t\t\t\t\tanimation.parent || animation.totalTime(animation.totalTime()); // if, for example, a toggleAction called play() and then refresh() happens and when we render(1) above, it would cause the animation to complete and get removed from its parent, so this makes sure it gets put back in.\n\t\t\t\t\t_suppressOverwrites(0);\n\t\t\t\t} else {\n\t\t\t\t\tpinChange = change\n\t\t\t\t}\n\t\t\t\tforcedOverflow && (forcedOverflow.value ? (forcedOverflow.style[\"overflow\" + direction.a.toUpperCase()] = forcedOverflow.value) : forcedOverflow.style.removeProperty(\"overflow-\" + direction.a));\n\t\t\t} else if (trigger && scrollFunc() && !containerAnimation) { // it may be INSIDE a pinned element, so walk up the tree and look for any elements with _pinOffset to compensate because anything with pinSpacing that's already scrolled would throw off the measurements in getBoundingClientRect()\n\t\t\t\tbounds = trigger.parentNode;\n\t\t\t\twhile (bounds && bounds !== _body) {\n\t\t\t\t\tif (bounds._pinOffset) {\n\t\t\t\t\t\tstart -= bounds._pinOffset;\n\t\t\t\t\t\tend -= bounds._pinOffset;\n\t\t\t\t\t}\n\t\t\t\t\tbounds = bounds.parentNode;\n\t\t\t\t}\n\t\t\t}\n\t\t\trevertedPins && revertedPins.forEach(t => t.revert(false, true));\n\t\t\tself.start = start;\n\t\t\tself.end = end;\n\t\t\tscroll1 = scroll2 = _refreshingAll ? prevScroll : scrollFunc(); // reset velocity\n\t\t\tif (!containerAnimation && !_refreshingAll) {\n\t\t\t\tscroll1 < prevScroll && scrollFunc(prevScroll);\n\t\t\t\tself.scroll.rec = 0;\n\t\t\t}\n\t\t\tself.revert(false, true);\n\t\t\tlastRefresh = _getTime();\n\t\t\tif (snapDelayedCall) {\n\t\t\t\tlastSnap = -1; // just so snapping gets re-enabled, clear out any recorded last value\n\t\t\t\t// self.isActive && scrollFunc(start + change * prevProgress); // previously this line was here to ensure that when snapping kicks in, it's from the previous progress but in some cases that's not desirable, like an all-page ScrollTrigger when new content gets added to the page, that'd totally change the progress.\n\t\t\t\tsnapDelayedCall.restart(true);\n\t\t\t}\n\t\t\t_refreshing = 0;\n\t\t\tanimation && isToggle && (animation._initted || prevAnimProgress) && animation.progress() !== prevAnimProgress && animation.progress(prevAnimProgress || 0, true).render(animation.time(), true, true); // must force a re-render because if saveStyles() was used on the target(s), the styles could have been wiped out during the refresh().\n\t\t\tif (isFirstRefresh || prevProgress !== self.progress || containerAnimation || invalidateOnRefresh || (animation && !animation._initted)) { // ensures that the direction is set properly (when refreshing, progress is set back to 0 initially, then back again to wherever it needs to be) and that callbacks are triggered.\n\t\t\t\tanimation && !isToggle && animation.totalProgress(containerAnimation && start < -0.001 && !prevProgress ? gsap.utils.normalize(start, end, 0) : prevProgress, true); // to avoid issues where animation callbacks like onStart aren't triggered.\n\t\t\t\tself.progress = isFirstRefresh || ((scroll1 - start) / change === prevProgress) ? 0 : prevProgress;\n\t\t\t}\n\t\t\tpin && pinSpacing && (spacer._pinOffset = Math.round(self.progress * pinChange));\n\t\t\tscrubTween && scrubTween.invalidate();\n\n\t\t\tif (!isNaN(markerStartOffset)) { // numbers were passed in for the position which are absolute, so instead of just putting the markers at the very bottom of the viewport, we figure out how far they shifted down (it's safe to assume they were originally positioned in closer relation to the trigger element with values like \"top\", \"center\", a percentage or whatever, so we offset that much in the opposite direction to basically revert them to the relative position thy were at previously.\n\t\t\t\tmarkerStartOffset -= gsap.getProperty(markerStartTrigger, direction.p);\n\t\t\t\tmarkerEndOffset -= gsap.getProperty(markerEndTrigger, direction.p);\n\t\t\t\t_shiftMarker(markerStartTrigger, direction, markerStartOffset);\n\t\t\t\t_shiftMarker(markerStart, direction, markerStartOffset - (pinOffset || 0));\n\t\t\t\t_shiftMarker(markerEndTrigger, direction, markerEndOffset);\n\t\t\t\t_shiftMarker(markerEnd, direction, markerEndOffset - (pinOffset || 0));\n\t\t\t}\n\n\t\t\tisFirstRefresh && !_refreshingAll && self.update(); // edge case - when you reload a page when it's already scrolled down, some browsers fire a \"scroll\" event before DOMContentLoaded, triggering an updateAll(). If we don't update the self.progress as part of refresh(), then when it happens next, it may record prevProgress as 0 when it really shouldn't, potentially causing a callback in an animation to fire again.\n\n\t\t\tif (onRefresh && !_refreshingAll && !executingOnRefresh) { // when refreshing all, we do extra work to correct pinnedContainer sizes and ensure things don't exceed the maxScroll, so we should do all the refreshes at the end after all that work so that the start/end values are corrected.\n\t\t\t\texecutingOnRefresh = true;\n\t\t\t\tonRefresh(self);\n\t\t\t\texecutingOnRefresh = false;\n\t\t\t}\n\t\t};\n\n\t\tself.getVelocity = () => ((scrollFunc() - scroll2) / (_getTime() - _time2) * 1000) || 0;\n\n\t\tself.endAnimation = () => {\n\t\t\t_endAnimation(self.callbackAnimation);\n\t\t\tif (animation) {\n\t\t\t\tscrubTween ? scrubTween.progress(1) : (!animation.paused() ? _endAnimation(animation, animation.reversed()) : isToggle || _endAnimation(animation, self.direction < 0, 1));\n\t\t\t}\n\t\t};\n\n\t\tself.labelToScroll = label => animation && animation.labels && ((start || self.refresh() || start) + (animation.labels[label] / animation.duration()) * change) || 0;\n\n\t\tself.getTrailing = name => {\n\t\t\tlet i = _triggers.indexOf(self),\n\t\t\t\ta = self.direction > 0 ? _triggers.slice(0, i).reverse() : _triggers.slice(i+1);\n\t\t\treturn (_isString(name) ? a.filter(t => t.vars.preventOverlaps === name) : a).filter(t => self.direction > 0 ? t.end <= start : t.start >= end);\n\t\t};\n\n\n\t\tself.update = (reset, recordVelocity, forceFake) => {\n\t\t\tif (containerAnimation && !forceFake && !reset) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlet scroll = _refreshingAll === true ? prevScroll : self.scroll(),\n\t\t\t\tp = reset ? 0 : (scroll - start) / change,\n\t\t\t\tclipped = p < 0 ? 0 : p > 1 ? 1 : p || 0,\n\t\t\t\tprevProgress = self.progress,\n\t\t\t\tisActive, wasActive, toggleState, action, stateChanged, toggled, isAtMax, isTakingAction;\n\t\t\tif (recordVelocity) {\n\t\t\t\tscroll2 = scroll1;\n\t\t\t\tscroll1 = containerAnimation ? scrollFunc() : scroll;\n\t\t\t\tif (snap) {\n\t\t\t\t\tsnap2 = snap1;\n\t\t\t\t\tsnap1 = animation && !isToggle ? animation.totalProgress() : clipped;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// anticipate the pinning a few ticks ahead of time based on velocity to avoid a visual glitch due to the fact that most browsers do scrolling on a separate thread (not synced with requestAnimationFrame).\n\t\t\tif (anticipatePin && pin && !_refreshing && !_startup && _lastScrollTime) {\n\t\t\t\tif (!clipped && start < scroll + ((scroll - scroll2) / (_getTime() - _time2)) * anticipatePin) {\n\t\t\t\t\tclipped = 0.0001;\n\t\t\t\t} else if (clipped === 1 && end > scroll + ((scroll - scroll2) / (_getTime() - _time2)) * anticipatePin) {\n\t\t\t\t\tclipped = 0.9999;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (clipped !== prevProgress && self.enabled) {\n\t\t\t\tisActive = self.isActive = !!clipped && clipped < 1;\n\t\t\t\twasActive = !!prevProgress && prevProgress < 1;\n\t\t\t\ttoggled = isActive !== wasActive;\n\t\t\t\tstateChanged = toggled || !!clipped !== !!prevProgress; // could go from start all the way to end, thus it didn't toggle but it did change state in a sense (may need to fire a callback)\n\t\t\t\tself.direction = clipped > prevProgress ? 1 : -1;\n\t\t\t\tself.progress = clipped;\n\n\t\t\t\tif (stateChanged && !_refreshing) {\n\t\t\t\t\ttoggleState = clipped && !prevProgress ? 0 : clipped === 1 ? 1 : prevProgress === 1 ? 2 : 3; // 0 = enter, 1 = leave, 2 = enterBack, 3 = leaveBack (we prioritize the FIRST encounter, thus if you scroll really fast past the onEnter and onLeave in one tick, it'd prioritize onEnter.\n\t\t\t\t\tif (isToggle) {\n\t\t\t\t\t\taction = (!toggled && toggleActions[toggleState + 1] !== \"none\" && toggleActions[toggleState + 1]) || toggleActions[toggleState]; // if it didn't toggle, that means it shot right past and since we prioritize the \"enter\" action, we should switch to the \"leave\" in this case (but only if one is defined)\n\t\t\t\t\t\tisTakingAction = animation && (action === \"complete\" || action === \"reset\" || action in animation);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tpreventOverlaps && (toggled || isTakingAction) && (isTakingAction || scrub || !animation) && (_isFunction(preventOverlaps) ? preventOverlaps(self) : self.getTrailing(preventOverlaps).forEach(t => t.endAnimation()));\n\n\t\t\t\tif (!isToggle) {\n\t\t\t\t\tif (scrubTween && !_refreshing && !_startup) {\n\t\t\t\t\t\t(scrubTween._dp._time - scrubTween._start !== scrubTween._time) && scrubTween.render(scrubTween._dp._time - scrubTween._start); // if there's a scrub on both the container animation and this one (or a ScrollSmoother), the update order would cause this one not to have rendered yet, so it wouldn't make any progress before we .restart() it heading toward the new progress so it'd appear stuck thus we force a render here.\n\t\t\t\t\t\tif (scrubTween.resetTo) {\n\t\t\t\t\t\t\tscrubTween.resetTo(\"totalProgress\", clipped, animation._tTime / animation._tDur);\n\t\t\t\t\t\t} else { // legacy support (courtesy), before 3.10.0\n\t\t\t\t\t\t\tscrubTween.vars.totalProgress = clipped;\n\t\t\t\t\t\t\tscrubTween.invalidate().restart();\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (animation) {\n\t\t\t\t\t\tanimation.totalProgress(clipped, !!(_refreshing && (lastRefresh || reset)));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (pin) {\n\t\t\t\t\treset && pinSpacing && (spacer.style[pinSpacing + direction.os2] = spacingStart);\n\t\t\t\t\tif (!useFixedPosition) {\n\t\t\t\t\t\tpinSetter(_round(pinStart + pinChange * clipped));\n\t\t\t\t\t} else if (stateChanged) {\n\t\t\t\t\t\tisAtMax = !reset && clipped > prevProgress && end + 1 > scroll && scroll + 1 >= _maxScroll(scroller, direction); // if it's at the VERY end of the page, don't switch away from position: fixed because it's pointless and it could cause a brief flash when the user scrolls back up (when it gets pinned again)\n\t\t\t\t\t\tif (pinReparent) {\n\t\t\t\t\t\t\tif (!reset && (isActive || isAtMax)) {\n\t\t\t\t\t\t\t\tlet bounds = _getBounds(pin, true),\n\t\t\t\t\t\t\t\t\toffset = scroll - start;\n\t\t\t\t\t\t\t\t_reparent(pin, _body, (bounds.top + (direction === _vertical ? offset : 0)) + _px, (bounds.left + (direction === _vertical ? 0 : offset)) + _px);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t_reparent(pin, spacer);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\t_setState(isActive || isAtMax ? pinActiveState : pinState);\n\t\t\t\t\t\t(pinMoves && clipped < 1 && isActive) || pinSetter(pinStart + (clipped === 1 && !isAtMax ? pinChange : 0));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tsnap && !tweenTo.tween && !_refreshing && !_startup && snapDelayedCall.restart(true);\n\t\t\t\ttoggleClass && (toggled || (once && clipped && (clipped < 1 || !_limitCallbacks))) && _toArray(toggleClass.targets).forEach(el => el.classList[isActive || once ? \"add\" : \"remove\"](toggleClass.className)); // classes could affect positioning, so do it even if reset or refreshing is true.\n\t\t\t\tonUpdate && !isToggle && !reset && onUpdate(self);\n\t\t\t\tif (stateChanged && !_refreshing) {\n\t\t\t\t\tif (isToggle) {\n\t\t\t\t\t\tif (isTakingAction) {\n\t\t\t\t\t\t\tif (action === \"complete\") {\n\t\t\t\t\t\t\t\tanimation.pause().totalProgress(1);\n\t\t\t\t\t\t\t} else if (action === \"reset\") {\n\t\t\t\t\t\t\t\tanimation.restart(true).pause();\n\t\t\t\t\t\t\t} else if (action === \"restart\") {\n\t\t\t\t\t\t\t\tanimation.restart(true);\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tanimation[action]();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tonUpdate && onUpdate(self);\n\t\t\t\t\t}\n\t\t\t\t\tif (toggled || !_limitCallbacks) { // on startup, the page could be scrolled and we don't want to fire callbacks that didn't toggle. For example onEnter shouldn't fire if the ScrollTrigger isn't actually entered.\n\t\t\t\t\t\tonToggle && toggled && _callback(self, onToggle);\n\t\t\t\t\t\tcallbacks[toggleState] && _callback(self, callbacks[toggleState]);\n\t\t\t\t\t\tonce && (clipped === 1 ? self.kill(false, 1) : (callbacks[toggleState] = 0)); // a callback shouldn't be called again if once is true.\n\t\t\t\t\t\tif (!toggled) { // it's possible to go completely past, like from before the start to after the end (or vice-versa) in which case BOTH callbacks should be fired in that order\n\t\t\t\t\t\t\ttoggleState = clipped === 1 ? 1 : 3;\n\t\t\t\t\t\t\tcallbacks[toggleState] && _callback(self, callbacks[toggleState]);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (fastScrollEnd && !isActive && Math.abs(self.getVelocity()) > (_isNumber(fastScrollEnd) ? fastScrollEnd : 2500)) {\n\t\t\t\t\t\t_endAnimation(self.callbackAnimation);\n\t\t\t\t\t\tscrubTween ? scrubTween.progress(1) : _endAnimation(animation, action === \"reverse\" ? 1 : !clipped, 1);\n\t\t\t\t\t}\n\t\t\t\t} else if (isToggle && onUpdate && !_refreshing) {\n\t\t\t\t\tonUpdate(self);\n\t\t\t\t}\n\t\t\t}\n\t\t\t// update absolutely-positioned markers (only if the scroller isn't the viewport)\n\t\t\tif (markerEndSetter) {\n\t\t\t\tlet n = containerAnimation ? scroll / containerAnimation.duration() * (containerAnimation._caScrollDist || 0) : scroll;\n\t\t\t\tmarkerStartSetter(n + (markerStartTrigger._isFlipped ? 1 : 0));\n\t\t\t\tmarkerEndSetter(n);\n\t\t\t}\n\t\t\tcaMarkerSetter && caMarkerSetter(-scroll / containerAnimation.duration() * (containerAnimation._caScrollDist || 0));\n\t\t};\n\n\t\tself.enable = (reset, refresh) => {\n\t\t\tif (!self.enabled) {\n\t\t\t\tself.enabled = true;\n\t\t\t\t_addListener(scroller, \"resize\", _onResize);\n\t\t\t\tisViewport || _addListener(scroller, \"scroll\", _onScroll);\n\t\t\t\tonRefreshInit && _addListener(ScrollTrigger, \"refreshInit\", onRefreshInit);\n\t\t\t\tif (reset !== false) {\n\t\t\t\t\tself.progress = prevProgress = 0;\n\t\t\t\t\tscroll1 = scroll2 = lastSnap = scrollFunc();\n\t\t\t\t}\n\t\t\t\trefresh !== false && self.refresh();\n\t\t\t}\n\t\t};\n\n\t\tself.getTween = snap => snap && tweenTo ? tweenTo.tween : scrubTween;\n\n\t\tself.setPositions = (newStart, newEnd, keepClamp, pinOffset) => { // doesn't persist after refresh()! Intended to be a way to override values that were set during refresh(), like you could set it in onRefresh()\n\t\t\tif (containerAnimation) { // convert ratios into scroll positions. Remember, start/end values on ScrollTriggers that have a containerAnimation refer to the time (in seconds), NOT scroll positions.\n\t\t\t\tlet st = containerAnimation.scrollTrigger,\n\t\t\t\t\tduration = containerAnimation.duration(),\n\t\t\t\t\tchange = st.end - st.start;\n\t\t\t\tnewStart = st.start + change * newStart / duration;\n\t\t\t\tnewEnd = st.start + change * newEnd / duration;\n\t\t\t}\n\t\t\tself.refresh(false, false, {start: _keepClamp(newStart, keepClamp && !!self._startClamp), end: _keepClamp(newEnd, keepClamp && !!self._endClamp)}, pinOffset);\n\t\t\tself.update();\n\t\t};\n\n\t\tself.adjustPinSpacing = amount => {\n\t\t\tif (spacerState && amount) {\n\t\t\t\tlet i = spacerState.indexOf(direction.d) + 1;\n\t\t\t\tspacerState[i] = (parseFloat(spacerState[i]) + amount) + _px;\n\t\t\t\tspacerState[1] = (parseFloat(spacerState[1]) + amount) + _px;\n\t\t\t\t_setState(spacerState);\n\t\t\t}\n\t\t};\n\n\t\tself.disable = (reset, allowAnimation) => {\n\t\t\tif (self.enabled) {\n\t\t\t\treset !== false && self.revert(true, true);\n\t\t\t\tself.enabled = self.isActive = false;\n\t\t\t\tallowAnimation || (scrubTween && scrubTween.pause());\n\t\t\t\tprevScroll = 0;\n\t\t\t\tpinCache && (pinCache.uncache = 1);\n\t\t\t\tonRefreshInit && _removeListener(ScrollTrigger, \"refreshInit\", onRefreshInit);\n\t\t\t\tif (snapDelayedCall) {\n\t\t\t\t\tsnapDelayedCall.pause();\n\t\t\t\t\ttweenTo.tween && tweenTo.tween.kill() && (tweenTo.tween = 0);\n\t\t\t\t}\n\t\t\t\tif (!isViewport) {\n\t\t\t\t\tlet i = _triggers.length;\n\t\t\t\t\twhile (i--) {\n\t\t\t\t\t\tif (_triggers[i].scroller === scroller && _triggers[i] !== self) {\n\t\t\t\t\t\t\treturn; //don't remove the listeners if there are still other triggers referencing it.\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t_removeListener(scroller, \"resize\", _onResize);\n\t\t\t\t\tisViewport || _removeListener(scroller, \"scroll\", _onScroll);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tself.kill = (revert, allowAnimation) => {\n\t\t\tself.disable(revert, allowAnimation);\n\t\t\tscrubTween && !allowAnimation && scrubTween.kill();\n\t\t\tid && (delete _ids[id]);\n\t\t\tlet i = _triggers.indexOf(self);\n\t\t\ti >= 0 && _triggers.splice(i, 1);\n\t\t\ti === _i && _direction > 0 && _i--; // if we're in the middle of a refresh() or update(), splicing would cause skips in the index, so adjust...\n\n\t\t\t// if no other ScrollTrigger instances of the same scroller are found, wipe out any recorded scroll position. Otherwise, in a single page application, for example, it could maintain scroll position when it really shouldn't.\n\t\t\ti = 0;\n\t\t\t_triggers.forEach(t => t.scroller === self.scroller && (i = 1));\n\t\t\ti || _refreshingAll || (self.scroll.rec = 0);\n\n\t\t\tif (animation) {\n\t\t\t\tanimation.scrollTrigger = null;\n\t\t\t\trevert && animation.revert({kill: false});\n\t\t\t\tallowAnimation || animation.kill();\n\t\t\t}\n\t\t\tmarkerStart && [markerStart, markerEnd, markerStartTrigger, markerEndTrigger].forEach(m => m.parentNode && m.parentNode.removeChild(m));\n\t\t\t_primary === self && (_primary = 0);\n\t\t\tif (pin) {\n\t\t\t\tpinCache && (pinCache.uncache = 1);\n\t\t\t\ti = 0;\n\t\t\t\t_triggers.forEach(t => t.pin === pin && i++);\n\t\t\t\ti || (pinCache.spacer = 0); // if there aren't any more ScrollTriggers with the same pin, remove the spacer, otherwise it could be contaminated with old/stale values if the user re-creates a ScrollTrigger for the same element.\n\t\t\t}\n\t\t\tvars.onKill && vars.onKill(self);\n\t\t};\n\n\t\t_triggers.push(self);\n\t\tself.enable(false, false);\n\t\tcustomRevertReturn && customRevertReturn(self);\n\n\t\tif (animation && animation.add && !change) { // if the animation is a timeline, it may not have been populated yet, so it wouldn't render at the proper place on the first refresh(), thus we should schedule one for the next tick. If \"change\" is defined, we know it must be re-enabling, thus we can refresh() right away.\n\t\t\tlet updateFunc = self.update; // some browsers may fire a scroll event BEFORE a tick elapses and/or the DOMContentLoaded fires. So there's a chance update() will be called BEFORE a refresh() has happened on a Timeline-attached ScrollTrigger which means the start/end won't be calculated yet. We don't want to add conditional logic inside the update() method (like check to see if end is defined and if not, force a refresh()) because that's a function that gets hit a LOT (performance). So we swap out the real update() method for this one that'll re-attach it the first time it gets called and of course forces a refresh().\n\t\t\tself.update = () => {\n\t\t\t\tself.update = updateFunc;\n\t\t\t\t_scrollers.cache++; // otherwise a cached scroll position may get used in the refresh() in a very rare scenario, like if ScrollTriggers are created inside a DOMContentLoaded event and the queued requestAnimationFrame() fires beforehand. See https://gsap.com/community/forums/topic/41267-scrolltrigger-breaks-on-refresh-when-using-domcontentloaded/\n\t\t\t\tstart || end || self.refresh();\n\t\t\t};\n\t\t\tgsap.delayedCall(0.01, self.update);\n\t\t\tchange = 0.01;\n\t\t\tstart = end = 0;\n\t\t} else {\n\t\t\tself.refresh();\n\t\t}\n\t\tpin && _queueRefreshAll(); // pinning could affect the positions of other things, so make sure we queue a full refresh()\n\t}\n\n\n\tstatic register(core) {\n\t\tif (!_coreInitted) {\n\t\t\tgsap = core || _getGSAP();\n\t\t\t_windowExists() && window.document && ScrollTrigger.enable();\n\t\t\t_coreInitted = _enabled;\n\t\t}\n\t\treturn _coreInitted;\n\t}\n\n\tstatic defaults(config) {\n\t\tif (config) {\n\t\t\tfor (let p in config) {\n\t\t\t\t_defaults[p] = config[p];\n\t\t\t}\n\t\t}\n\t\treturn _defaults;\n\t}\n\n\tstatic disable(reset, kill) {\n\t\t_enabled = 0;\n\t\t_triggers.forEach(trigger => trigger[kill ? \"kill\" : \"disable\"](reset));\n\t\t_removeListener(_win, \"wheel\", _onScroll);\n\t\t_removeListener(_doc, \"scroll\", _onScroll);\n\t\tclearInterval(_syncInterval);\n\t\t_removeListener(_doc, \"touchcancel\", _passThrough);\n\t\t_removeListener(_body, \"touchstart\", _passThrough);\n\t\t_multiListener(_removeListener, _doc, \"pointerdown,touchstart,mousedown\", _pointerDownHandler);\n\t\t_multiListener(_removeListener, _doc, \"pointerup,touchend,mouseup\", _pointerUpHandler);\n\t\t_resizeDelay.kill();\n\t\t_iterateAutoRefresh(_removeListener);\n\t\tfor (let i = 0; i < _scrollers.length; i+=3) {\n\t\t\t_wheelListener(_removeListener, _scrollers[i], _scrollers[i+1]);\n\t\t\t_wheelListener(_removeListener, _scrollers[i], _scrollers[i+2]);\n\t\t}\n\t}\n\n\tstatic enable() {\n\t\t_win = window;\n\t\t_doc = document;\n\t\t_docEl = _doc.documentElement;\n\t\t_body = _doc.body;\n\t\tif (gsap) {\n\t\t\t_toArray = gsap.utils.toArray;\n\t\t\t_clamp = gsap.utils.clamp;\n\t\t\t_context = gsap.core.context || _passThrough;\n\t\t\t_suppressOverwrites = gsap.core.suppressOverwrites || _passThrough;\n\t\t\t_scrollRestoration = _win.history.scrollRestoration || \"auto\";\n\t\t\t_lastScroll = _win.pageYOffset || 0;\n\t\t\tgsap.core.globals(\"ScrollTrigger\", ScrollTrigger); // must register the global manually because in Internet Explorer, functions (classes) don't have a \"name\" property.\n\t\t\tif (_body) {\n\t\t\t\t_enabled = 1;\n\t\t\t\t_div100vh = document.createElement(\"div\"); // to solve mobile browser address bar show/hide resizing, we shouldn't rely on window.innerHeight. Instead, use a
with its height set to 100vh and measure that since that's what the scrolling is based on anyway and it's not affected by address bar showing/hiding.\n\t\t\t\t_div100vh.style.height = \"100vh\";\n\t\t\t\t_div100vh.style.position = \"absolute\";\n\t\t\t\t_refresh100vh();\n\t\t\t\t_rafBugFix();\n\t\t\t\tObserver.register(gsap);\n\t\t\t\t// isTouch is 0 if no touch, 1 if ONLY touch, and 2 if it can accommodate touch but also other types like mouse/pointer.\n\t\t\t\tScrollTrigger.isTouch = Observer.isTouch;\n\t\t\t\t_fixIOSBug = Observer.isTouch && /(iPad|iPhone|iPod|Mac)/g.test(navigator.userAgent); // since 2017, iOS has had a bug that causes event.clientX/Y to be inaccurate when a scroll occurs, thus we must alternate ignoring every other touchmove event to work around it. See https://bugs.webkit.org/show_bug.cgi?id=181954 and https://codepen.io/GreenSock/pen/ExbrPNa/087cef197dc35445a0951e8935c41503\n\t\t\t\t_ignoreMobileResize = Observer.isTouch === 1;\n\t\t\t\t_addListener(_win, \"wheel\", _onScroll); // mostly for 3rd party smooth scrolling libraries.\n\t\t\t\t_root = [_win, _doc, _docEl, _body];\n\t\t\t\tif (gsap.matchMedia) {\n\t\t\t\t\tScrollTrigger.matchMedia = vars => {\n\t\t\t\t\t\tlet mm = gsap.matchMedia(),\n\t\t\t\t\t\t\tp;\n\t\t\t\t\t\tfor (p in vars) {\n\t\t\t\t\t\t\tmm.add(p, vars[p]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn mm;\n\t\t\t\t\t};\n\t\t\t\t\tgsap.addEventListener(\"matchMediaInit\", () => _revertAll());\n\t\t\t\t\tgsap.addEventListener(\"matchMediaRevert\", () => _revertRecorded());\n\t\t\t\t\tgsap.addEventListener(\"matchMedia\", () => {\n\t\t\t\t\t\t_refreshAll(0, 1);\n\t\t\t\t\t\t_dispatch(\"matchMedia\");\n\t\t\t\t\t});\n\t\t\t\t\tgsap.matchMedia().add(\"(orientation: portrait)\", () => { // when orientation changes, we should take new base measurements for the ignoreMobileResize feature.\n\t\t\t\t\t\t_setBaseDimensions();\n\t\t\t\t\t\treturn _setBaseDimensions;\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tconsole.warn(\"Requires GSAP 3.11.0 or later\");\n\t\t\t\t}\n\t\t\t\t_setBaseDimensions();\n\t\t\t\t_addListener(_doc, \"scroll\", _onScroll); // some browsers (like Chrome), the window stops dispatching scroll events on the window if you scroll really fast, but it's consistent on the document!\n\t\t\t\tlet bodyHasStyle = _body.hasAttribute(\"style\"),\n\t\t\t\t\tbodyStyle = _body.style,\n\t\t\t\t\tborder = bodyStyle.borderTopStyle,\n\t\t\t\t\tAnimationProto = gsap.core.Animation.prototype,\n\t\t\t\t\tbounds, i;\n\t\t\t\tAnimationProto.revert || Object.defineProperty(AnimationProto, \"revert\", { value: function() { return this.time(-0.01, true); }}); // only for backwards compatibility (Animation.revert() was added after 3.10.4)\n\t\t\t\tbodyStyle.borderTopStyle = \"solid\"; // works around an issue where a margin of a child element could throw off the bounds of the _body, making it seem like there's a margin when there actually isn't. The border ensures that the bounds are accurate.\n\t\t\t\tbounds = _getBounds(_body);\n\t\t\t\t_vertical.m = Math.round(bounds.top + _vertical.sc()) || 0; // accommodate the offset of the caused by margins and/or padding\n\t\t\t\t_horizontal.m = Math.round(bounds.left + _horizontal.sc()) || 0;\n\t\t\t\tborder ? (bodyStyle.borderTopStyle = border) : bodyStyle.removeProperty(\"border-top-style\");\n\t\t\t\tif (!bodyHasStyle) { // SSR frameworks like Next.js complain if this attribute gets added.\n\t\t\t\t\t_body.setAttribute(\"style\", \"\"); // it's not enough to just removeAttribute() - we must first set it to empty, otherwise Next.js complains.\n\t\t\t\t\t_body.removeAttribute(\"style\");\n\t\t\t\t}\n\t\t\t\t// TODO: (?) maybe move to leveraging the velocity mechanism in Observer and skip intervals.\n\t\t\t\t_syncInterval = setInterval(_sync, 250);\n\t\t\t\tgsap.delayedCall(0.5, () => _startup = 0);\n\t\t\t\t_addListener(_doc, \"touchcancel\", _passThrough); // some older Android devices intermittently stop dispatching \"touchmove\" events if we don't listen for \"touchcancel\" on the document.\n\t\t\t\t_addListener(_body, \"touchstart\", _passThrough); //works around Safari bug: https://gsap.com/forums/topic/21450-draggable-in-iframe-on-mobile-is-buggy/\n\t\t\t\t_multiListener(_addListener, _doc, \"pointerdown,touchstart,mousedown\", _pointerDownHandler);\n\t\t\t\t_multiListener(_addListener, _doc, \"pointerup,touchend,mouseup\", _pointerUpHandler);\n\t\t\t\t_transformProp = gsap.utils.checkPrefix(\"transform\");\n\t\t\t\t_stateProps.push(_transformProp);\n\t\t\t\t_coreInitted = _getTime();\n\t\t\t\t_resizeDelay = gsap.delayedCall(0.2, _refreshAll).pause();\n\t\t\t\t_autoRefresh = [_doc, \"visibilitychange\", () => {\n\t\t\t\t\tlet w = _win.innerWidth,\n\t\t\t\t\t\th = _win.innerHeight;\n\t\t\t\t\tif (_doc.hidden) {\n\t\t\t\t\t\t_prevWidth = w;\n\t\t\t\t\t\t_prevHeight = h;\n\t\t\t\t\t} else if (_prevWidth !== w || _prevHeight !== h) {\n\t\t\t\t\t\t_onResize();\n\t\t\t\t\t}\n\t\t\t\t}, _doc, \"DOMContentLoaded\", _refreshAll, _win, \"load\", _refreshAll, _win, \"resize\", _onResize];\n\t\t\t\t_iterateAutoRefresh(_addListener);\n\t\t\t\t_triggers.forEach(trigger => trigger.enable(0, 1));\n\t\t\t\tfor (i = 0; i < _scrollers.length; i+=3) {\n\t\t\t\t\t_wheelListener(_removeListener, _scrollers[i], _scrollers[i+1]);\n\t\t\t\t\t_wheelListener(_removeListener, _scrollers[i], _scrollers[i+2]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tstatic config(vars) {\n\t\t(\"limitCallbacks\" in vars) && (_limitCallbacks = !!vars.limitCallbacks);\n\t\tlet ms = vars.syncInterval;\n\t\tms && clearInterval(_syncInterval) || ((_syncInterval = ms) && setInterval(_sync, ms));\n\t\t(\"ignoreMobileResize\" in vars) && (_ignoreMobileResize = ScrollTrigger.isTouch === 1 && vars.ignoreMobileResize);\n\t\tif (\"autoRefreshEvents\" in vars) {\n\t\t\t_iterateAutoRefresh(_removeListener) || _iterateAutoRefresh(_addListener, vars.autoRefreshEvents || \"none\");\n\t\t\t_ignoreResize = (vars.autoRefreshEvents + \"\").indexOf(\"resize\") === -1;\n\t\t}\n\t}\n\n\tstatic scrollerProxy(target, vars) {\n\t\tlet t = _getTarget(target),\n\t\t\ti = _scrollers.indexOf(t),\n\t\t\tisViewport = _isViewport(t);\n\t\tif (~i) {\n\t\t\t_scrollers.splice(i, isViewport ? 6 : 2);\n\t\t}\n\t\tif (vars) {\n\t\t\tisViewport ? _proxies.unshift(_win, vars, _body, vars, _docEl, vars) : _proxies.unshift(t, vars);\n\t\t}\n\t}\n\n\tstatic clearMatchMedia(query) {\n\t\t_triggers.forEach(t => t._ctx && t._ctx.query === query && t._ctx.kill(true, true));\n\t}\n\n\tstatic isInViewport(element, ratio, horizontal) {\n\t\tlet bounds = (_isString(element) ? _getTarget(element) : element).getBoundingClientRect(),\n\t\t\toffset = bounds[horizontal ? _width : _height] * ratio || 0;\n\t\treturn horizontal ? bounds.right - offset > 0 && bounds.left + offset < _win.innerWidth : bounds.bottom - offset > 0 && bounds.top + offset < _win.innerHeight;\n\t}\n\n\tstatic positionInViewport(element, referencePoint, horizontal) {\n\t\t_isString(element) && (element = _getTarget(element));\n\t\tlet bounds = element.getBoundingClientRect(),\n\t\t\tsize = bounds[horizontal ? _width : _height],\n\t\t\toffset = referencePoint == null ? size / 2 : ((referencePoint in _keywords) ? _keywords[referencePoint] * size : ~referencePoint.indexOf(\"%\") ? parseFloat(referencePoint) * size / 100 : parseFloat(referencePoint) || 0);\n\t\treturn horizontal ? (bounds.left + offset) / _win.innerWidth : (bounds.top + offset) / _win.innerHeight;\n\t}\n\n\tstatic killAll(allowListeners) {\n\t\t_triggers.slice(0).forEach(t => t.vars.id !== \"ScrollSmoother\" && t.kill());\n\t\tif (allowListeners !== true) {\n\t\t\tlet listeners = _listeners.killAll || [];\n\t\t\t_listeners = {};\n\t\t\tlisteners.forEach(f => f());\n\t\t}\n\t}\n\n}\n\nScrollTrigger.version = \"3.12.7\";\nScrollTrigger.saveStyles = targets => targets ? _toArray(targets).forEach(target => { // saved styles are recorded in a consecutive alternating Array, like [element, cssText, transform attribute, cache, matchMedia, ...]\n\tif (target && target.style) {\n\t\tlet i = _savedStyles.indexOf(target);\n\t\ti >= 0 && _savedStyles.splice(i, 5);\n\t\t_savedStyles.push(target, target.style.cssText, target.getBBox && target.getAttribute(\"transform\"), gsap.core.getCache(target), _context());\n\t}\n}) : _savedStyles;\nScrollTrigger.revert = (soft, media) => _revertAll(!soft, media);\nScrollTrigger.create = (vars, animation) => new ScrollTrigger(vars, animation);\nScrollTrigger.refresh = safe => safe ? _onResize(true) : (_coreInitted || ScrollTrigger.register()) && _refreshAll(true);\nScrollTrigger.update = force => ++_scrollers.cache && _updateAll(force === true ? 2 : 0);\nScrollTrigger.clearScrollMemory = _clearScrollMemory;\nScrollTrigger.maxScroll = (element, horizontal) => _maxScroll(element, horizontal ? _horizontal : _vertical);\nScrollTrigger.getScrollFunc = (element, horizontal) => _getScrollFunc(_getTarget(element), horizontal ? _horizontal : _vertical);\nScrollTrigger.getById = id => _ids[id];\nScrollTrigger.getAll = () => _triggers.filter(t => t.vars.id !== \"ScrollSmoother\"); // it's common for people to ScrollTrigger.getAll(t => t.kill()) on page routes, for example, and we don't want it to ruin smooth scrolling by killing the main ScrollSmoother one.\nScrollTrigger.isScrolling = () => !!_lastScrollTime;\nScrollTrigger.snapDirectional = _snapDirectional;\nScrollTrigger.addEventListener = (type, callback) => {\n\tlet a = _listeners[type] || (_listeners[type] = []);\n\t~a.indexOf(callback) || a.push(callback);\n};\nScrollTrigger.removeEventListener = (type, callback) => {\n\tlet a = _listeners[type],\n\t\ti = a && a.indexOf(callback);\n\ti >= 0 && a.splice(i, 1);\n};\nScrollTrigger.batch = (targets, vars) => {\n\tlet result = [],\n\t\tvarsCopy = {},\n\t\tinterval = vars.interval || 0.016,\n\t\tbatchMax = vars.batchMax || 1e9,\n\t\tproxyCallback = (type, callback) => {\n\t\t\tlet elements = [],\n\t\t\t\ttriggers = [],\n\t\t\t\tdelay = gsap.delayedCall(interval, () => {callback(elements, triggers); elements = []; triggers = [];}).pause();\n\t\t\treturn self => {\n\t\t\t\telements.length || delay.restart(true);\n\t\t\t\telements.push(self.trigger);\n\t\t\t\ttriggers.push(self);\n\t\t\t\tbatchMax <= elements.length && delay.progress(1);\n\t\t\t};\n\t\t},\n\t\tp;\n\tfor (p in vars) {\n\t\tvarsCopy[p] = (p.substr(0, 2) === \"on\" && _isFunction(vars[p]) && p !== \"onRefreshInit\") ? proxyCallback(p, vars[p]) : vars[p];\n\t}\n\tif (_isFunction(batchMax)) {\n\t\tbatchMax = batchMax();\n\t\t_addListener(ScrollTrigger, \"refresh\", () => batchMax = vars.batchMax());\n\t}\n\t_toArray(targets).forEach(target => {\n\t\tlet config = {};\n\t\tfor (p in varsCopy) {\n\t\t\tconfig[p] = varsCopy[p];\n\t\t}\n\t\tconfig.trigger = target;\n\t\tresult.push(ScrollTrigger.create(config));\n\t});\n\treturn result;\n}\n\n\n// to reduce file size. clamps the scroll and also returns a duration multiplier so that if the scroll gets chopped shorter, the duration gets curtailed as well (otherwise if you're very close to the top of the page, for example, and swipe up really fast, it'll suddenly slow down and take a long time to reach the top).\nlet _clampScrollAndGetDurationMultiplier = (scrollFunc, current, end, max) => {\n\t\tcurrent > max ? scrollFunc(max) : current < 0 && scrollFunc(0);\n\t\treturn end > max ? (max - current) / (end - current) : end < 0 ? current / (current - end) : 1;\n\t},\n\t_allowNativePanning = (target, direction) => {\n\t\tif (direction === true) {\n\t\t\ttarget.style.removeProperty(\"touch-action\");\n\t\t} else {\n\t\t\ttarget.style.touchAction = direction === true ? \"auto\" : direction ? \"pan-\" + direction + (Observer.isTouch ? \" pinch-zoom\" : \"\") : \"none\"; // note: Firefox doesn't support it pinch-zoom properly, at least in addition to a pan-x or pan-y.\n\t\t}\n\t\ttarget === _docEl && _allowNativePanning(_body, direction);\n\t},\n\t_overflow = {auto: 1, scroll: 1},\n\t_nestedScroll = ({event, target, axis}) => {\n\t\tlet node = (event.changedTouches ? event.changedTouches[0] : event).target,\n\t\t\tcache = node._gsap || gsap.core.getCache(node),\n\t\t\ttime = _getTime(), cs;\n\t\tif (!cache._isScrollT || time - cache._isScrollT > 2000) { // cache for 2 seconds to improve performance.\n\t\t\twhile (node && node !== _body && ((node.scrollHeight <= node.clientHeight && node.scrollWidth <= node.clientWidth) || !(_overflow[(cs = _getComputedStyle(node)).overflowY] || _overflow[cs.overflowX]))) node = node.parentNode;\n\t\t\tcache._isScroll = node && node !== target && !_isViewport(node) && (_overflow[(cs = _getComputedStyle(node)).overflowY] || _overflow[cs.overflowX]);\n\t\t\tcache._isScrollT = time;\n\t\t}\n\t\tif (cache._isScroll || axis === \"x\") {\n\t\t\tevent.stopPropagation();\n\t\t\tevent._gsapAllow = true;\n\t\t}\n\t},\n\t// capture events on scrollable elements INSIDE the and allow those by calling stopPropagation() when we find a scrollable ancestor\n\t_inputObserver = (target, type, inputs, nested) => Observer.create({\n\t\ttarget: target,\n\t\tcapture: true,\n\t\tdebounce: false,\n\t\tlockAxis: true,\n\t\ttype: type,\n\t\tonWheel: (nested = nested && _nestedScroll),\n\t\tonPress: nested,\n\t\tonDrag: nested,\n\t\tonScroll: nested,\n\t\tonEnable: () => inputs && _addListener(_doc, Observer.eventTypes[0], _captureInputs, false, true),\n\t\tonDisable: () => _removeListener(_doc, Observer.eventTypes[0], _captureInputs, true)\n\t}),\n\t_inputExp = /(input|label|select|textarea)/i,\n\t_inputIsFocused,\n\t_captureInputs = e => {\n\t\tlet isInput = _inputExp.test(e.target.tagName);\n\t\tif (isInput || _inputIsFocused) {\n\t\t\te._gsapAllow = true;\n\t\t\t_inputIsFocused = isInput;\n\t\t}\n\t},\n\t_getScrollNormalizer = vars => {\n\t\t_isObject(vars) || (vars = {});\n\t\tvars.preventDefault = vars.isNormalizer = vars.allowClicks = true;\n\t\tvars.type || (vars.type = \"wheel,touch\");\n\t\tvars.debounce = !!vars.debounce;\n\t\tvars.id = vars.id || \"normalizer\";\n\t\tlet {normalizeScrollX, momentum, allowNestedScroll, onRelease} = vars,\n\t\t\tself, maxY,\n\t\t\ttarget = _getTarget(vars.target) || _docEl,\n\t\t\tsmoother = gsap.core.globals().ScrollSmoother,\n\t\t\tsmootherInstance = smoother && smoother.get(),\n\t\t\tcontent = _fixIOSBug && ((vars.content && _getTarget(vars.content)) || (smootherInstance && vars.content !== false && !smootherInstance.smooth() && smootherInstance.content())),\n\t\t\tscrollFuncY = _getScrollFunc(target, _vertical),\n\t\t\tscrollFuncX = _getScrollFunc(target, _horizontal),\n\t\t\tscale = 1,\n\t\t\tinitialScale = (Observer.isTouch && _win.visualViewport ? _win.visualViewport.scale * _win.visualViewport.width : _win.outerWidth) / _win.innerWidth,\n\t\t\twheelRefresh = 0,\n\t\t\tresolveMomentumDuration = _isFunction(momentum) ? () => momentum(self) : () => momentum || 2.8,\n\t\t\tlastRefreshID, skipTouchMove,\n\t\t\tinputObserver = _inputObserver(target, vars.type, true, allowNestedScroll),\n\t\t\tresumeTouchMove = () => skipTouchMove = false,\n\t\t\tscrollClampX = _passThrough,\n\t\t\tscrollClampY = _passThrough,\n\t\t\tupdateClamps = () => {\n\t\t\t\tmaxY = _maxScroll(target, _vertical);\n\t\t\t\tscrollClampY = _clamp(_fixIOSBug ? 1 : 0, maxY);\n\t\t\t\tnormalizeScrollX && (scrollClampX = _clamp(0, _maxScroll(target, _horizontal)));\n\t\t\t\tlastRefreshID = _refreshID;\n\t\t\t},\n\t\t\tremoveContentOffset = () => {\n\t\t\t\tcontent._gsap.y = _round(parseFloat(content._gsap.y) + scrollFuncY.offset) + \"px\";\n\t\t\t\tcontent.style.transform = \"matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, \" + parseFloat(content._gsap.y) + \", 0, 1)\";\n\t\t\t\tscrollFuncY.offset = scrollFuncY.cacheID = 0;\n\t\t\t},\n\t\t\tignoreDrag = () => {\n\t\t\t\tif (skipTouchMove) {\n\t\t\t\t\trequestAnimationFrame(resumeTouchMove);\n\t\t\t\t\tlet offset = _round(self.deltaY / 2),\n\t\t\t\t\t\tscroll = scrollClampY(scrollFuncY.v - offset);\n\t\t\t\t\tif (content && scroll !== scrollFuncY.v + scrollFuncY.offset) {\n\t\t\t\t\t\tscrollFuncY.offset = scroll - scrollFuncY.v;\n\t\t\t\t\t\tlet y = _round((parseFloat(content && content._gsap.y) || 0) - scrollFuncY.offset);\n\t\t\t\t\t\tcontent.style.transform = \"matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, \" + y + \", 0, 1)\";\n\t\t\t\t\t\tcontent._gsap.y = y + \"px\";\n\t\t\t\t\t\tscrollFuncY.cacheID = _scrollers.cache;\n\t\t\t\t\t\t_updateAll();\n\t\t\t\t\t}\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\tscrollFuncY.offset && removeContentOffset();\n\t\t\t\tskipTouchMove = true;\n\t\t\t},\n\t\t\ttween, startScrollX, startScrollY, onStopDelayedCall,\n\t\t\tonResize = () => { // if the window resizes, like on an iPhone which Apple FORCES the address bar to show/hide even if we event.preventDefault(), it may be scrolling too far now that the address bar is showing, so we must dynamically adjust the momentum tween.\n\t\t\t\tupdateClamps();\n\t\t\t\tif (tween.isActive() && tween.vars.scrollY > maxY) {\n\t\t\t\t\tscrollFuncY() > maxY ? tween.progress(1) && scrollFuncY(maxY) : tween.resetTo(\"scrollY\", maxY);\n\t\t\t\t}\n\t\t\t};\n\t\tcontent && gsap.set(content, {y: \"+=0\"}); // to ensure there's a cache (element._gsap)\n\t\tvars.ignoreCheck = e => (_fixIOSBug && e.type === \"touchmove\" && ignoreDrag(e)) || (scale > 1.05 && e.type !== \"touchstart\") || self.isGesturing || (e.touches && e.touches.length > 1);\n\t\tvars.onPress = () => {\n\t\t\tskipTouchMove = false;\n\t\t\tlet prevScale = scale;\n\t\t\tscale = _round(((_win.visualViewport && _win.visualViewport.scale) || 1) / initialScale);\n\t\t\ttween.pause();\n\t\t\tprevScale !== scale && _allowNativePanning(target, scale > 1.01 ? true : normalizeScrollX ? false : \"x\");\n\t\t\tstartScrollX = scrollFuncX();\n\t\t\tstartScrollY = scrollFuncY();\n\t\t\tupdateClamps();\n\t\t\tlastRefreshID = _refreshID;\n\t\t}\n\t\tvars.onRelease = vars.onGestureStart = (self, wasDragging) => {\n\t\t\tscrollFuncY.offset && removeContentOffset();\n\t\t\tif (!wasDragging) {\n\t\t\t\tonStopDelayedCall.restart(true);\n\t\t\t} else {\n\t\t\t\t_scrollers.cache++; // make sure we're pulling the non-cached value\n\t\t\t\t// alternate algorithm: durX = Math.min(6, Math.abs(self.velocityX / 800)),\tdur = Math.max(durX, Math.min(6, Math.abs(self.velocityY / 800))); dur = dur * (0.4 + (1 - _power4In(dur / 6)) * 0.6)) * (momentumSpeed || 1)\n\t\t\t\tlet dur = resolveMomentumDuration(),\n\t\t\t\t\tcurrentScroll, endScroll;\n\t\t\t\tif (normalizeScrollX) {\n\t\t\t\t\tcurrentScroll = scrollFuncX();\n\t\t\t\t\tendScroll = currentScroll + (dur * 0.05 * -self.velocityX) / 0.227; // the constant .227 is from power4(0.05). velocity is inverted because scrolling goes in the opposite direction.\n\t\t\t\t\tdur *= _clampScrollAndGetDurationMultiplier(scrollFuncX, currentScroll, endScroll, _maxScroll(target, _horizontal));\n\t\t\t\t\ttween.vars.scrollX = scrollClampX(endScroll);\n\t\t\t\t}\n\t\t\t\tcurrentScroll = scrollFuncY();\n\t\t\t\tendScroll = currentScroll + (dur * 0.05 * -self.velocityY) / 0.227; // the constant .227 is from power4(0.05)\n\t\t\t\tdur *= _clampScrollAndGetDurationMultiplier(scrollFuncY, currentScroll, endScroll, _maxScroll(target, _vertical));\n\t\t\t\ttween.vars.scrollY = scrollClampY(endScroll);\n\t\t\t\ttween.invalidate().duration(dur).play(0.01);\n\t\t\t\tif (_fixIOSBug && tween.vars.scrollY >= maxY || currentScroll >= maxY-1) { // iOS bug: it'll show the address bar but NOT fire the window \"resize\" event until the animation is done but we must protect against overshoot so we leverage an onUpdate to do so.\n\t\t\t\t\tgsap.to({}, {onUpdate: onResize, duration: dur});\n\t\t\t\t}\n\t\t\t}\n\t\t\tonRelease && onRelease(self);\n\t\t};\n\t\tvars.onWheel = () => {\n\t\t\ttween._ts && tween.pause();\n\t\t\tif (_getTime() - wheelRefresh > 1000) { // after 1 second, refresh the clamps otherwise that'll only happen when ScrollTrigger.refresh() is called or for touch-scrolling.\n\t\t\t\tlastRefreshID = 0;\n\t\t\t\twheelRefresh = _getTime();\n\t\t\t}\n\t\t};\n\t\tvars.onChange = (self, dx, dy, xArray, yArray) => {\n\t\t\t_refreshID !== lastRefreshID && updateClamps();\n\t\t\tdx && normalizeScrollX && scrollFuncX(scrollClampX(xArray[2] === dx ? startScrollX + (self.startX - self.x) : scrollFuncX() + dx - xArray[1])); // for more precision, we track pointer/touch movement from the start, otherwise it'll drift.\n\t\t\tif (dy) {\n\t\t\t\tscrollFuncY.offset && removeContentOffset();\n\t\t\t\tlet isTouch = yArray[2] === dy,\n\t\t\t\t\ty = isTouch ? startScrollY + self.startY - self.y : scrollFuncY() + dy - yArray[1],\n\t\t\t\t\tyClamped = scrollClampY(y);\n\t\t\t\tisTouch && y !== yClamped && (startScrollY += yClamped - y);\n\t\t\t\tscrollFuncY(yClamped);\n\t\t\t}\n\t\t\t(dy || dx) && _updateAll();\n\t\t};\n\t\tvars.onEnable = () => {\n\t\t\t_allowNativePanning(target, normalizeScrollX ? false : \"x\");\n\t\t\tScrollTrigger.addEventListener(\"refresh\", onResize);\n\t\t\t_addListener(_win, \"resize\", onResize);\n\t\t\tif (scrollFuncY.smooth) {\n\t\t\t\tscrollFuncY.target.style.scrollBehavior = \"auto\";\n\t\t\t\tscrollFuncY.smooth = scrollFuncX.smooth = false;\n\t\t\t}\n\t\t\tinputObserver.enable();\n\t\t};\n\t\tvars.onDisable = () => {\n\t\t\t_allowNativePanning(target, true);\n\t\t\t_removeListener(_win, \"resize\", onResize);\n\t\t\tScrollTrigger.removeEventListener(\"refresh\", onResize);\n\t\t\tinputObserver.kill();\n\t\t};\n\t\tvars.lockAxis = vars.lockAxis !== false;\n\t\tself = new Observer(vars);\n\t\tself.iOS = _fixIOSBug; // used in the Observer getCachedScroll() function to work around an iOS bug that wreaks havoc with TouchEvent.clientY if we allow scroll to go all the way back to 0.\n\t\t_fixIOSBug && !scrollFuncY() && scrollFuncY(1); // iOS bug causes event.clientY values to freak out (wildly inaccurate) if the scroll position is exactly 0.\n\t\t_fixIOSBug && gsap.ticker.add(_passThrough); // prevent the ticker from sleeping\n\t\tonStopDelayedCall = self._dc;\n\t\ttween = gsap.to(self, {ease: \"power4\", paused: true, inherit: false, scrollX: normalizeScrollX ? \"+=0.1\" : \"+=0\", scrollY: \"+=0.1\", modifiers: {scrollY: _interruptionTracker(scrollFuncY, scrollFuncY(), () => tween.pause())\t}, onUpdate: _updateAll, onComplete: onStopDelayedCall.vars.onComplete}); // we need the modifier to sense if the scroll position is altered outside of the momentum tween (like with a scrollTo tween) so we can pause() it to prevent conflicts.\n\t\treturn self;\n\t};\n\nScrollTrigger.sort = func => {\n\tif (_isFunction(func)) {\n\t\treturn _triggers.sort(func);\n\t}\n\tlet scroll = _win.pageYOffset || 0;\n\tScrollTrigger.getAll().forEach(t => t._sortY = t.trigger ? scroll + t.trigger.getBoundingClientRect().top : t.start + _win.innerHeight);\n\treturn _triggers.sort(func || ((a, b) => (a.vars.refreshPriority || 0) * -1e6 + (a.vars.containerAnimation ? 1e6 : a._sortY) - ((b.vars.containerAnimation ? 1e6 : b._sortY) + (b.vars.refreshPriority || 0) * -1e6))); // anything with a containerAnimation should refresh last.\n}\nScrollTrigger.observe = vars => new Observer(vars);\nScrollTrigger.normalizeScroll = vars => {\n\tif (typeof(vars) === \"undefined\") {\n\t\treturn _normalizer;\n\t}\n\tif (vars === true && _normalizer) {\n\t\treturn _normalizer.enable();\n\t}\n\tif (vars === false) {\n\t\t_normalizer && _normalizer.kill();\n\t\t_normalizer = vars;\n\t\treturn;\n\t}\n\tlet normalizer = vars instanceof Observer ? vars : _getScrollNormalizer(vars);\n\t_normalizer && _normalizer.target === normalizer.target && _normalizer.kill();\n\t_isViewport(normalizer.target) && (_normalizer = normalizer);\n\treturn normalizer;\n};\n\n\nScrollTrigger.core = { // smaller file size way to leverage in ScrollSmoother and Observer\n\t_getVelocityProp,\n\t_inputObserver,\n\t_scrollers,\n\t_proxies,\n\tbridge: {\n\t\t// when normalizeScroll sets the scroll position (ss = setScroll)\n\t\tss: () => {\n\t\t\t_lastScrollTime || _dispatch(\"scrollStart\");\n\t\t\t_lastScrollTime = _getTime();\n\t\t},\n\t\t// a way to get the _refreshing value in Observer\n\t\tref: () => _refreshing\n\t}\n};\n\n_getGSAP() && gsap.registerPlugin(ScrollTrigger);\n\nexport { ScrollTrigger as default };"],"names":["_getGSAP","gsap","window","registerPlugin","_getProxyProp","element","property","_proxies","indexOf","_isViewport","el","_root","_addListener","type","func","passive","capture","addEventListener","_removeListener","removeEventListener","_onScroll","_normalizer","isPressed","_scrollers","cache","_scrollCacheFunc","f","doNotCache","cachingFunc","value","_startup","_win","history","scrollRestoration","isNormalizing","v","Math","round","iOS","cacheID","_bridge","offset","_getTarget","t","self","_ctx","selector","utils","toArray","config","nullTargetWarn","console","warn","_getScrollFunc","s","sc","_doc","scrollingElement","_docEl","i","_vertical","push","prev","arguments","length","target","smooth","getProperty","_getVelocityProp","minTimeRefresh","useDelta","update","force","_getTime","min","t1","v2","v1","t2","dropToZeroTime","max","reset","getVelocity","latestValue","tOld","vOld","_getEvent","e","preventDefault","_gsapAllow","changedTouches","_getAbsoluteMax","a","abs","_setScrollTrigger","ScrollTrigger","core","globals","_integrate","data","bridge","scrollers","proxies","name","_initCore","_coreInitted","document","body","documentElement","_body","clamp","_context","context","_pointerType","_isTouch","Observer","isTouch","matchMedia","matches","navigator","maxTouchPoints","msMaxTouchPoints","_eventTypes","eventTypes","split","setTimeout","_observers","Date","now","_scrollLeft","_scrollTop","_horizontal","p","p2","os","os2","d","d2","scrollTo","pageXOffset","op","pageYOffset","init","vars","tolerance","dragMinimum","lineHeight","debounce","onStop","onStopDelay","ignore","wheelSpeed","event","onDragStart","onDragEnd","onDrag","onPress","onRelease","onRight","onLeft","onUp","onDown","onChangeX","onChangeY","onChange","onToggleX","onToggleY","onHover","onHoverEnd","onMove","ignoreCheck","isNormalizer","onGestureStart","onGestureEnd","onWheel","onEnable","onDisable","onClick","scrollSpeed","allowClicks","lockAxis","onLockAxis","clickCapture","onClickTime","_ignoreCheck","isPointerOrTouch","limitToTouch","pointerType","dx","deltaX","dy","deltaY","changedX","changedY","prevDeltaX","prevDeltaY","moved","dragged","locked","wheeled","id","onDelta","x","y","index","_vx","_vy","requestAnimationFrame","onTouchOrPointerDelta","axis","_onDrag","clientX","clientY","isDragging","startX","startY","_onGestureStart","touches","isGesturing","_onGestureEnd","onScroll","scrollFuncX","scrollFuncY","scrollX","scrollY","onStopDelayedCall","restart","_onWheel","multiplier","deltaMode","innerHeight","_onMove","_onHover","_onHoverEnd","_onClick","parseFloat","getComputedStyle","this","isViewport","ownerDoc","ownerDocument","_onPress","button","pause","_onRelease","isTrackingDrag","isNaN","wasDragging","isDragNotClick","eventData","delayedCall","defaultPrevented","click","createEvent","syntheticEvent","initMouseEvent","screenX","screenY","dispatchEvent","_dc","onStopFunc","enable","isEnabled","disable","filter","o","kill","revert","splice","version","create","register","getAll","slice","getById","_parseClamp","_isString","substr","_keepClamp","_pointerDownHandler","_pointerIsDown","_pointerUpHandler","_passThrough","_round","_windowExists","_getViewportDimension","dimensionProperty","_100vh","_getBoundsFunc","_winOffsets","width","innerWidth","height","_getBounds","_maxScroll","_iterateAutoRefresh","events","_autoRefresh","_isFunction","_isNumber","_isObject","_endAnimation","animation","reversed","progress","_callback","enabled","result","add","totalTime","callbackAnimation","_getComputedStyle","_setDefaults","obj","defaults","_getSize","_getLabelRatioArray","timeline","labels","duration","_snapDirectional","snapIncrementOrArray","snap","Array","isArray","sort","b","direction","threshold","snapped","_multiListener","types","callback","forEach","nonPassive","_wheelListener","scrollFunc","wheelHandler","_offsetToPx","size","eqIndex","relative","charAt","_keywords","_createMarker","container","matchWidthEl","containerAnimation","startColor","endColor","fontSize","indent","fontWeight","createElement","useFixedPosition","isScroller","parent","isStart","color","css","_right","_bottom","offsetWidth","_isStart","setAttribute","style","cssText","innerText","children","insertBefore","appendChild","_offset","_positionMarker","_sync","_lastScrollTime","_rafID","_updateAll","clientWidth","_dispatch","_setBaseDimensions","_baseScreenWidth","_baseScreenHeight","_onResize","_refreshing","_ignoreResize","fullscreenElement","webkitFullscreenElement","_ignoreMobileResize","_resizeDelay","_softRefresh","_refreshAll","_revertRecorded","media","_savedStyles","query","getBBox","uncache","_revertAll","trigger","_i","_triggers","_isReverted","_clearScrollMemory","_refreshingAll","rec","_scrollRestoration","_refresh100vh","_div100vh","offsetHeight","removeChild","_hideAllMarkers","hide","_toArray","display","_swapPinIn","pin","spacer","cs","spacerState","_gsap","swappedIn","_propNamesToCopy","spacerStyle","pinStyle","position","flexBasis","overflow","boxSizing","_width","_px","_height","_padding","_margin","_setState","parentNode","_getState","l","_stateProps","state","_parsePosition","scrollerSize","scroll","marker","markerScroller","scrollerBounds","borderWidth","scrollerMax","clampZeroProp","p1","time","seek","mapRange","scrollTrigger","start","end","bounds","localOffset","globalOffset","offsets","left","top","removeProperty","m","_caScrollDist","_reparent","_stOrig","_prefixExp","test","getCache","_interruptionTracker","getValueFunc","initialValue","onInterrupt","last1","last2","current","_shiftMarker","set","_getTweenCreator","scroller","getTween","change1","change2","tween","onComplete","modifiers","getScroll","checkForInterruption","prop","inherit","ratio","onUpdate","call","to","_clamp","_time2","_syncInterval","_transformProp","_prevWidth","_prevHeight","_sort","_suppressOverwrites","_fixIOSBug","_clampingMax","_limitCallbacks","_queueRefreshID","_primary","_time1","_enabled","_abs","_Right","_Left","_Top","_Bottom","_Width","_Height","withoutTransforms","xPercent","yPercent","rotation","rotationX","rotationY","scale","skewX","skewY","getBoundingClientRect","_markerDefaults","_defaults","toggleActions","anticipatePin","center","bottom","right","flipped","side","oppositeSide","_isFlipped","_ids","_listeners","_emptyArray","map","_refreshID","skipRevert","isRefreshing","refreshInits","scrollBehavior","refresh","_subPinOffset","horizontal","original","adjustPinSpacing","_dir","endClamp","_endClamp","startClamp","_startClamp","setPositions","render","onRefresh","_lastScroll","_direction","isUpdating","recordVelocity","concat","_capsExp","replace","toLowerCase","tweenTo","pinCache","snapFunc","scroll1","scroll2","markerStart","markerEnd","markerStartTrigger","markerEndTrigger","markerVars","executingOnRefresh","change","pinOriginalState","pinActiveState","pinState","pinGetter","pinSetter","pinStart","pinChange","spacingStart","markerStartSetter","pinMoves","markerEndSetter","snap1","snap2","scrubTween","scrubSmooth","snapDurClamp","snapDelayedCall","prevScroll","prevAnimProgress","caMarkerSetter","customRevertReturn","nodeType","toggleClass","onToggle","scrub","pinSpacing","invalidateOnRefresh","onScrubComplete","onSnapComplete","once","pinReparent","pinSpacer","fastScrollEnd","preventOverlaps","isToggle","scrollerCache","pinType","callbacks","onEnter","onLeave","onEnterBack","onLeaveBack","markers","onRefreshInit","getScrollerSize","_getSizeFunc","getScrollerOffsets","_getOffsetsFunc","lastSnap","lastRefresh","prevProgress","bind","refreshPriority","tweenScroll","scrubDuration","ease","totalProgress","paused","lazy","_initted","isReverted","immediateRender","snapTo","_getClosestLabel","_getLabelAtDirection","st","directional","delay","refreshedRecently","isActive","endValue","endScroll","velocity","naturalEnd","inertia","onStart","resetTo","_tTime","_tDur","stRevert","targets","className","nativeElement","spacerIsNative","classList","force3D","quickSetter","content","_makePositionable","oldOnUpdate","oldParams","onUpdateParams","eventCallback","apply","previous","next","temp","r","prevRefreshing","_swapPinOut","soft","pinOffset","invalidate","isVertical","override","curTrigger","curPin","oppositeScroll","initted","revertedPins","forcedOverflow","markerStartOffset","markerEndOffset","isFirstRefresh","otherPinOffset","parsedEnd","parsedEndTrigger","endTrigger","parsedStart","pinnedContainer","triggerIndex","unshift","_pinPush","normalize","_pinOffset","toUpperCase","ceil","_copyState","omitOffsets","endAnimation","labelToScroll","label","getTrailing","reverse","forceFake","toggleState","action","stateChanged","toggled","isAtMax","isTakingAction","clipped","_dp","_time","_start","n","newStart","newEnd","keepClamp","amount","allowAnimation","onKill","updateFunc","_queueRefreshAll","clearInterval","suppressOverwrites","_rafBugFix","userAgent","mm","bodyHasStyle","hasAttribute","bodyStyle","border","borderTopStyle","AnimationProto","Animation","prototype","Object","defineProperty","removeAttribute","setInterval","checkPrefix","w","h","hidden","limitCallbacks","ms","syncInterval","ignoreMobileResize","autoRefreshEvents","scrollerProxy","clearMatchMedia","isInViewport","positionInViewport","referencePoint","killAll","allowListeners","listeners","saveStyles","getAttribute","safe","clearScrollMemory","maxScroll","getScrollFunc","isScrolling","snapDirectional","batch","proxyCallback","elements","triggers","interval","batchMax","varsCopy","_clampScrollAndGetDurationMultiplier","_allowNativePanning","touchAction","_nestedScroll","node","_isScrollT","scrollHeight","clientHeight","scrollWidth","_overflow","overflowY","overflowX","_isScroll","stopPropagation","_inputObserver","inputs","nested","_captureInputs","_getScrollNormalizer","resumeTouchMove","skipTouchMove","updateClamps","maxY","scrollClampY","normalizeScrollX","scrollClampX","lastRefreshID","removeContentOffset","transform","onResize","startScrollX","startScrollY","momentum","allowNestedScroll","smoother","ScrollSmoother","smootherInstance","get","initialScale","visualViewport","outerWidth","wheelRefresh","resolveMomentumDuration","inputObserver","ignoreDrag","prevScale","currentScroll","dur","velocityX","velocityY","play","_ts","xArray","yArray","yClamped","ticker","_inputIsFocused","auto","_inputExp","isInput","tagName","_sortY","observe","normalizeScroll","normalizer","ss","ref"],"mappings":"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"} \ No newline at end of file diff --git a/dot-line-system/public/gsap.min.js b/dot-line-system/public/gsap.min.js new file mode 100644 index 0000000..c0f9bfa --- /dev/null +++ b/dot-line-system/public/gsap.min.js @@ -0,0 +1,11 @@ +/*! + * GSAP 3.12.7 + * https://gsap.com + * + * @license Copyright 2025, GreenSock. All rights reserved. + * Subject to the terms at https://gsap.com/standard-license or for Club GSAP members, the agreement issued with that membership. + * @author: Jack Doyle, jack@greensock.com + */ + +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function P(t){return(i=yt(t,ot))&&ze}function Q(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function R(t,e){return!e&&console.warn(t)}function S(t,e){return t&&(ot[t]=e)&&i&&(i[t]=e)||ot}function T(){return 0}function ea(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=gt.length;r--&&!gt[r].targetTest(i););e=gt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Vt(t[r],e)))||t.splice(r,1);return t}function fa(t){return t._gsap||ea(Mt(t))[0]._gsap}function ga(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ha(t,e){return(t=t.split(",")).forEach(e)||t}function ia(t){return Math.round(1e5*t)/1e5||0}function ja(t){return Math.round(1e7*t)/1e7||0}function ka(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function la(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++ia;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function ya(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function za(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Aa(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Ca(t,e,r,i){return t._startAt&&(L?t._startAt.revert(ht):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ea(t){return t._repeat?Tt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ga(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ha(t){return t._end=ja(t._start+(t._tDur/Math.abs(t._ts||t._rts||X)||0))}function Ia(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=ja(r._time-(0X)&&e.render(r,!0)),Aa(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur(n=Math.abs(n))&&(a=i,o=n);return a}function tb(t){return za(t),t.scrollTrigger&&t.scrollTrigger.kill(!!L),t.progress()<1&&Ct(t,"onInterrupt"),t}function wb(t){if(t)if(t=!t.name&&t.default||t,x()||t.headless){var e=t.name,r=s(t),i=e&&!r&&t.init?function(){this._props=[]}:t,n={init:T,render:he,add:Wt,kill:ce,modifier:fe,rawVars:0},a={targetTest:0,get:0,getSetter:ne,aliases:{},register:0};if(Ft(),t!==i){if(pt[e])return;qa(i,qa(ua(t,n),a)),yt(i.prototype,yt(n,ua(t,a))),pt[i.prop=e]=i,t.targetTest&&(gt.push(i),ft[e]=1),e=("css"===e?"CSS":e.charAt(0).toUpperCase()+e.substr(1))+"Plugin"}S(e,i),t.register&&t.register(ze,i,_e)}else At.push(t)}function zb(t,e,r){return(6*(t+=t<0?1:1>16,e>>8&St,e&St]:0:zt.black;if(!p){if(","===e.substr(-1)&&(e=e.substr(0,e.length-1)),zt[e])p=zt[e];else if("#"===e.charAt(0)){if(e.length<6&&(e="#"+(n=e.charAt(1))+n+(a=e.charAt(2))+a+(s=e.charAt(3))+s+(5===e.length?e.charAt(4)+e.charAt(4):"")),9===e.length)return[(p=parseInt(e.substr(1,6),16))>>16,p>>8&St,p&St,parseInt(e.substr(7),16)/255];p=[(e=parseInt(e.substr(1),16))>>16,e>>8&St,e&St]}else if("hsl"===e.substr(0,3))if(p=c=e.match(tt),r){if(~e.indexOf("="))return p=e.match(et),i&&p.length<4&&(p[3]=1),p}else o=+p[0]%360/360,u=p[1]/100,n=2*(h=p[2]/100)-(a=h<=.5?h*(u+1):h+u-h*u),3=U?u.endTime(!1):t._dur;return r(e)&&(isNaN(e)||e in o)?(a=e.charAt(0),s="%"===e.substr(-1),n=e.indexOf("="),"<"===a||">"===a?(0<=n&&(e=e.replace(/=/,"")),("<"===a?u._start:u.endTime(0<=u._repeat))+(parseFloat(e.substr(1))||0)*(s?(n<0?u:i).totalDuration()/100:1)):n<0?(e in o||(o[e]=h),o[e]):(a=parseFloat(e.charAt(n-1)+e.substr(n+1)),s&&i&&(a=a/100*(Z(i)?i[0]:i).totalDuration()),1=r&&te)return i;i=i._next}else for(i=t._last;i&&i._start>=r;){if("isPause"===i.data&&i._start=n._start)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0=this.totalDuration()||!v&&_)&&(f!==this._start&&Math.abs(l)===Math.abs(this._ts)||this._lock||(!t&&g||!(v===m&&0=i&&(a instanceof $t?e&&n.push(a):(r&&n.push(a),t&&n.push.apply(n,a.getChildren(!0,e,r)))),a=a._next;return n},e.getById=function getById(t){for(var e=this.getChildren(1,1,1),r=e.length;r--;)if(e[r].vars.id===t)return e[r]},e.remove=function remove(t){return r(t)?this.removeLabel(t):s(t)?this.killTweensOf(t):(t.parent===this&&ya(this,t),t===this._recent&&(this._recent=this._last),Aa(this))},e.totalTime=function totalTime(t,e){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=ja(Rt.time-(0r:!r||s.isActive())&&n.push(s):(i=s.getTweensOf(a,r)).length&&n.push.apply(n,i),s=s._next;return n},e.tweenTo=function tweenTo(t,e){e=e||{};var r,i=this,n=xt(i,t),a=e.startAt,s=e.onStart,o=e.onStartParams,u=e.immediateRender,h=$t.to(i,qa({ease:e.ease||"none",lazy:!1,immediateRender:!1,time:n,overwrite:"auto",duration:e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale())||X,onStart:function onStart(){if(i.pause(),!r){var t=e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale());h._dur!==t&&Ra(h,t,0,1).render(h._time,!0,!0),r=1}s&&s.apply(h,o||[])}},e));return u?h.render(0):h},e.tweenFromTo=function tweenFromTo(t,e,r){return this.tweenTo(e,qa({startAt:{time:xt(this,t)}},r))},e.recent=function recent(){return this._recent},e.nextLabel=function nextLabel(t){return void 0===t&&(t=this._time),rb(this,xt(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),rb(this,xt(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+X)},e.shiftChildren=function shiftChildren(t,e,r){void 0===r&&(r=0);for(var i,n=this._first,a=this.labels;n;)n._start>=r&&(n._start+=t,n._end+=t),n=n._next;if(e)for(i in a)a[i]>=r&&(a[i]+=t);return Aa(this)},e.invalidate=function invalidate(t){var e=this._first;for(this._lock=0;e;)e.invalidate(t),e=e._next;return i.prototype.invalidate.call(this,t)},e.clear=function clear(t){void 0===t&&(t=!0);for(var e,r=this._first;r;)e=r._next,this.remove(r),r=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),Aa(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=U;if(arguments.length)return a.timeScale((a._repeat<0?a.duration():a.totalDuration())/(a.reversed()?-t:t));if(a._dirty){for(i=a.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),o<(r=s._start)&&a._sort&&s._ts&&!a._lock?(a._lock=1,Ka(a,s,r-s._delay,1)._lock=0):o=r,r<0&&s._ts&&(n-=r,(!i&&!a._dp||i&&i.smoothChildTiming)&&(a._start+=r/a._ts,a._time-=r,a._tTime-=r),a.shiftChildren(-r,!1,-Infinity),o=0),s._end>n&&s._ts&&(n=s._end),s=e;Ra(a,a===I&&a._time>n?a._time:n,1,1),a._dirty=0}return a._tDur},Timeline.updateRoot=function updateRoot(t){if(I._ts&&(na(I,Ga(t,I)),f=Rt.frame),Rt.frame>=mt){mt+=q.autoSleep||120;var e=I._first;if((!e||!e._ts)&&q.autoSleep&&Rt._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||Rt.sleep()}}},Timeline}(Ut);qa(Xt.prototype,{_lock:0,_hasPause:0,_forcing:0});function ac(t,e,i,n,a,o){var u,h,l,f;if(pt[t]&&!1!==(u=new pt[t]).init(a,u.rawVars?e[t]:function _processVars(t,e,i,n,a){if(s(t)&&(t=Kt(t,a,e,i,n)),!v(t)||t.style&&t.nodeType||Z(t)||$(t))return r(t)?Kt(t,a,e,i,n):t;var o,u={};for(o in t)u[o]=Kt(t[o],a,e,i,n);return u}(e[t],n,a,o,i),i,n,o)&&(i._pt=h=new _e(i._pt,a,t,0,1,u.render,u,0,u.priority),i!==d))for(l=i._ptLookup[i._targets.indexOf(a)],f=u._props.length;f--;)l[u._props[f]]=h;return u}function gc(t,r,e,i){var n,a,s=r.ease||i||"power1.inOut";if(Z(r))a=e[t]||(e[t]=[]),r.forEach(function(t,e){return a.push({t:e/(r.length-1)*100,v:t,e:s})});else for(n in r)a=e[n]||(e[n]=[]),"ease"===n||a.push({t:parseFloat(t),v:r[n],e:s})}var Nt,Gt,Wt=function _addPropTween(t,e,i,n,a,o,u,h,l,f){s(n)&&(n=n(a||0,t,o));var d,c=t[e],p="get"!==i?i:s(c)?l?t[e.indexOf("set")||!s(t["get"+e.substr(3)])?e:"get"+e.substr(3)](l):t[e]():c,_=s(c)?l?re:te:Zt;if(r(n)&&(~n.indexOf("random(")&&(n=ob(n)),"="===n.charAt(1)&&(!(d=ka(p,n)+(Ya(p)||0))&&0!==d||(n=d))),!f||p!==n||Gt)return isNaN(p*n)||""===n?(c||e in t||Q(e,n),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,d,c,p,_=new _e(this._pt,t,e,0,1,ue,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(c=~(i+="").indexOf("random("))&&(i=ob(i)),a&&(a(p=[r,i],t,e),r=p[0],i=p[1]),u=r.match(it)||[];o=it.exec(i);)l=o[0],f=i.substring(m,o.index),h?h=(h+1)%5:"rgba("===f.substr(-5)&&(h=1),l!==u[g++]&&(d=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:d,c:"="===l.charAt(1)?ka(d,l)-d:parseFloat(l)-d,m:h&&h<4?Math.round:0},m=it.lastIndex);return _.c=m")}),s.duration();else{for(l in u={},x)"ease"===l||"easeEach"===l||gc(l,x[l],u,x.easeEach);for(l in u)for(A=u[l].sort(function(t,e){return t.t-e.t}),o=E=0;o=t._tDur||e<0)&&t.ratio===u&&(u&&za(t,1),r||L||(Ct(t,u?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,r);return this},e.targets=function targets(){return this._targets},e.invalidate=function invalidate(t){return t&&this.vars.runBackwards||(this._startAt=0),this._pt=this._op=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(t),D.prototype.invalidate.call(this,t)},e.resetTo=function resetTo(t,e,r,i,n){c||Rt.wake(),this._ts||this.play();var a,s=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||Qt(this,s),a=this._ease(s/this._dur),function _updatePropTweens(t,e,r,i,n,a,s,o){var u,h,l,f,d=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!d)for(d=t._ptCache[e]=[],l=t._ptLookup,f=t._targets.length;f--;){if((u=l[f][e])&&u.d&&u.d._pt)for(u=u.d._pt;u&&u.p!==e&&u.fp!==e;)u=u._next;if(!u)return Gt=1,t.vars[e]="+=0",Qt(t,s),Gt=0,o?R(e+" not eligible for reset"):1;d.push(u)}for(f=d.length;f--;)(u=(h=d[f])._pt||h).s=!i&&0!==i||n?u.s+(i||0)+a*u.c:i,u.c=r-u.s,h.e&&(h.e=ia(r)+Ya(h.e)),h.b&&(h.b=u.s+Ya(h.b))}(this,t,e,r,i,a,s,n)?this.resetTo(t,e,r,i,1):(Ia(this,0),this.parent||xa(this._dp,this,"_first","_last",this._dp._sort?"_start":0),this.render(0))},e.kill=function kill(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?tb(this):this.scrollTrigger&&this.scrollTrigger.kill(!!L),this;if(this.timeline){var i=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,Nt&&!0!==Nt.vars.overwrite)._first||tb(this),this.parent&&i!==this.timeline.totalDuration()&&Ra(this,this._dur*this.timeline._tDur/i,0,1),this}var n,a,s,o,u,h,l,f=this._targets,d=t?Mt(t):f,c=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function _arraysMatch(t,e){for(var r=t.length,i=r===e.length;i&&r--&&t[r]===e[r];);return r<0}(f,d))return"all"===e&&(this._pt=0),tb(this);for(n=this._op=this._op||[],"all"!==e&&(r(e)&&(u={},ha(e,function(t){return u[t]=1}),e=u),e=function _addAliasesToVars(t,e){var r,i,n,a,s=t[0]?fa(t[0]).harness:0,o=s&&s.aliases;if(!o)return e;for(i in r=yt({},e),o)if(i in r)for(n=(a=o[i].split(",")).length;n--;)r[a[n]]=r[i];return r}(f,e)),l=f.length;l--;)if(~d.indexOf(f[l]))for(u in a=c[l],"all"===e?(n[l]=e,o=a,s={}):(s=n[l]=n[l]||{},o=e),o)(h=a&&a[u])&&("kill"in h.d&&!0!==h.d.kill(u)||ya(this,h,"_pt"),delete a[u]),"all"!==s&&(s[u]=1);return this._initted&&!this._pt&&p&&tb(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return Va(1,arguments)},Tween.delayedCall=function delayedCall(t,e,r,i){return new Tween(e,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:e,onReverseComplete:e,onCompleteParams:r,onReverseCompleteParams:r,callbackScope:i})},Tween.fromTo=function fromTo(t,e,r){return Va(2,arguments)},Tween.set=function set(t,e){return e.duration=0,e.repeatDelay||(e.repeat=0),new Tween(t,e)},Tween.killTweensOf=function killTweensOf(t,e,r){return I.killTweensOf(t,e,r)},Tween}(Ut);qa($t.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ha("staggerTo,staggerFrom,staggerFromTo",function(r){$t[r]=function(){var t=new Xt,e=Ot.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function oc(t,e,r){return t.setAttribute(e,r)}function wc(t,e,r,i){i.mSet(t,e,i.m.call(i.tween,r,i.mt),i)}var Zt=function _setterPlain(t,e,r){return t[e]=r},te=function _setterFunc(t,e,r){return t[e](r)},re=function _setterFuncWithParam(t,e,r,i){return t[e](i.fp,r)},ne=function _getSetter(t,e){return s(t[e])?te:u(t[e])&&t.setAttribute?oc:Zt},ae=function _renderPlain(t,e){return e.set(e.t,e.p,Math.round(1e6*(e.s+e.c*t))/1e6,e)},se=function _renderBoolean(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},ue=function _renderComplexString(t,e){var r=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;r;)i=r.p+(r.m?r.m(r.s+r.c*t):Math.round(1e4*(r.s+r.c*t))/1e4)+i,r=r._next;i+=e.c}e.set(e.t,e.p,i,e)},he=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},fe=function _addPluginModifier(t,e,r,i){for(var n,a=this._pt;a;)n=a._next,a.p===i&&a.modifier(t,e,r),a=n},ce=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?ya(this,i,"_pt"):i.dep||(e=1),i=r;return!e},pe=function _sortPropTweensByPriority(t){for(var e,r,i,n,a=t._pt;a;){for(e=a._next,r=i;r&&r.pr>a.pr;)r=r._next;(a._prev=r?r._prev:n)?a._prev._next=a:i=a,(a._next=r)?r._prev=a:n=a,a=e}t._pt=i},_e=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=wc,this.m=t,this.mt=r,this.tween=e},PropTween);function PropTween(t,e,r,i,n,a,s,o,u){this.t=e,this.s=i,this.c=n,this.p=r,this.r=a||ae,this.d=s||this,this.set=o||Zt,this.pr=u||0,(this._next=t)&&(t._prev=this)}ha(vt+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",function(t){return ft[t]=1}),ot.TweenMax=ot.TweenLite=$t,ot.TimelineLite=ot.TimelineMax=Xt,I=new Xt({sortChildren:!1,defaults:V,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),q.stringFilter=Fb;function Ec(t){return(ye[t]||Te).map(function(t){return t()})}function Fc(){var t=Date.now(),o=[];2 typeof(value) === \"string\",\n\t_isFunction = value => typeof(value) === \"function\",\n\t_isNumber = value => typeof(value) === \"number\",\n\t_isUndefined = value => typeof(value) === \"undefined\",\n\t_isObject = value => typeof(value) === \"object\",\n\t_isNotFalse = value => value !== false,\n\t_windowExists = () => typeof(window) !== \"undefined\",\n\t_isFuncOrString = value => _isFunction(value) || _isString(value),\n\t_isTypedArray = (typeof ArrayBuffer === \"function\" && ArrayBuffer.isView) || function() {}, // note: IE10 has ArrayBuffer, but NOT ArrayBuffer.isView().\n\t_isArray = Array.isArray,\n\t_strictNumExp = /(?:-?\\.?\\d|\\.)+/gi, //only numbers (including negatives and decimals) but NOT relative values.\n\t_numExp = /[-+=.]*\\d+[.e\\-+]*\\d*[e\\-+]*\\d*/g, //finds any numbers, including ones that start with += or -=, negative numbers, and ones in scientific notation like 1e-8.\n\t_numWithUnitExp = /[-+=.]*\\d+[.e-]*\\d*[a-z%]*/g,\n\t_complexStringNumExp = /[-+=.]*\\d+\\.?\\d*(?:e-|e\\+)?\\d*/gi, //duplicate so that while we're looping through matches from exec(), it doesn't contaminate the lastIndex of _numExp which we use to search for colors too.\n\t_relExp = /[+-]=-?[.\\d]+/,\n\t_delimitedValueExp = /[^,'\"\\[\\]\\s]+/gi, // previously /[#\\-+.]*\\b[a-z\\d\\-=+%.]+/gi but didn't catch special characters.\n\t_unitExp = /^[+\\-=e\\s\\d]*\\d+[.\\d]*([a-z]*|%)\\s*$/i,\n\t_globalTimeline, _win, _coreInitted, _doc,\n\t_globals = {},\n\t_installScope = {},\n\t_coreReady,\n\t_install = scope => (_installScope = _merge(scope, _globals)) && gsap,\n\t_missingPlugin = (property, value) => console.warn(\"Invalid property\", property, \"set to\", value, \"Missing plugin? gsap.registerPlugin()\"),\n\t_warn = (message, suppress) => !suppress && console.warn(message),\n\t_addGlobal = (name, obj) => (name && (_globals[name] = obj) && (_installScope && (_installScope[name] = obj))) || _globals,\n\t_emptyFunc = () => 0,\n\t_startAtRevertConfig = {suppressEvents: true, isStart: true, kill: false},\n\t_revertConfigNoKill = {suppressEvents: true, kill: false},\n\t_revertConfig = {suppressEvents: true},\n\t_reservedProps = {},\n\t_lazyTweens = [],\n\t_lazyLookup = {},\n\t_lastRenderedFrame,\n\t_plugins = {},\n\t_effects = {},\n\t_nextGCFrame = 30,\n\t_harnessPlugins = [],\n\t_callbackNames = \"\",\n\t_harness = targets => {\n\t\tlet target = targets[0],\n\t\t\tharnessPlugin, i;\n\t\t_isObject(target) || _isFunction(target) || (targets = [targets]);\n\t\tif (!(harnessPlugin = (target._gsap || {}).harness)) { // find the first target with a harness. We assume targets passed into an animation will be of similar type, meaning the same kind of harness can be used for them all (performance optimization)\n\t\t\ti = _harnessPlugins.length;\n\t\t\twhile (i-- && !_harnessPlugins[i].targetTest(target)) {\t}\n\t\t\tharnessPlugin = _harnessPlugins[i];\n\t\t}\n\t\ti = targets.length;\n\t\twhile (i--) {\n\t\t\t(targets[i] && (targets[i]._gsap || (targets[i]._gsap = new GSCache(targets[i], harnessPlugin)))) || targets.splice(i, 1);\n\t\t}\n\t\treturn targets;\n\t},\n\t_getCache = target => target._gsap || _harness(toArray(target))[0]._gsap,\n\t_getProperty = (target, property, v) => (v = target[property]) && _isFunction(v) ? target[property]() : (_isUndefined(v) && target.getAttribute && target.getAttribute(property)) || v,\n\t_forEachName = (names, func) => ((names = names.split(\",\")).forEach(func)) || names, //split a comma-delimited list of names into an array, then run a forEach() function and return the split array (this is just a way to consolidate/shorten some code).\n\t_round = value => Math.round(value * 100000) / 100000 || 0,\n\t_roundPrecise = value => Math.round(value * 10000000) / 10000000 || 0, // increased precision mostly for timing values.\n\t_parseRelative = (start, value) => {\n\t\tlet operator = value.charAt(0),\n\t\t\tend = parseFloat(value.substr(2));\n\t\tstart = parseFloat(start);\n\t\treturn operator === \"+\" ? start + end : operator === \"-\" ? start - end : operator === \"*\" ? start * end : start / end;\n\t},\n\t_arrayContainsAny = (toSearch, toFind) => { //searches one array to find matches for any of the items in the toFind array. As soon as one is found, it returns true. It does NOT return all the matches; it's simply a boolean search.\n\t\tlet l = toFind.length,\n\t\t\ti = 0;\n\t\tfor (; toSearch.indexOf(toFind[i]) < 0 && ++i < l;) { }\n\t\treturn (i < l);\n\t},\n\t_lazyRender = () => {\n\t\tlet l = _lazyTweens.length,\n\t\t\ta = _lazyTweens.slice(0),\n\t\t\ti, tween;\n\t\t_lazyLookup = {};\n\t\t_lazyTweens.length = 0;\n\t\tfor (i = 0; i < l; i++) {\n\t\t\ttween = a[i];\n\t\t\ttween && tween._lazy && (tween.render(tween._lazy[0], tween._lazy[1], true)._lazy = 0);\n\t\t}\n\t},\n\t_lazySafeRender = (animation, time, suppressEvents, force) => {\n\t\t_lazyTweens.length && !_reverting && _lazyRender();\n\t\tanimation.render(time, suppressEvents, force || (_reverting && time < 0 && (animation._initted || animation._startAt)));\n\t\t_lazyTweens.length && !_reverting && _lazyRender(); //in case rendering caused any tweens to lazy-init, we should render them because typically when someone calls seek() or time() or progress(), they expect an immediate render.\n\t},\n\t_numericIfPossible = value => {\n\t\tlet n = parseFloat(value);\n\t\treturn (n || n === 0) && (value + \"\").match(_delimitedValueExp).length < 2 ? n : _isString(value) ? value.trim() : value;\n\t},\n\t_passThrough = p => p,\n\t_setDefaults = (obj, defaults) => {\n\t\tfor (let p in defaults) {\n\t\t\t(p in obj) || (obj[p] = defaults[p]);\n\t\t}\n\t\treturn obj;\n\t},\n\t_setKeyframeDefaults = excludeDuration => (obj, defaults) => {\n\t\tfor (let p in defaults) {\n\t\t\t(p in obj) || (p === \"duration\" && excludeDuration) || p === \"ease\" || (obj[p] = defaults[p]);\n\t\t}\n\t},\n\t_merge = (base, toMerge) => {\n\t\tfor (let p in toMerge) {\n\t\t\tbase[p] = toMerge[p];\n\t\t}\n\t\treturn base;\n\t},\n\t_mergeDeep = (base, toMerge) => {\n\t\tfor (let p in toMerge) {\n\t\t\tp !== \"__proto__\" && p !== \"constructor\" && p !== \"prototype\" && (base[p] = _isObject(toMerge[p]) ? _mergeDeep(base[p] || (base[p] = {}), toMerge[p]) : toMerge[p]);\n\t\t}\n\t\treturn base;\n\t},\n\t_copyExcluding = (obj, excluding) => {\n\t\tlet copy = {},\n\t\t\tp;\n\t\tfor (p in obj) {\n\t\t\t(p in excluding) || (copy[p] = obj[p]);\n\t\t}\n\t\treturn copy;\n\t},\n\t_inheritDefaults = vars => {\n\t\tlet parent = vars.parent || _globalTimeline,\n\t\t\tfunc = vars.keyframes ? _setKeyframeDefaults(_isArray(vars.keyframes)) : _setDefaults;\n\t\tif (_isNotFalse(vars.inherit)) {\n\t\t\twhile (parent) {\n\t\t\t\tfunc(vars, parent.vars.defaults);\n\t\t\t\tparent = parent.parent || parent._dp;\n\t\t\t}\n\t\t}\n\t\treturn vars;\n\t},\n\t_arraysMatch = (a1, a2) => {\n\t\tlet i = a1.length,\n\t\t\tmatch = i === a2.length;\n\t\twhile (match && i-- && a1[i] === a2[i]) { }\n\t\treturn i < 0;\n\t},\n\t_addLinkedListItem = (parent, child, firstProp = \"_first\", lastProp = \"_last\", sortBy) => {\n\t\tlet prev = parent[lastProp],\n\t\t\tt;\n\t\tif (sortBy) {\n\t\t\tt = child[sortBy];\n\t\t\twhile (prev && prev[sortBy] > t) {\n\t\t\t\tprev = prev._prev;\n\t\t\t}\n\t\t}\n\t\tif (prev) {\n\t\t\tchild._next = prev._next;\n\t\t\tprev._next = child;\n\t\t} else {\n\t\t\tchild._next = parent[firstProp];\n\t\t\tparent[firstProp] = child;\n\t\t}\n\t\tif (child._next) {\n\t\t\tchild._next._prev = child;\n\t\t} else {\n\t\t\tparent[lastProp] = child;\n\t\t}\n\t\tchild._prev = prev;\n\t\tchild.parent = child._dp = parent;\n\t\treturn child;\n\t},\n\t_removeLinkedListItem = (parent, child, firstProp = \"_first\", lastProp = \"_last\") => {\n\t\tlet prev = child._prev,\n\t\t\tnext = child._next;\n\t\tif (prev) {\n\t\t\tprev._next = next;\n\t\t} else if (parent[firstProp] === child) {\n\t\t\tparent[firstProp] = next;\n\t\t}\n\t\tif (next) {\n\t\t\tnext._prev = prev;\n\t\t} else if (parent[lastProp] === child) {\n\t\t\tparent[lastProp] = prev;\n\t\t}\n\t\tchild._next = child._prev = child.parent = null; // don't delete the _dp just so we can revert if necessary. But parent should be null to indicate the item isn't in a linked list.\n\t},\n\t_removeFromParent = (child, onlyIfParentHasAutoRemove) => {\n\t\tchild.parent && (!onlyIfParentHasAutoRemove || child.parent.autoRemoveChildren) && child.parent.remove && child.parent.remove(child);\n\t\tchild._act = 0;\n\t},\n\t_uncache = (animation, child) => {\n\t\tif (animation && (!child || child._end > animation._dur || child._start < 0)) { // performance optimization: if a child animation is passed in we should only uncache if that child EXTENDS the animation (its end time is beyond the end)\n\t\t\tlet a = animation;\n\t\t\twhile (a) {\n\t\t\t\ta._dirty = 1;\n\t\t\t\ta = a.parent;\n\t\t\t}\n\t\t}\n\t\treturn animation;\n\t},\n\t_recacheAncestors = animation => {\n\t\tlet parent = animation.parent;\n\t\twhile (parent && parent.parent) { //sometimes we must force a re-sort of all children and update the duration/totalDuration of all ancestor timelines immediately in case, for example, in the middle of a render loop, one tween alters another tween's timeScale which shoves its startTime before 0, forcing the parent timeline to shift around and shiftChildren() which could affect that next tween's render (startTime). Doesn't matter for the root timeline though.\n\t\t\tparent._dirty = 1;\n\t\t\tparent.totalDuration();\n\t\t\tparent = parent.parent;\n\t\t}\n\t\treturn animation;\n\t},\n\t_rewindStartAt = (tween, totalTime, suppressEvents, force) => tween._startAt && (_reverting ? tween._startAt.revert(_revertConfigNoKill) : (tween.vars.immediateRender && !tween.vars.autoRevert) || tween._startAt.render(totalTime, true, force)),\n\t_hasNoPausedAncestors = animation => !animation || (animation._ts && _hasNoPausedAncestors(animation.parent)),\n\t_elapsedCycleDuration = animation => animation._repeat ? _animationCycle(animation._tTime, (animation = animation.duration() + animation._rDelay)) * animation : 0,\n\t// feed in the totalTime and cycleDuration and it'll return the cycle (iteration minus 1) and if the playhead is exactly at the very END, it will NOT bump up to the next cycle.\n\t_animationCycle = (tTime, cycleDuration) => {\n\t\tlet whole = Math.floor(tTime = _roundPrecise(tTime / cycleDuration));\n\t\treturn tTime && (whole === tTime) ? whole - 1 : whole;\n\t},\n\t_parentToChildTotalTime = (parentTime, child) => (parentTime - child._start) * child._ts + (child._ts >= 0 ? 0 : (child._dirty ? child.totalDuration() : child._tDur)),\n\t_setEnd = animation => (animation._end = _roundPrecise(animation._start + ((animation._tDur / Math.abs(animation._ts || animation._rts || _tinyNum)) || 0))),\n\t_alignPlayhead = (animation, totalTime) => { // adjusts the animation's _start and _end according to the provided totalTime (only if the parent's smoothChildTiming is true and the animation isn't paused). It doesn't do any rendering or forcing things back into parent timelines, etc. - that's what totalTime() is for.\n\t\tlet parent = animation._dp;\n\t\tif (parent && parent.smoothChildTiming && animation._ts) {\n\t\t\tanimation._start = _roundPrecise(parent._time - (animation._ts > 0 ? totalTime / animation._ts : ((animation._dirty ? animation.totalDuration() : animation._tDur) - totalTime) / -animation._ts));\n\t\t\t_setEnd(animation);\n\t\t\tparent._dirty || _uncache(parent, animation); //for performance improvement. If the parent's cache is already dirty, it already took care of marking the ancestors as dirty too, so skip the function call here.\n\t\t}\n\t\treturn animation;\n\t},\n\t/*\n\t_totalTimeToTime = (clampedTotalTime, duration, repeat, repeatDelay, yoyo) => {\n\t\tlet cycleDuration = duration + repeatDelay,\n\t\t\ttime = _round(clampedTotalTime % cycleDuration);\n\t\tif (time > duration) {\n\t\t\ttime = duration;\n\t\t}\n\t\treturn (yoyo && (~~(clampedTotalTime / cycleDuration) & 1)) ? duration - time : time;\n\t},\n\t*/\n\t_postAddChecks = (timeline, child) => {\n\t\tlet t;\n\t\tif (child._time || (!child._dur && child._initted) || (child._start < timeline._time && (child._dur || !child.add))) { // in case, for example, the _start is moved on a tween that has already rendered, or if it's being inserted into a timeline BEFORE where the playhead is currently. Imagine it's at its end state, then the startTime is moved WAY later (after the end of this timeline), it should render at its beginning. Special case: if it's a timeline (has .add() method) and no duration, we can skip rendering because the user may be populating it AFTER adding it to a parent timeline (unconventional, but possible, and we wouldn't want it to get removed if the parent's autoRemoveChildren is true).\n\t\t\tt = _parentToChildTotalTime(timeline.rawTime(), child);\n\t\t\tif (!child._dur || _clamp(0, child.totalDuration(), t) - child._tTime > _tinyNum) {\n\t\t\t\tchild.render(t, true);\n\t\t\t}\n\t\t}\n\t\t//if the timeline has already ended but the inserted tween/timeline extends the duration, we should enable this timeline again so that it renders properly. We should also align the playhead with the parent timeline's when appropriate.\n\t\tif (_uncache(timeline, child)._dp && timeline._initted && timeline._time >= timeline._dur && timeline._ts) {\n\t\t\t//in case any of the ancestors had completed but should now be enabled...\n\t\t\tif (timeline._dur < timeline.duration()) {\n\t\t\t\tt = timeline;\n\t\t\t\twhile (t._dp) {\n\t\t\t\t\t(t.rawTime() >= 0) && t.totalTime(t._tTime); //moves the timeline (shifts its startTime) if necessary, and also enables it. If it's currently zero, though, it may not be scheduled to render until later so there's no need to force it to align with the current playhead position. Only move to catch up with the playhead.\n\t\t\t\t\tt = t._dp;\n\t\t\t\t}\n\t\t\t}\n\t\t\ttimeline._zTime = -_tinyNum; // helps ensure that the next render() will be forced (crossingStart = true in render()), even if the duration hasn't changed (we're adding a child which would need to get rendered). Definitely an edge case. Note: we MUST do this AFTER the loop above where the totalTime() might trigger a render() because this _addToTimeline() method gets called from the Animation constructor, BEFORE tweens even record their targets, etc. so we wouldn't want things to get triggered in the wrong order.\n\t\t}\n\t},\n\t_addToTimeline = (timeline, child, position, skipChecks) => {\n\t\tchild.parent && _removeFromParent(child);\n\t\tchild._start = _roundPrecise((_isNumber(position) ? position : position || timeline !== _globalTimeline ? _parsePosition(timeline, position, child) : timeline._time) + child._delay);\n\t\tchild._end = _roundPrecise(child._start + ((child.totalDuration() / Math.abs(child.timeScale())) || 0));\n\t\t_addLinkedListItem(timeline, child, \"_first\", \"_last\", timeline._sort ? \"_start\" : 0);\n\t\t_isFromOrFromStart(child) || (timeline._recent = child);\n\t\tskipChecks || _postAddChecks(timeline, child);\n\t\ttimeline._ts < 0 && _alignPlayhead(timeline, timeline._tTime); // if the timeline is reversed and the new child makes it longer, we may need to adjust the parent's _start (push it back)\n\t\treturn timeline;\n\t},\n\t_scrollTrigger = (animation, trigger) => (_globals.ScrollTrigger || _missingPlugin(\"scrollTrigger\", trigger)) && _globals.ScrollTrigger.create(trigger, animation),\n\t_attemptInitTween = (tween, time, force, suppressEvents, tTime) => {\n\t\t_initTween(tween, time, tTime);\n\t\tif (!tween._initted) {\n\t\t\treturn 1;\n\t\t}\n\t\tif (!force && tween._pt && !_reverting && ((tween._dur && tween.vars.lazy !== false) || (!tween._dur && tween.vars.lazy)) && _lastRenderedFrame !== _ticker.frame) {\n\t\t\t_lazyTweens.push(tween);\n\t\t\ttween._lazy = [tTime, suppressEvents];\n\t\t\treturn 1;\n\t\t}\n\t},\n\t_parentPlayheadIsBeforeStart = ({parent}) => parent && parent._ts && parent._initted && !parent._lock && (parent.rawTime() < 0 || _parentPlayheadIsBeforeStart(parent)), // check parent's _lock because when a timeline repeats/yoyos and does its artificial wrapping, we shouldn't force the ratio back to 0\n\t_isFromOrFromStart = ({data}) => data === \"isFromStart\" || data === \"isStart\",\n\t_renderZeroDurationTween = (tween, totalTime, suppressEvents, force) => {\n\t\tlet prevRatio = tween.ratio,\n\t\t\tratio = totalTime < 0 || (!totalTime && ((!tween._start && _parentPlayheadIsBeforeStart(tween) && !(!tween._initted && _isFromOrFromStart(tween))) || ((tween._ts < 0 || tween._dp._ts < 0) && !_isFromOrFromStart(tween)))) ? 0 : 1, // if the tween or its parent is reversed and the totalTime is 0, we should go to a ratio of 0. Edge case: if a from() or fromTo() stagger tween is placed later in a timeline, the \"startAt\" zero-duration tween could initially render at a time when the parent timeline's playhead is technically BEFORE where this tween is, so make sure that any \"from\" and \"fromTo\" startAt tweens are rendered the first time at a ratio of 1.\n\t\t\trepeatDelay = tween._rDelay,\n\t\t\ttTime = 0,\n\t\t\tpt, iteration, prevIteration;\n\t\tif (repeatDelay && tween._repeat) { // in case there's a zero-duration tween that has a repeat with a repeatDelay\n\t\t\ttTime = _clamp(0, tween._tDur, totalTime);\n\t\t\titeration = _animationCycle(tTime, repeatDelay);\n\t\t\ttween._yoyo && (iteration & 1) && (ratio = 1 - ratio);\n\t\t\tif (iteration !== _animationCycle(tween._tTime, repeatDelay)) { // if iteration changed\n\t\t\t\tprevRatio = 1 - ratio;\n\t\t\t\ttween.vars.repeatRefresh && tween._initted && tween.invalidate();\n\t\t\t}\n\t\t}\n\t\tif (ratio !== prevRatio || _reverting || force || tween._zTime === _tinyNum || (!totalTime && tween._zTime)) {\n\t\t\tif (!tween._initted && _attemptInitTween(tween, totalTime, force, suppressEvents, tTime)) { // if we render the very beginning (time == 0) of a fromTo(), we must force the render (normal tweens wouldn't need to render at a time of 0 when the prevTime was also 0). This is also mandatory to make sure overwriting kicks in immediately.\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprevIteration = tween._zTime;\n\t\t\ttween._zTime = totalTime || (suppressEvents ? _tinyNum : 0); // when the playhead arrives at EXACTLY time 0 (right on top) of a zero-duration tween, we need to discern if events are suppressed so that when the playhead moves again (next time), it'll trigger the callback. If events are NOT suppressed, obviously the callback would be triggered in this render. Basically, the callback should fire either when the playhead ARRIVES or LEAVES this exact spot, not both. Imagine doing a timeline.seek(0) and there's a callback that sits at 0. Since events are suppressed on that seek() by default, nothing will fire, but when the playhead moves off of that position, the callback should fire. This behavior is what people intuitively expect.\n\t\t\tsuppressEvents || (suppressEvents = totalTime && !prevIteration); // if it was rendered previously at exactly 0 (_zTime) and now the playhead is moving away, DON'T fire callbacks otherwise they'll seem like duplicates.\n\t\t\ttween.ratio = ratio;\n\t\t\ttween._from && (ratio = 1 - ratio);\n\t\t\ttween._time = 0;\n\t\t\ttween._tTime = tTime;\n\t\t\tpt = tween._pt;\n\t\t\twhile (pt) {\n\t\t\t\tpt.r(ratio, pt.d);\n\t\t\t\tpt = pt._next;\n\t\t\t}\n\t\t\ttotalTime < 0 && _rewindStartAt(tween, totalTime, suppressEvents, true);\n\t\t\ttween._onUpdate && !suppressEvents && _callback(tween, \"onUpdate\");\n\t\t\ttTime && tween._repeat && !suppressEvents && tween.parent && _callback(tween, \"onRepeat\");\n\t\t\tif ((totalTime >= tween._tDur || totalTime < 0) && tween.ratio === ratio) {\n\t\t\t\tratio && _removeFromParent(tween, 1);\n\t\t\t\tif (!suppressEvents && !_reverting) {\n\t\t\t\t\t_callback(tween, (ratio ? \"onComplete\" : \"onReverseComplete\"), true);\n\t\t\t\t\ttween._prom && tween._prom();\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (!tween._zTime) {\n\t\t\ttween._zTime = totalTime;\n\t\t}\n\t},\n\t_findNextPauseTween = (animation, prevTime, time) => {\n\t\tlet child;\n\t\tif (time > prevTime) {\n\t\t\tchild = animation._first;\n\t\t\twhile (child && child._start <= time) {\n\t\t\t\tif (child.data === \"isPause\" && child._start > prevTime) {\n\t\t\t\t\treturn child;\n\t\t\t\t}\n\t\t\t\tchild = child._next;\n\t\t\t}\n\t\t} else {\n\t\t\tchild = animation._last;\n\t\t\twhile (child && child._start >= time) {\n\t\t\t\tif (child.data === \"isPause\" && child._start < prevTime) {\n\t\t\t\t\treturn child;\n\t\t\t\t}\n\t\t\t\tchild = child._prev;\n\t\t\t}\n\t\t}\n\t},\n\t_setDuration = (animation, duration, skipUncache, leavePlayhead) => {\n\t\tlet repeat = animation._repeat,\n\t\t\tdur = _roundPrecise(duration) || 0,\n\t\t\ttotalProgress = animation._tTime / animation._tDur;\n\t\ttotalProgress && !leavePlayhead && (animation._time *= dur / animation._dur);\n\t\tanimation._dur = dur;\n\t\tanimation._tDur = !repeat ? dur : repeat < 0 ? 1e10 : _roundPrecise(dur * (repeat + 1) + (animation._rDelay * repeat));\n\t\ttotalProgress > 0 && !leavePlayhead && _alignPlayhead(animation, (animation._tTime = animation._tDur * totalProgress));\n\t\tanimation.parent && _setEnd(animation);\n\t\tskipUncache || _uncache(animation.parent, animation);\n\t\treturn animation;\n\t},\n\t_onUpdateTotalDuration = animation => (animation instanceof Timeline) ? _uncache(animation) : _setDuration(animation, animation._dur),\n\t_zeroPosition = {_start:0, endTime:_emptyFunc, totalDuration:_emptyFunc},\n\t_parsePosition = (animation, position, percentAnimation) => {\n\t\tlet labels = animation.labels,\n\t\t\trecent = animation._recent || _zeroPosition,\n\t\t\tclippedDuration = animation.duration() >= _bigNum ? recent.endTime(false) : animation._dur, //in case there's a child that infinitely repeats, users almost never intend for the insertion point of a new child to be based on a SUPER long value like that so we clip it and assume the most recently-added child's endTime should be used instead.\n\t\t\ti, offset, isPercent;\n\t\tif (_isString(position) && (isNaN(position) || (position in labels))) { //if the string is a number like \"1\", check to see if there's a label with that name, otherwise interpret it as a number (absolute value).\n\t\t\toffset = position.charAt(0);\n\t\t\tisPercent = position.substr(-1) === \"%\";\n\t\t\ti = position.indexOf(\"=\");\n\t\t\tif (offset === \"<\" || offset === \">\") {\n\t\t\t\ti >= 0 && (position = position.replace(/=/, \"\"));\n\t\t\t\treturn (offset === \"<\" ? recent._start : recent.endTime(recent._repeat >= 0)) + (parseFloat(position.substr(1)) || 0) * (isPercent ? (i < 0 ? recent : percentAnimation).totalDuration() / 100 : 1);\n\t\t\t}\n\t\t\tif (i < 0) {\n\t\t\t\t(position in labels) || (labels[position] = clippedDuration);\n\t\t\t\treturn labels[position];\n\t\t\t}\n\t\t\toffset = parseFloat(position.charAt(i-1) + position.substr(i+1));\n\t\t\tif (isPercent && percentAnimation) {\n\t\t\t\toffset = offset / 100 * (_isArray(percentAnimation) ? percentAnimation[0] : percentAnimation).totalDuration();\n\t\t\t}\n\t\t\treturn (i > 1) ? _parsePosition(animation, position.substr(0, i-1), percentAnimation) + offset : clippedDuration + offset;\n\t\t}\n\t\treturn (position == null) ? clippedDuration : +position;\n\t},\n\t_createTweenType = (type, params, timeline) => {\n\t\tlet isLegacy = _isNumber(params[1]),\n\t\t\tvarsIndex = (isLegacy ? 2 : 1) + (type < 2 ? 0 : 1),\n\t\t\tvars = params[varsIndex],\n\t\t\tirVars, parent;\n\t\tisLegacy && (vars.duration = params[1]);\n\t\tvars.parent = timeline;\n\t\tif (type) {\n\t\t\tirVars = vars;\n\t\t\tparent = timeline;\n\t\t\twhile (parent && !(\"immediateRender\" in irVars)) { // inheritance hasn't happened yet, but someone may have set a default in an ancestor timeline. We could do vars.immediateRender = _isNotFalse(_inheritDefaults(vars).immediateRender) but that'd exact a slight performance penalty because _inheritDefaults() also runs in the Tween constructor. We're paying a small kb price here to gain speed.\n\t\t\t\tirVars = parent.vars.defaults || {};\n\t\t\t\tparent = _isNotFalse(parent.vars.inherit) && parent.parent;\n\t\t\t}\n\t\t\tvars.immediateRender = _isNotFalse(irVars.immediateRender);\n\t\t\ttype < 2 ? (vars.runBackwards = 1) : (vars.startAt = params[varsIndex - 1]); // \"from\" vars\n\t\t}\n\t\treturn new Tween(params[0], vars, params[varsIndex + 1]);\n\t},\n\t_conditionalReturn = (value, func) => value || value === 0 ? func(value) : func,\n\t_clamp = (min, max, value) => value < min ? min : value > max ? max : value,\n\tgetUnit = (value, v) => !_isString(value) || !(v = _unitExp.exec(value)) ? \"\" : v[1], // note: protect against padded numbers as strings, like \"100.100\". That shouldn't return \"00\" as the unit. If it's numeric, return no unit.\n\tclamp = (min, max, value) => _conditionalReturn(value, v => _clamp(min, max, v)),\n\t_slice = [].slice,\n\t_isArrayLike = (value, nonEmpty) => value && (_isObject(value) && \"length\" in value && ((!nonEmpty && !value.length) || ((value.length - 1) in value && _isObject(value[0]))) && !value.nodeType && value !== _win),\n\t_flatten = (ar, leaveStrings, accumulator = []) => ar.forEach(value => (_isString(value) && !leaveStrings) || _isArrayLike(value, 1) ? accumulator.push(...toArray(value)) : accumulator.push(value)) || accumulator,\n\t//takes any value and returns an array. If it's a string (and leaveStrings isn't true), it'll use document.querySelectorAll() and convert that to an array. It'll also accept iterables like jQuery objects.\n\ttoArray = (value, scope, leaveStrings) => _context && !scope && _context.selector ? _context.selector(value) : _isString(value) && !leaveStrings && (_coreInitted || !_wake()) ? _slice.call((scope || _doc).querySelectorAll(value), 0) : _isArray(value) ? _flatten(value, leaveStrings) : _isArrayLike(value) ? _slice.call(value, 0) : value ? [value] : [],\n\tselector = value => {\n\t\tvalue = toArray(value)[0] || _warn(\"Invalid scope\") || {};\n\t\treturn v => {\n\t\t\tlet el = value.current || value.nativeElement || value;\n\t\t\treturn toArray(v, el.querySelectorAll ? el : el === value ? _warn(\"Invalid scope\") || _doc.createElement(\"div\") : value);\n\t\t};\n\t},\n\tshuffle = a => a.sort(() => .5 - Math.random()), // alternative that's a bit faster and more reliably diverse but bigger: for (let j, v, i = a.length; i; j = (Math.random() * i) | 0, v = a[--i], a[i] = a[j], a[j] = v); return a;\n\t//for distributing values across an array. Can accept a number, a function or (most commonly) a function which can contain the following properties: {base, amount, from, ease, grid, axis, length, each}. Returns a function that expects the following parameters: index, target, array. Recognizes the following\n\tdistribute = v => {\n\t\tif (_isFunction(v)) {\n\t\t\treturn v;\n\t\t}\n\t\tlet vars = _isObject(v) ? v : {each:v}, //n:1 is just to indicate v was a number; we leverage that later to set v according to the length we get. If a number is passed in, we treat it like the old stagger value where 0.1, for example, would mean that things would be distributed with 0.1 between each element in the array rather than a total \"amount\" that's chunked out among them all.\n\t\t\tease = _parseEase(vars.ease),\n\t\t\tfrom = vars.from || 0,\n\t\t\tbase = parseFloat(vars.base) || 0,\n\t\t\tcache = {},\n\t\t\tisDecimal = (from > 0 && from < 1),\n\t\t\tratios = isNaN(from) || isDecimal,\n\t\t\taxis = vars.axis,\n\t\t\tratioX = from,\n\t\t\tratioY = from;\n\t\tif (_isString(from)) {\n\t\t\tratioX = ratioY = {center:.5, edges:.5, end:1}[from] || 0;\n\t\t} else if (!isDecimal && ratios) {\n\t\t\tratioX = from[0];\n\t\t\tratioY = from[1];\n\t\t}\n\t\treturn (i, target, a) => {\n\t\t\tlet l = (a || vars).length,\n\t\t\t\tdistances = cache[l],\n\t\t\t\toriginX, originY, x, y, d, j, max, min, wrapAt;\n\t\t\tif (!distances) {\n\t\t\t\twrapAt = (vars.grid === \"auto\") ? 0 : (vars.grid || [1, _bigNum])[1];\n\t\t\t\tif (!wrapAt) {\n\t\t\t\t\tmax = -_bigNum;\n\t\t\t\t\twhile (max < (max = a[wrapAt++].getBoundingClientRect().left) && wrapAt < l) { }\n\t\t\t\t\twrapAt < l && wrapAt--;\n\t\t\t\t}\n\t\t\t\tdistances = cache[l] = [];\n\t\t\t\toriginX = ratios ? (Math.min(wrapAt, l) * ratioX) - .5 : from % wrapAt;\n\t\t\t\toriginY = wrapAt === _bigNum ? 0 : ratios ? l * ratioY / wrapAt - .5 : (from / wrapAt) | 0;\n\t\t\t\tmax = 0;\n\t\t\t\tmin = _bigNum;\n\t\t\t\tfor (j = 0; j < l; j++) {\n\t\t\t\t\tx = (j % wrapAt) - originX;\n\t\t\t\t\ty = originY - ((j / wrapAt) | 0);\n\t\t\t\t\tdistances[j] = d = !axis ? _sqrt(x * x + y * y) : Math.abs((axis === \"y\") ? y : x);\n\t\t\t\t\t(d > max) && (max = d);\n\t\t\t\t\t(d < min) && (min = d);\n\t\t\t\t}\n\t\t\t\t(from === \"random\") && shuffle(distances);\n\t\t\t\tdistances.max = max - min;\n\t\t\t\tdistances.min = min;\n\t\t\t\tdistances.v = l = (parseFloat(vars.amount) || (parseFloat(vars.each) * (wrapAt > l ? l - 1 : !axis ? Math.max(wrapAt, l / wrapAt) : axis === \"y\" ? l / wrapAt : wrapAt)) || 0) * (from === \"edges\" ? -1 : 1);\n\t\t\t\tdistances.b = (l < 0) ? base - l : base;\n\t\t\t\tdistances.u = getUnit(vars.amount || vars.each) || 0; //unit\n\t\t\t\tease = (ease && l < 0) ? _invertEase(ease) : ease;\n\t\t\t}\n\t\t\tl = ((distances[i] - distances.min) / distances.max) || 0;\n\t\t\treturn _roundPrecise(distances.b + (ease ? ease(l) : l) * distances.v) + distances.u; //round in order to work around floating point errors\n\t\t};\n\t},\n\t_roundModifier = v => { //pass in 0.1 get a function that'll round to the nearest tenth, or 5 to round to the closest 5, or 0.001 to the closest 1000th, etc.\n\t\tlet p = Math.pow(10, ((v + \"\").split(\".\")[1] || \"\").length); //to avoid floating point math errors (like 24 * 0.1 == 2.4000000000000004), we chop off at a specific number of decimal places (much faster than toFixed())\n\t\treturn raw => {\n\t\t\tlet n = _roundPrecise(Math.round(parseFloat(raw) / v) * v * p);\n\t\t\treturn (n - n % 1) / p + (_isNumber(raw) ? 0 : getUnit(raw)); // n - n % 1 replaces Math.floor() in order to handle negative values properly. For example, Math.floor(-150.00000000000003) is 151!\n\t\t};\n\t},\n\tsnap = (snapTo, value) => {\n\t\tlet isArray = _isArray(snapTo),\n\t\t\tradius, is2D;\n\t\tif (!isArray && _isObject(snapTo)) {\n\t\t\tradius = isArray = snapTo.radius || _bigNum;\n\t\t\tif (snapTo.values) {\n\t\t\t\tsnapTo = toArray(snapTo.values);\n\t\t\t\tif ((is2D = !_isNumber(snapTo[0]))) {\n\t\t\t\t\tradius *= radius; //performance optimization so we don't have to Math.sqrt() in the loop.\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsnapTo = _roundModifier(snapTo.increment);\n\t\t\t}\n\t\t}\n\t\treturn _conditionalReturn(value, !isArray ? _roundModifier(snapTo) : _isFunction(snapTo) ? raw => {is2D = snapTo(raw); return Math.abs(is2D - raw) <= radius ? is2D : raw; } : raw => {\n\t\t\tlet x = parseFloat(is2D ? raw.x : raw),\n\t\t\t\ty = parseFloat(is2D ? raw.y : 0),\n\t\t\t\tmin = _bigNum,\n\t\t\t\tclosest = 0,\n\t\t\t\ti = snapTo.length,\n\t\t\t\tdx, dy;\n\t\t\twhile (i--) {\n\t\t\t\tif (is2D) {\n\t\t\t\t\tdx = snapTo[i].x - x;\n\t\t\t\t\tdy = snapTo[i].y - y;\n\t\t\t\t\tdx = dx * dx + dy * dy;\n\t\t\t\t} else {\n\t\t\t\t\tdx = Math.abs(snapTo[i] - x);\n\t\t\t\t}\n\t\t\t\tif (dx < min) {\n\t\t\t\t\tmin = dx;\n\t\t\t\t\tclosest = i;\n\t\t\t\t}\n\t\t\t}\n\t\t\tclosest = (!radius || min <= radius) ? snapTo[closest] : raw;\n\t\t\treturn (is2D || closest === raw || _isNumber(raw)) ? closest : closest + getUnit(raw);\n\t\t});\n\t},\n\trandom = (min, max, roundingIncrement, returnFunction) => _conditionalReturn(_isArray(min) ? !max : roundingIncrement === true ? !!(roundingIncrement = 0) : !returnFunction, () => _isArray(min) ? min[~~(Math.random() * min.length)] : (roundingIncrement = roundingIncrement || 1e-5) && (returnFunction = roundingIncrement < 1 ? 10 ** ((roundingIncrement + \"\").length - 2) : 1) && (Math.floor(Math.round((min - roundingIncrement / 2 + Math.random() * (max - min + roundingIncrement * .99)) / roundingIncrement) * roundingIncrement * returnFunction) / returnFunction)),\n\tpipe = (...functions) => value => functions.reduce((v, f) => f(v), value),\n\tunitize = (func, unit) => value => func(parseFloat(value)) + (unit || getUnit(value)),\n\tnormalize = (min, max, value) => mapRange(min, max, 0, 1, value),\n\t_wrapArray = (a, wrapper, value) => _conditionalReturn(value, index => a[~~wrapper(index)]),\n\twrap = function(min, max, value) { // NOTE: wrap() CANNOT be an arrow function! A very odd compiling bug causes problems (unrelated to GSAP).\n\t\tlet range = max - min;\n\t\treturn _isArray(min) ? _wrapArray(min, wrap(0, min.length), max) : _conditionalReturn(value, value => ((range + (value - min) % range) % range) + min);\n\t},\n\twrapYoyo = (min, max, value) => {\n\t\tlet range = max - min,\n\t\t\ttotal = range * 2;\n\t\treturn _isArray(min) ? _wrapArray(min, wrapYoyo(0, min.length - 1), max) : _conditionalReturn(value, value => {\n\t\t\tvalue = (total + (value - min) % total) % total || 0;\n\t\t\treturn min + ((value > range) ? (total - value) : value);\n\t\t});\n\t},\n\t_replaceRandom = value => { //replaces all occurrences of random(...) in a string with the calculated random value. can be a range like random(-100, 100, 5) or an array like random([0, 100, 500])\n\t\tlet prev = 0,\n\t\t\ts = \"\",\n\t\t\ti, nums, end, isArray;\n\t\twhile (~(i = value.indexOf(\"random(\", prev))) {\n\t\t\tend = value.indexOf(\")\", i);\n\t\t\tisArray = value.charAt(i + 7) === \"[\";\n\t\t\tnums = value.substr(i + 7, end - i - 7).match(isArray ? _delimitedValueExp : _strictNumExp);\n\t\t\ts += value.substr(prev, i - prev) + random(isArray ? nums : +nums[0], isArray ? 0 : +nums[1], +nums[2] || 1e-5);\n\t\t\tprev = end + 1;\n\t\t}\n\t\treturn s + value.substr(prev, value.length - prev);\n\t},\n\tmapRange = (inMin, inMax, outMin, outMax, value) => {\n\t\tlet inRange = inMax - inMin,\n\t\t\toutRange = outMax - outMin;\n\t\treturn _conditionalReturn(value, value => outMin + ((((value - inMin) / inRange) * outRange) || 0));\n\t},\n\tinterpolate = (start, end, progress, mutate) => {\n\t\tlet func = isNaN(start + end) ? 0 : p => (1 - p) * start + p * end;\n\t\tif (!func) {\n\t\t\tlet isString = _isString(start),\n\t\t\t\tmaster = {},\n\t\t\t\tp, i, interpolators, l, il;\n\t\t\tprogress === true && (mutate = 1) && (progress = null);\n\t\t\tif (isString) {\n\t\t\t\tstart = {p: start};\n\t\t\t\tend = {p: end};\n\n\t\t\t} else if (_isArray(start) && !_isArray(end)) {\n\t\t\t\tinterpolators = [];\n\t\t\t\tl = start.length;\n\t\t\t\til = l - 2;\n\t\t\t\tfor (i = 1; i < l; i++) {\n\t\t\t\t\tinterpolators.push(interpolate(start[i-1], start[i])); //build the interpolators up front as a performance optimization so that when the function is called many times, it can just reuse them.\n\t\t\t\t}\n\t\t\t\tl--;\n\t\t\t\tfunc = p => {\n\t\t\t\t\tp *= l;\n\t\t\t\t\tlet i = Math.min(il, ~~p);\n\t\t\t\t\treturn interpolators[i](p - i);\n\t\t\t\t};\n\t\t\t\tprogress = end;\n\t\t\t} else if (!mutate) {\n\t\t\t\tstart = _merge(_isArray(start) ? [] : {}, start);\n\t\t\t}\n\t\t\tif (!interpolators) {\n\t\t\t\tfor (p in end) {\n\t\t\t\t\t_addPropTween.call(master, start, p, \"get\", end[p]);\n\t\t\t\t}\n\t\t\t\tfunc = p => _renderPropTweens(p, master) || (isString ? start.p : start);\n\t\t\t}\n\t\t}\n\t\treturn _conditionalReturn(progress, func);\n\t},\n\t_getLabelInDirection = (timeline, fromTime, backward) => { //used for nextLabel() and previousLabel()\n\t\tlet labels = timeline.labels,\n\t\t\tmin = _bigNum,\n\t\t\tp, distance, label;\n\t\tfor (p in labels) {\n\t\t\tdistance = labels[p] - fromTime;\n\t\t\tif ((distance < 0) === !!backward && distance && min > (distance = Math.abs(distance))) {\n\t\t\t\tlabel = p;\n\t\t\t\tmin = distance;\n\t\t\t}\n\t\t}\n\t\treturn label;\n\t},\n\t_callback = (animation, type, executeLazyFirst) => {\n\t\tlet v = animation.vars,\n\t\t\tcallback = v[type],\n\t\t\tprevContext = _context,\n\t\t\tcontext = animation._ctx,\n\t\t\tparams, scope, result;\n\t\tif (!callback) {\n\t\t\treturn;\n\t\t}\n\t\tparams = v[type + \"Params\"];\n\t\tscope = v.callbackScope || animation;\n\t\texecuteLazyFirst && _lazyTweens.length && _lazyRender(); //in case rendering caused any tweens to lazy-init, we should render them because typically when a timeline finishes, users expect things to have rendered fully. Imagine an onUpdate on a timeline that reports/checks tweened values.\n\t\tcontext && (_context = context);\n\t\tresult = params ? callback.apply(scope, params) : callback.call(scope);\n\t\t_context = prevContext;\n\t\treturn result;\n\t},\n\t_interrupt = animation => {\n\t\t_removeFromParent(animation);\n\t\tanimation.scrollTrigger && animation.scrollTrigger.kill(!!_reverting);\n\t\tanimation.progress() < 1 && _callback(animation, \"onInterrupt\");\n\t\treturn animation;\n\t},\n\t_quickTween,\n\t_registerPluginQueue = [],\n\t_createPlugin = config => {\n\t\tif (!config) return;\n\t\tconfig = (!config.name && config.default) || config; // UMD packaging wraps things oddly, so for example MotionPathHelper becomes {MotionPathHelper:MotionPathHelper, default:MotionPathHelper}.\n\t\tif (_windowExists() || config.headless) { // edge case: some build tools may pass in a null/undefined value\n\t\t\tlet name = config.name,\n\t\t\t\tisFunc = _isFunction(config),\n\t\t\t\tPlugin = (name && !isFunc && config.init) ? function () {\n\t\t\t\t\tthis._props = [];\n\t\t\t\t} : config, //in case someone passes in an object that's not a plugin, like CustomEase\n\t\t\t\tinstanceDefaults = {init: _emptyFunc, render: _renderPropTweens, add: _addPropTween, kill: _killPropTweensOf, modifier: _addPluginModifier, rawVars: 0},\n\t\t\t\tstatics = {targetTest: 0, get: 0, getSetter: _getSetter, aliases: {}, register: 0};\n\t\t\t_wake();\n\t\t\tif (config !== Plugin) {\n\t\t\t\tif (_plugins[name]) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\t_setDefaults(Plugin, _setDefaults(_copyExcluding(config, instanceDefaults), statics)); //static methods\n\t\t\t\t_merge(Plugin.prototype, _merge(instanceDefaults, _copyExcluding(config, statics))); //instance methods\n\t\t\t\t_plugins[(Plugin.prop = name)] = Plugin;\n\t\t\t\tif (config.targetTest) {\n\t\t\t\t\t_harnessPlugins.push(Plugin);\n\t\t\t\t\t_reservedProps[name] = 1;\n\t\t\t\t}\n\t\t\t\tname = (name === \"css\" ? \"CSS\" : name.charAt(0).toUpperCase() + name.substr(1)) + \"Plugin\"; //for the global name. \"motionPath\" should become MotionPathPlugin\n\t\t\t}\n\t\t\t_addGlobal(name, Plugin);\n\t\t\tconfig.register && config.register(gsap, Plugin, PropTween);\n\t\t} else {\n\t\t\t_registerPluginQueue.push(config);\n\t\t}\n\t},\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/*\n * --------------------------------------------------------------------------------------\n * COLORS\n * --------------------------------------------------------------------------------------\n */\n\n\t_255 = 255,\n\t_colorLookup = {\n\t\taqua:[0,_255,_255],\n\t\tlime:[0,_255,0],\n\t\tsilver:[192,192,192],\n\t\tblack:[0,0,0],\n\t\tmaroon:[128,0,0],\n\t\tteal:[0,128,128],\n\t\tblue:[0,0,_255],\n\t\tnavy:[0,0,128],\n\t\twhite:[_255,_255,_255],\n\t\tolive:[128,128,0],\n\t\tyellow:[_255,_255,0],\n\t\torange:[_255,165,0],\n\t\tgray:[128,128,128],\n\t\tpurple:[128,0,128],\n\t\tgreen:[0,128,0],\n\t\tred:[_255,0,0],\n\t\tpink:[_255,192,203],\n\t\tcyan:[0,_255,_255],\n\t\ttransparent:[_255,_255,_255,0]\n\t},\n\t// possible future idea to replace the hard-coded color name values - put this in the ticker.wake() where we set the _doc:\n\t// let ctx = _doc.createElement(\"canvas\").getContext(\"2d\");\n\t// _forEachName(\"aqua,lime,silver,black,maroon,teal,blue,navy,white,olive,yellow,orange,gray,purple,green,red,pink,cyan\", color => {ctx.fillStyle = color; _colorLookup[color] = splitColor(ctx.fillStyle)});\n\t_hue = (h, m1, m2) => {\n\t\th += h < 0 ? 1 : h > 1 ? -1 : 0;\n\t\treturn ((((h * 6 < 1) ? m1 + (m2 - m1) * h * 6 : h < .5 ? m2 : (h * 3 < 2) ? m1 + (m2 - m1) * (2 / 3 - h) * 6 : m1) * _255) + .5) | 0;\n\t},\n\tsplitColor = (v, toHSL, forceAlpha) => {\n\t\tlet a = !v ? _colorLookup.black : _isNumber(v) ? [v >> 16, (v >> 8) & _255, v & _255] : 0,\n\t\t\tr, g, b, h, s, l, max, min, d, wasHSL;\n\t\tif (!a) {\n\t\t\tif (v.substr(-1) === \",\") { //sometimes a trailing comma is included and we should chop it off (typically from a comma-delimited list of values like a textShadow:\"2px 2px 2px blue, 5px 5px 5px rgb(255,0,0)\" - in this example \"blue,\" has a trailing comma. We could strip it out inside parseComplex() but we'd need to do it to the beginning and ending values plus it wouldn't provide protection from other potential scenarios like if the user passes in a similar value.\n\t\t\t\tv = v.substr(0, v.length - 1);\n\t\t\t}\n\t\t\tif (_colorLookup[v]) {\n\t\t\t\ta = _colorLookup[v];\n\t\t\t} else if (v.charAt(0) === \"#\") {\n\t\t\t\tif (v.length < 6) { //for shorthand like #9F0 or #9F0F (could have alpha)\n\t\t\t\t\tr = v.charAt(1);\n\t\t\t\t\tg = v.charAt(2);\n\t\t\t\t\tb = v.charAt(3);\n\t\t\t\t\tv = \"#\" + r + r + g + g + b + b + (v.length === 5 ? v.charAt(4) + v.charAt(4) : \"\");\n\t\t\t\t}\n\t\t\t\tif (v.length === 9) { // hex with alpha, like #fd5e53ff\n\t\t\t\t\ta = parseInt(v.substr(1, 6), 16);\n\t\t\t\t\treturn [a >> 16, (a >> 8) & _255, a & _255, parseInt(v.substr(7), 16) / 255];\n\t\t\t\t}\n\t\t\t\tv = parseInt(v.substr(1), 16);\n\t\t\t\ta = [v >> 16, (v >> 8) & _255, v & _255];\n\t\t\t} else if (v.substr(0, 3) === \"hsl\") {\n\t\t\t\ta = wasHSL = v.match(_strictNumExp);\n\t\t\t\tif (!toHSL) {\n\t\t\t\t\th = (+a[0] % 360) / 360;\n\t\t\t\t\ts = +a[1] / 100;\n\t\t\t\t\tl = +a[2] / 100;\n\t\t\t\t\tg = (l <= .5) ? l * (s + 1) : l + s - l * s;\n\t\t\t\t\tr = l * 2 - g;\n\t\t\t\t\ta.length > 3 && (a[3] *= 1); //cast as number\n\t\t\t\t\ta[0] = _hue(h + 1 / 3, r, g);\n\t\t\t\t\ta[1] = _hue(h, r, g);\n\t\t\t\t\ta[2] = _hue(h - 1 / 3, r, g);\n\t\t\t\t} else if (~v.indexOf(\"=\")) { //if relative values are found, just return the raw strings with the relative prefixes in place.\n\t\t\t\t\ta = v.match(_numExp);\n\t\t\t\t\tforceAlpha && a.length < 4 && (a[3] = 1);\n\t\t\t\t\treturn a;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ta = v.match(_strictNumExp) || _colorLookup.transparent;\n\t\t\t}\n\t\t\ta = a.map(Number);\n\t\t}\n\t\tif (toHSL && !wasHSL) {\n\t\t\tr = a[0] / _255;\n\t\t\tg = a[1] / _255;\n\t\t\tb = a[2] / _255;\n\t\t\tmax = Math.max(r, g, b);\n\t\t\tmin = Math.min(r, g, b);\n\t\t\tl = (max + min) / 2;\n\t\t\tif (max === min) {\n\t\t\t\th = s = 0;\n\t\t\t} else {\n\t\t\t\td = max - min;\n\t\t\t\ts = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n\t\t\t\th = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;\n\t\t\t\th *= 60;\n\t\t\t}\n\t\t\ta[0] = ~~(h + .5);\n\t\t\ta[1] = ~~(s * 100 + .5);\n\t\t\ta[2] = ~~(l * 100 + .5);\n\t\t}\n\t\tforceAlpha && a.length < 4 && (a[3] = 1);\n\t\treturn a;\n\t},\n\t_colorOrderData = v => { // strips out the colors from the string, finds all the numeric slots (with units) and returns an array of those. The Array also has a \"c\" property which is an Array of the index values where the colors belong. This is to help work around issues where there's a mis-matched order of color/numeric data like drop-shadow(#f00 0px 1px 2px) and drop-shadow(0x 1px 2px #f00). This is basically a helper function used in _formatColors()\n\t\tlet values = [],\n\t\t\tc = [],\n\t\t\ti = -1;\n\t\tv.split(_colorExp).forEach(v => {\n\t\t\tlet a = v.match(_numWithUnitExp) || [];\n\t\t\tvalues.push(...a);\n\t\t\tc.push(i += a.length + 1);\n\t\t});\n\t\tvalues.c = c;\n\t\treturn values;\n\t},\n\t_formatColors = (s, toHSL, orderMatchData) => {\n\t\tlet result = \"\",\n\t\t\tcolors = (s + result).match(_colorExp),\n\t\t\ttype = toHSL ? \"hsla(\" : \"rgba(\",\n\t\t\ti = 0,\n\t\t\tc, shell, d, l;\n\t\tif (!colors) {\n\t\t\treturn s;\n\t\t}\n\t\tcolors = colors.map(color => (color = splitColor(color, toHSL, 1)) && type + (toHSL ? color[0] + \",\" + color[1] + \"%,\" + color[2] + \"%,\" + color[3] : color.join(\",\")) + \")\");\n\t\tif (orderMatchData) {\n\t\t\td = _colorOrderData(s);\n\t\t\tc = orderMatchData.c;\n\t\t\tif (c.join(result) !== d.c.join(result)) {\n\t\t\t\tshell = s.replace(_colorExp, \"1\").split(_numWithUnitExp);\n\t\t\t\tl = shell.length - 1;\n\t\t\t\tfor (; i < l; i++) {\n\t\t\t\t\tresult += shell[i] + (~c.indexOf(i) ? colors.shift() || type + \"0,0,0,0)\" : (d.length ? d : colors.length ? colors : orderMatchData).shift());\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!shell) {\n\t\t\tshell = s.split(_colorExp);\n\t\t\tl = shell.length - 1;\n\t\t\tfor (; i < l; i++) {\n\t\t\t\tresult += shell[i] + colors[i];\n\t\t\t}\n\t\t}\n\t\treturn result + shell[l];\n\t},\n\t_colorExp = (function() {\n\t\tlet s = \"(?:\\\\b(?:(?:rgb|rgba|hsl|hsla)\\\\(.+?\\\\))|\\\\B#(?:[0-9a-f]{3,4}){1,2}\\\\b\", //we'll dynamically build this Regular Expression to conserve file size. After building it, it will be able to find rgb(), rgba(), # (hexadecimal), and named color values like red, blue, purple, etc.,\n\t\t\tp;\n\t\tfor (p in _colorLookup) {\n\t\t\ts += \"|\" + p + \"\\\\b\";\n\t\t}\n\t\treturn new RegExp(s + \")\", \"gi\");\n\t})(),\n\t_hslExp = /hsl[a]?\\(/,\n\t_colorStringFilter = a => {\n\t\tlet combined = a.join(\" \"),\n\t\t\ttoHSL;\n\t\t_colorExp.lastIndex = 0;\n\t\tif (_colorExp.test(combined)) {\n\t\t\ttoHSL = _hslExp.test(combined);\n\t\t\ta[1] = _formatColors(a[1], toHSL);\n\t\t\ta[0] = _formatColors(a[0], toHSL, _colorOrderData(a[1])); // make sure the order of numbers/colors match with the END value.\n\t\t\treturn true;\n\t\t}\n\t},\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/*\n * --------------------------------------------------------------------------------------\n * TICKER\n * --------------------------------------------------------------------------------------\n */\n\t_tickerActive,\n\t_ticker = (function() {\n\t\tlet _getTime = Date.now,\n\t\t\t_lagThreshold = 500,\n\t\t\t_adjustedLag = 33,\n\t\t\t_startTime = _getTime(),\n\t\t\t_lastUpdate = _startTime,\n\t\t\t_gap = 1000 / 240,\n\t\t\t_nextTime = _gap,\n\t\t\t_listeners = [],\n\t\t\t_id, _req, _raf, _self, _delta, _i,\n\t\t\t_tick = v => {\n\t\t\t\tlet elapsed = _getTime() - _lastUpdate,\n\t\t\t\t\tmanual = v === true,\n\t\t\t\t\toverlap, dispatch, time, frame;\n\t\t\t\t(elapsed > _lagThreshold || elapsed < 0) && (_startTime += elapsed - _adjustedLag);\n\t\t\t\t_lastUpdate += elapsed;\n\t\t\t\ttime = _lastUpdate - _startTime;\n\t\t\t\toverlap = time - _nextTime;\n\t\t\t\tif (overlap > 0 || manual) {\n\t\t\t\t\tframe = ++_self.frame;\n\t\t\t\t\t_delta = time - _self.time * 1000;\n\t\t\t\t\t_self.time = time = time / 1000;\n\t\t\t\t\t_nextTime += overlap + (overlap >= _gap ? 4 : _gap - overlap);\n\t\t\t\t\tdispatch = 1;\n\t\t\t\t}\n\t\t\t\tmanual || (_id = _req(_tick)); //make sure the request is made before we dispatch the \"tick\" event so that timing is maintained. Otherwise, if processing the \"tick\" requires a bunch of time (like 15ms) and we're using a setTimeout() that's based on 16.7ms, it'd technically take 31.7ms between frames otherwise.\n\t\t\t\tif (dispatch) {\n\t\t\t\t\tfor (_i = 0; _i < _listeners.length; _i++) { // use _i and check _listeners.length instead of a variable because a listener could get removed during the loop, and if that happens to an element less than the current index, it'd throw things off in the loop.\n\t\t\t\t\t\t_listeners[_i](time, _delta, frame, v);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t_self = {\n\t\t\ttime:0,\n\t\t\tframe:0,\n\t\t\ttick() {\n\t\t\t\t_tick(true);\n\t\t\t},\n\t\t\tdeltaRatio(fps) {\n\t\t\t\treturn _delta / (1000 / (fps || 60));\n\t\t\t},\n\t\t\twake() {\n\t\t\t\tif (_coreReady) {\n\t\t\t\t\tif (!_coreInitted && _windowExists()) {\n\t\t\t\t\t\t_win = _coreInitted = window;\n\t\t\t\t\t\t_doc = _win.document || {};\n\t\t\t\t\t\t_globals.gsap = gsap;\n\t\t\t\t\t\t(_win.gsapVersions || (_win.gsapVersions = [])).push(gsap.version);\n\t\t\t\t\t\t_install(_installScope || _win.GreenSockGlobals || (!_win.gsap && _win) || {});\n\t\t\t\t\t\t_registerPluginQueue.forEach(_createPlugin);\n\t\t\t\t\t}\n\t\t\t\t\t_raf = typeof(requestAnimationFrame) !== \"undefined\" && requestAnimationFrame;\n\t\t\t\t\t_id && _self.sleep();\n\t\t\t\t\t_req = _raf || (f => setTimeout(f, (_nextTime - _self.time * 1000 + 1) | 0));\n\t\t\t\t\t_tickerActive = 1;\n\t\t\t\t\t_tick(2);\n\t\t\t\t}\n\t\t\t},\n\t\t\tsleep() {\n\t\t\t\t(_raf ? cancelAnimationFrame : clearTimeout)(_id);\n\t\t\t\t_tickerActive = 0;\n\t\t\t\t_req = _emptyFunc;\n\t\t\t},\n\t\t\tlagSmoothing(threshold, adjustedLag) {\n\t\t\t\t_lagThreshold = threshold || Infinity; // zero should be interpreted as basically unlimited\n\t\t\t\t_adjustedLag = Math.min(adjustedLag || 33, _lagThreshold);\n\t\t\t},\n\t\t\tfps(fps) {\n\t\t\t\t_gap = 1000 / (fps || 240);\n\t\t\t\t_nextTime = _self.time * 1000 + _gap;\n\t\t\t},\n\t\t\tadd(callback, once, prioritize) {\n\t\t\t\tlet func = once ? (t, d, f, v) => {callback(t, d, f, v); _self.remove(func);} : callback;\n\t\t\t\t_self.remove(callback);\n\t\t\t\t_listeners[prioritize ? \"unshift\" : \"push\"](func);\n\t\t\t\t_wake();\n\t\t\t\treturn func;\n\t\t\t},\n\t\t\tremove(callback, i) {\n\t\t\t\t~(i = _listeners.indexOf(callback)) && _listeners.splice(i, 1) && _i >= i && _i--;\n\t\t\t},\n\t\t\t_listeners:_listeners\n\t\t};\n\t\treturn _self;\n\t})(),\n\t_wake = () => !_tickerActive && _ticker.wake(), //also ensures the core classes are initialized.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/*\n* -------------------------------------------------\n* EASING\n* -------------------------------------------------\n*/\n\t_easeMap = {},\n\t_customEaseExp = /^[\\d.\\-M][\\d.\\-,\\s]/,\n\t_quotesExp = /[\"']/g,\n\t_parseObjectInString = value => { //takes a string like \"{wiggles:10, type:anticipate})\" and turns it into a real object. Notice it ends in \")\" and includes the {} wrappers. This is because we only use this function for parsing ease configs and prioritized optimization rather than reusability.\n\t\tlet obj = {},\n\t\t\tsplit = value.substr(1, value.length-3).split(\":\"),\n\t\t\tkey = split[0],\n\t\t\ti = 1,\n\t\t\tl = split.length,\n\t\t\tindex, val, parsedVal;\n\t\tfor (; i < l; i++) {\n\t\t\tval = split[i];\n\t\t\tindex = i !== l-1 ? val.lastIndexOf(\",\") : val.length;\n\t\t\tparsedVal = val.substr(0, index);\n\t\t\tobj[key] = isNaN(parsedVal) ? parsedVal.replace(_quotesExp, \"\").trim() : +parsedVal;\n\t\t\tkey = val.substr(index+1).trim();\n\t\t}\n\t\treturn obj;\n\t},\n\t_valueInParentheses = value => {\n\t\tlet open = value.indexOf(\"(\") + 1,\n\t\t\tclose = value.indexOf(\")\"),\n\t\t\tnested = value.indexOf(\"(\", open);\n\t\treturn value.substring(open, ~nested && nested < close ? value.indexOf(\")\", close + 1) : close);\n\t},\n\t_configEaseFromString = name => { //name can be a string like \"elastic.out(1,0.5)\", and pass in _easeMap as obj and it'll parse it out and call the actual function like _easeMap.Elastic.easeOut.config(1,0.5). It will also parse custom ease strings as long as CustomEase is loaded and registered (internally as _easeMap._CE).\n\t\tlet split = (name + \"\").split(\"(\"),\n\t\t\tease = _easeMap[split[0]];\n\t\treturn (ease && split.length > 1 && ease.config) ? ease.config.apply(null, ~name.indexOf(\"{\") ? [_parseObjectInString(split[1])] : _valueInParentheses(name).split(\",\").map(_numericIfPossible)) : (_easeMap._CE && _customEaseExp.test(name)) ? _easeMap._CE(\"\", name) : ease;\n\t},\n\t_invertEase = ease => p => 1 - ease(1 - p),\n\t// allow yoyoEase to be set in children and have those affected when the parent/ancestor timeline yoyos.\n\t_propagateYoyoEase = (timeline, isYoyo) => {\n\t\tlet child = timeline._first, ease;\n\t\twhile (child) {\n\t\t\tif (child instanceof Timeline) {\n\t\t\t\t_propagateYoyoEase(child, isYoyo);\n\t\t\t} else if (child.vars.yoyoEase && (!child._yoyo || !child._repeat) && child._yoyo !== isYoyo) {\n\t\t\t\tif (child.timeline) {\n\t\t\t\t\t_propagateYoyoEase(child.timeline, isYoyo);\n\t\t\t\t} else {\n\t\t\t\t\tease = child._ease;\n\t\t\t\t\tchild._ease = child._yEase;\n\t\t\t\t\tchild._yEase = ease;\n\t\t\t\t\tchild._yoyo = isYoyo;\n\t\t\t\t}\n\t\t\t}\n\t\t\tchild = child._next;\n\t\t}\n\t},\n\t_parseEase = (ease, defaultEase) => !ease ? defaultEase : (_isFunction(ease) ? ease : _easeMap[ease] || _configEaseFromString(ease)) || defaultEase,\n\t_insertEase = (names, easeIn, easeOut = p => 1 - easeIn(1 - p), easeInOut = (p => p < .5 ? easeIn(p * 2) / 2 : 1 - easeIn((1 - p) * 2) / 2)) => {\n\t\tlet ease = {easeIn, easeOut, easeInOut},\n\t\t\tlowercaseName;\n\t\t_forEachName(names, name => {\n\t\t\t_easeMap[name] = _globals[name] = ease;\n\t\t\t_easeMap[(lowercaseName = name.toLowerCase())] = easeOut;\n\t\t\tfor (let p in ease) {\n\t\t\t\t_easeMap[lowercaseName + (p === \"easeIn\" ? \".in\" : p === \"easeOut\" ? \".out\" : \".inOut\")] = _easeMap[name + \".\" + p] = ease[p];\n\t\t\t}\n\t\t});\n\t\treturn ease;\n\t},\n\t_easeInOutFromOut = easeOut => (p => p < .5 ? (1 - easeOut(1 - (p * 2))) / 2 : .5 + easeOut((p - .5) * 2) / 2),\n\t_configElastic = (type, amplitude, period) => {\n\t\tlet p1 = (amplitude >= 1) ? amplitude : 1, //note: if amplitude is < 1, we simply adjust the period for a more natural feel. Otherwise the math doesn't work right and the curve starts at 1.\n\t\t\tp2 = (period || (type ? .3 : .45)) / (amplitude < 1 ? amplitude : 1),\n\t\t\tp3 = p2 / _2PI * (Math.asin(1 / p1) || 0),\n\t\t\teaseOut = p => p === 1 ? 1 : p1 * (2 ** (-10 * p)) * _sin((p - p3) * p2) + 1,\n\t\t\tease = (type === \"out\") ? easeOut : (type === \"in\") ? p => 1 - easeOut(1 - p) : _easeInOutFromOut(easeOut);\n\t\tp2 = _2PI / p2; //precalculate to optimize\n\t\tease.config = (amplitude, period) => _configElastic(type, amplitude, period);\n\t\treturn ease;\n\t},\n\t_configBack = (type, overshoot = 1.70158) => {\n\t\tlet easeOut = p => p ? ((--p) * p * ((overshoot + 1) * p + overshoot) + 1) : 0,\n\t\t\tease = type === \"out\" ? easeOut : type === \"in\" ? p => 1 - easeOut(1 - p) : _easeInOutFromOut(easeOut);\n\t\tease.config = overshoot => _configBack(type, overshoot);\n\t\treturn ease;\n\t};\n\t// a cheaper (kb and cpu) but more mild way to get a parameterized weighted ease by feeding in a value between -1 (easeIn) and 1 (easeOut) where 0 is linear.\n\t// _weightedEase = ratio => {\n\t// \tlet y = 0.5 + ratio / 2;\n\t// \treturn p => (2 * (1 - p) * p * y + p * p);\n\t// },\n\t// a stronger (but more expensive kb/cpu) parameterized weighted ease that lets you feed in a value between -1 (easeIn) and 1 (easeOut) where 0 is linear.\n\t// _weightedEaseStrong = ratio => {\n\t// \tratio = .5 + ratio / 2;\n\t// \tlet o = 1 / 3 * (ratio < .5 ? ratio : 1 - ratio),\n\t// \t\tb = ratio - o,\n\t// \t\tc = ratio + o;\n\t// \treturn p => p === 1 ? p : 3 * b * (1 - p) * (1 - p) * p + 3 * c * (1 - p) * p * p + p * p * p;\n\t// };\n\n_forEachName(\"Linear,Quad,Cubic,Quart,Quint,Strong\", (name, i) => {\n\tlet power = i < 5 ? i + 1 : i;\n\t_insertEase(name + \",Power\" + (power - 1), i ? p => p ** power : p => p, p => 1 - (1 - p) ** power, p => p < .5 ? (p * 2) ** power / 2 : 1 - ((1 - p) * 2) ** power / 2);\n});\n_easeMap.Linear.easeNone = _easeMap.none = _easeMap.Linear.easeIn;\n_insertEase(\"Elastic\", _configElastic(\"in\"), _configElastic(\"out\"), _configElastic());\n((n, c) => {\n\tlet n1 = 1 / c,\n\t\tn2 = 2 * n1,\n\t\tn3 = 2.5 * n1,\n\t\teaseOut = p => (p < n1) ? n * p * p : (p < n2) ? n * (p - 1.5 / c) ** 2 + .75 : (p < n3) ? n * (p -= 2.25 / c) * p + .9375 : n * (p - 2.625 / c) ** 2 + .984375;\n\t_insertEase(\"Bounce\", p => 1 - easeOut(1 - p), easeOut);\n})(7.5625, 2.75);\n_insertEase(\"Expo\", p => (2 ** (10 * (p - 1))) * p + p * p * p * p * p * p * (1-p)); // previously 2 ** (10 * (p - 1)) but that doesn't end up with the value quite at the right spot so we do a blended ease to ensure it lands where it should perfectly.\n_insertEase(\"Circ\", p => -(_sqrt(1 - (p * p)) - 1));\n_insertEase(\"Sine\", p => p === 1 ? 1 : -_cos(p * _HALF_PI) + 1);\n_insertEase(\"Back\", _configBack(\"in\"), _configBack(\"out\"), _configBack());\n_easeMap.SteppedEase = _easeMap.steps = _globals.SteppedEase = {\n\tconfig(steps = 1, immediateStart) {\n\t\tlet p1 = 1 / steps,\n\t\t\tp2 = steps + (immediateStart ? 0 : 1),\n\t\t\tp3 = immediateStart ? 1 : 0,\n\t\t\tmax = 1 - _tinyNum;\n\t\treturn p => (((p2 * _clamp(0, max, p)) | 0) + p3) * p1;\n\t}\n};\n_defaults.ease = _easeMap[\"quad.out\"];\n\n\n_forEachName(\"onComplete,onUpdate,onStart,onRepeat,onReverseComplete,onInterrupt\", name => _callbackNames += name + \",\" + name + \"Params,\");\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/*\n * --------------------------------------------------------------------------------------\n * CACHE\n * --------------------------------------------------------------------------------------\n */\nexport class GSCache {\n\n\tconstructor(target, harness) {\n\t\tthis.id = _gsID++;\n\t\ttarget._gsap = this;\n\t\tthis.target = target;\n\t\tthis.harness = harness;\n\t\tthis.get = harness ? harness.get : _getProperty;\n\t\tthis.set = harness ? harness.getSetter : _getSetter;\n\t}\n\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/*\n * --------------------------------------------------------------------------------------\n * ANIMATION\n * --------------------------------------------------------------------------------------\n */\n\nexport class Animation {\n\n\tconstructor(vars) {\n\t\tthis.vars = vars;\n\t\tthis._delay = +vars.delay || 0;\n\t\tif ((this._repeat = vars.repeat === Infinity ? -2 : vars.repeat || 0)) { // TODO: repeat: Infinity on a timeline's children must flag that timeline internally and affect its totalDuration, otherwise it'll stop in the negative direction when reaching the start.\n\t\t\tthis._rDelay = vars.repeatDelay || 0;\n\t\t\tthis._yoyo = !!vars.yoyo || !!vars.yoyoEase;\n\t\t}\n\t\tthis._ts = 1;\n\t\t_setDuration(this, +vars.duration, 1, 1);\n\t\tthis.data = vars.data;\n\t\tif (_context) {\n\t\t\tthis._ctx = _context;\n\t\t\t_context.data.push(this);\n\t\t}\n\t\t_tickerActive || _ticker.wake();\n\t}\n\n\tdelay(value) {\n\t\tif (value || value === 0) {\n\t\t\tthis.parent && this.parent.smoothChildTiming && (this.startTime(this._start + value - this._delay));\n\t\t\tthis._delay = value;\n\t\t\treturn this;\n\t\t}\n\t\treturn this._delay;\n\t}\n\n\tduration(value) {\n\t\treturn arguments.length ? this.totalDuration(this._repeat > 0 ? value + (value + this._rDelay) * this._repeat : value) : this.totalDuration() && this._dur;\n\t}\n\n\ttotalDuration(value) {\n\t\tif (!arguments.length) {\n\t\t\treturn this._tDur;\n\t\t}\n\t\tthis._dirty = 0;\n\t\treturn _setDuration(this, this._repeat < 0 ? value : (value - (this._repeat * this._rDelay)) / (this._repeat + 1));\n\t}\n\n\ttotalTime(totalTime, suppressEvents) {\n\t\t_wake();\n\t\tif (!arguments.length) {\n\t\t\treturn this._tTime;\n\t\t}\n\t\tlet parent = this._dp;\n\t\tif (parent && parent.smoothChildTiming && this._ts) {\n\t\t\t_alignPlayhead(this, totalTime);\n\t\t\t!parent._dp || parent.parent || _postAddChecks(parent, this); // edge case: if this is a child of a timeline that already completed, for example, we must re-activate the parent.\n\t\t\t//in case any of the ancestor timelines had completed but should now be enabled, we should reset their totalTime() which will also ensure that they're lined up properly and enabled. Skip for animations that are on the root (wasteful). Example: a TimelineLite.exportRoot() is performed when there's a paused tween on the root, the export will not complete until that tween is unpaused, but imagine a child gets restarted later, after all [unpaused] tweens have completed. The start of that child would get pushed out, but one of the ancestors may have completed.\n\t\t\twhile (parent && parent.parent) {\n\t\t\t\tif (parent.parent._time !== parent._start + (parent._ts >= 0 ? parent._tTime / parent._ts : (parent.totalDuration() - parent._tTime) / -parent._ts)) {\n\t\t\t\t\tparent.totalTime(parent._tTime, true);\n\t\t\t\t}\n\t\t\t\tparent = parent.parent;\n\t\t\t}\n\t\t\tif (!this.parent && this._dp.autoRemoveChildren && ((this._ts > 0 && totalTime < this._tDur) || (this._ts < 0 && totalTime > 0) || (!this._tDur && !totalTime) )) { //if the animation doesn't have a parent, put it back into its last parent (recorded as _dp for exactly cases like this). Limit to parents with autoRemoveChildren (like globalTimeline) so that if the user manually removes an animation from a timeline and then alters its playhead, it doesn't get added back in.\n\t\t\t\t_addToTimeline(this._dp, this, this._start - this._delay);\n\t\t\t}\n\t\t}\n if (this._tTime !== totalTime || (!this._dur && !suppressEvents) || (this._initted && Math.abs(this._zTime) === _tinyNum) || (!totalTime && !this._initted && (this.add || this._ptLookup))) { // check for _ptLookup on a Tween instance to ensure it has actually finished being instantiated, otherwise if this.reverse() gets called in the Animation constructor, it could trigger a render() here even though the _targets weren't populated, thus when _init() is called there won't be any PropTweens (it'll act like the tween is non-functional)\n \tthis._ts || (this._pTime = totalTime); // otherwise, if an animation is paused, then the playhead is moved back to zero, then resumed, it'd revert back to the original time at the pause\n\t //if (!this._lock) { // avoid endless recursion (not sure we need this yet or if it's worth the performance hit)\n\t\t // this._lock = 1;\n\t\t _lazySafeRender(this, totalTime, suppressEvents);\n\t\t // this._lock = 0;\n\t //}\n\t\t}\n\t\treturn this;\n\t}\n\n\ttime(value, suppressEvents) {\n\t\treturn arguments.length ? this.totalTime((Math.min(this.totalDuration(), value + _elapsedCycleDuration(this)) % (this._dur + this._rDelay)) || (value ? this._dur : 0), suppressEvents) : this._time; // note: if the modulus results in 0, the playhead could be exactly at the end or the beginning, and we always defer to the END with a non-zero value, otherwise if you set the time() to the very end (duration()), it would render at the START!\n\t}\n\n\ttotalProgress(value, suppressEvents) {\n\t\treturn arguments.length ? this.totalTime( this.totalDuration() * value, suppressEvents) : this.totalDuration() ? Math.min(1, this._tTime / this._tDur) : this.rawTime() >= 0 && this._initted ? 1 : 0;\n\t}\n\n\tprogress(value, suppressEvents) {\n\t\treturn arguments.length ? this.totalTime( this.duration() * (this._yoyo && !(this.iteration() & 1) ? 1 - value : value) + _elapsedCycleDuration(this), suppressEvents) : (this.duration() ? Math.min(1, this._time / this._dur) : this.rawTime() > 0 ? 1 : 0);\n\t}\n\n\titeration(value, suppressEvents) {\n\t\tlet cycleDuration = this.duration() + this._rDelay;\n\t\treturn arguments.length ? this.totalTime(this._time + (value - 1) * cycleDuration, suppressEvents) : this._repeat ? _animationCycle(this._tTime, cycleDuration) + 1 : 1;\n\t}\n\n\t// potential future addition:\n\t// isPlayingBackwards() {\n\t// \tlet animation = this,\n\t// \t\torientation = 1; // 1 = forward, -1 = backward\n\t// \twhile (animation) {\n\t// \t\torientation *= animation.reversed() || (animation.repeat() && !(animation.iteration() & 1)) ? -1 : 1;\n\t// \t\tanimation = animation.parent;\n\t// \t}\n\t// \treturn orientation < 0;\n\t// }\n\n\ttimeScale(value, suppressEvents) {\n\t\tif (!arguments.length) {\n\t\t\treturn this._rts === -_tinyNum ? 0 : this._rts; // recorded timeScale. Special case: if someone calls reverse() on an animation with timeScale of 0, we assign it -_tinyNum to remember it's reversed.\n\t\t}\n\t\tif (this._rts === value) {\n\t\t\treturn this;\n\t\t}\n\t\tlet tTime = this.parent && this._ts ? _parentToChildTotalTime(this.parent._time, this) : this._tTime; // make sure to do the parentToChildTotalTime() BEFORE setting the new _ts because the old one must be used in that calculation.\n\n\t\t// future addition? Up side: fast and minimal file size. Down side: only works on this animation; if a timeline is reversed, for example, its childrens' onReverse wouldn't get called.\n\t\t//(+value < 0 && this._rts >= 0) && _callback(this, \"onReverse\", true);\n\n\t\t// prioritize rendering where the parent's playhead lines up instead of this._tTime because there could be a tween that's animating another tween's timeScale in the same rendering loop (same parent), thus if the timeScale tween renders first, it would alter _start BEFORE _tTime was set on that tick (in the rendering loop), effectively freezing it until the timeScale tween finishes.\n\t\tthis._rts = +value || 0;\n\t\tthis._ts = (this._ps || value === -_tinyNum) ? 0 : this._rts; // _ts is the functional timeScale which would be 0 if the animation is paused.\n\t\tthis.totalTime(_clamp(-Math.abs(this._delay), this._tDur, tTime), suppressEvents !== false);\n\t\t_setEnd(this); // if parent.smoothChildTiming was false, the end time didn't get updated in the _alignPlayhead() method, so do it here.\n\t\treturn _recacheAncestors(this);\n\t}\n\n\tpaused(value) {\n\t\tif (!arguments.length) {\n\t\t\treturn this._ps;\n\t\t}\n\t\t// possible future addition - if an animation is removed from its parent and then .restart() or .play() or .resume() is called, perhaps we should force it back into the globalTimeline but be careful because what if it's already at its end? We don't want it to just persist forever and not get released for GC.\n\t\t// !this.parent && !value && this._tTime < this._tDur && this !== _globalTimeline && _globalTimeline.add(this);\n\t\tif (this._ps !== value) {\n\t\t\tthis._ps = value;\n\t\t\tif (value) {\n\t\t\t\tthis._pTime = this._tTime || Math.max(-this._delay, this.rawTime()); // if the pause occurs during the delay phase, make sure that's factored in when resuming.\n\t\t\t\tthis._ts = this._act = 0; // _ts is the functional timeScale, so a paused tween would effectively have a timeScale of 0. We record the \"real\" timeScale as _rts (recorded time scale)\n\t\t\t} else {\n\t\t\t\t_wake();\n\t\t\t\tthis._ts = this._rts;\n\t\t\t\t//only defer to _pTime (pauseTime) if tTime is zero. Remember, someone could pause() an animation, then scrub the playhead and resume(). If the parent doesn't have smoothChildTiming, we render at the rawTime() because the startTime won't get updated.\n\t\t\t\tthis.totalTime(this.parent && !this.parent.smoothChildTiming ? this.rawTime() : this._tTime || this._pTime, (this.progress() === 1) && Math.abs(this._zTime) !== _tinyNum && (this._tTime -= _tinyNum)); // edge case: animation.progress(1).pause().play() wouldn't render again because the playhead is already at the end, but the call to totalTime() below will add it back to its parent...and not remove it again (since removing only happens upon rendering at a new time). Offsetting the _tTime slightly is done simply to cause the final render in totalTime() that'll pop it off its timeline (if autoRemoveChildren is true, of course). Check to make sure _zTime isn't -_tinyNum to avoid an edge case where the playhead is pushed to the end but INSIDE a tween/callback, the timeline itself is paused thus halting rendering and leaving a few unrendered. When resuming, it wouldn't render those otherwise.\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\tstartTime(value) {\n\t\tif (arguments.length) {\n\t\t\tthis._start = value;\n\t\t\tlet parent = this.parent || this._dp;\n\t\t\tparent && (parent._sort || !this.parent) && _addToTimeline(parent, this, value - this._delay);\n\t\t\treturn this;\n\t\t}\n\t\treturn this._start;\n\t}\n\n\tendTime(includeRepeats) {\n\t\treturn this._start + (_isNotFalse(includeRepeats) ? this.totalDuration() : this.duration()) / Math.abs(this._ts || 1);\n\t}\n\n\trawTime(wrapRepeats) {\n\t\tlet parent = this.parent || this._dp; // _dp = detached parent\n\t\treturn !parent ? this._tTime : (wrapRepeats && (!this._ts || (this._repeat && this._time && this.totalProgress() < 1))) ? this._tTime % (this._dur + this._rDelay) : !this._ts ? this._tTime : _parentToChildTotalTime(parent.rawTime(wrapRepeats), this);\n\t}\n\n\trevert(config= _revertConfig) {\n\t\tlet prevIsReverting = _reverting;\n\t\t_reverting = config;\n\t\tif (this._initted || this._startAt) {\n\t\t\tthis.timeline && this.timeline.revert(config);\n\t\t\tthis.totalTime(-0.01, config.suppressEvents);\n\t\t}\n\t\tthis.data !== \"nested\" && config.kill !== false && this.kill();\n\t\t_reverting = prevIsReverting;\n\t\treturn this;\n\t}\n\n\tglobalTime(rawTime) {\n\t\tlet animation = this,\n\t\t\ttime = arguments.length ? rawTime : animation.rawTime();\n\t\twhile (animation) {\n\t\t\ttime = animation._start + time / (Math.abs(animation._ts) || 1);\n\t\t\tanimation = animation._dp;\n\t\t}\n\t\treturn !this.parent && this._sat ? this._sat.globalTime(rawTime) : time; // the _startAt tweens for .fromTo() and .from() that have immediateRender should always be FIRST in the timeline (important for context.revert()). \"_sat\" stands for _startAtTween, referring to the parent tween that created the _startAt. We must discern if that tween had immediateRender so that we can know whether or not to prioritize it in revert().\n\t}\n\n\trepeat(value) {\n\t\tif (arguments.length) {\n\t\t\tthis._repeat = value === Infinity ? -2 : value;\n\t\t\treturn _onUpdateTotalDuration(this);\n\t\t}\n\t\treturn this._repeat === -2 ? Infinity : this._repeat;\n\t}\n\n\trepeatDelay(value) {\n\t\tif (arguments.length) {\n\t\t\tlet time = this._time;\n\t\t\tthis._rDelay = value;\n\t\t\t_onUpdateTotalDuration(this);\n\t\t\treturn time ? this.time(time) : this;\n\t\t}\n\t\treturn this._rDelay;\n\t}\n\n\tyoyo(value) {\n\t\tif (arguments.length) {\n\t\t\tthis._yoyo = value;\n\t\t\treturn this;\n\t\t}\n\t\treturn this._yoyo;\n\t}\n\n\tseek(position, suppressEvents) {\n\t\treturn this.totalTime(_parsePosition(this, position), _isNotFalse(suppressEvents));\n\t}\n\n\trestart(includeDelay, suppressEvents) {\n\t\tthis.play().totalTime(includeDelay ? -this._delay : 0, _isNotFalse(suppressEvents));\n\t\tthis._dur || (this._zTime = -_tinyNum); // ensures onComplete fires on a zero-duration animation that gets restarted.\n\t\treturn this;\n\t}\n\n\tplay(from, suppressEvents) {\n\t\tfrom != null && this.seek(from, suppressEvents);\n\t\treturn this.reversed(false).paused(false);\n\t}\n\n\treverse(from, suppressEvents) {\n\t\tfrom != null && this.seek(from || this.totalDuration(), suppressEvents);\n\t\treturn this.reversed(true).paused(false);\n\t}\n\n\tpause(atTime, suppressEvents) {\n\t\tatTime != null && this.seek(atTime, suppressEvents);\n\t\treturn this.paused(true);\n\t}\n\n\tresume() {\n\t\treturn this.paused(false);\n\t}\n\n\treversed(value) {\n\t\tif (arguments.length) {\n\t\t\t!!value !== this.reversed() && this.timeScale(-this._rts || (value ? -_tinyNum : 0)); // in case timeScale is zero, reversing would have no effect so we use _tinyNum.\n\t\t\treturn this;\n\t\t}\n\t\treturn this._rts < 0;\n\t}\n\n\tinvalidate() {\n\t\tthis._initted = this._act = 0;\n\t\tthis._zTime = -_tinyNum;\n\t\treturn this;\n\t}\n\n\tisActive() {\n\t\tlet parent = this.parent || this._dp,\n\t\t\tstart = this._start,\n\t\t\trawTime;\n\t\treturn !!(!parent || (this._ts && this._initted && parent.isActive() && (rawTime = parent.rawTime(true)) >= start && rawTime < this.endTime(true) - _tinyNum));\n\t}\n\n\teventCallback(type, callback, params) {\n\t\tlet vars = this.vars;\n\t\tif (arguments.length > 1) {\n\t\t\tif (!callback) {\n\t\t\t\tdelete vars[type];\n\t\t\t} else {\n\t\t\t\tvars[type] = callback;\n\t\t\t\tparams && (vars[type + \"Params\"] = params);\n\t\t\t\ttype === \"onUpdate\" && (this._onUpdate = callback);\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\treturn vars[type];\n\t}\n\n\tthen(onFulfilled) {\n\t\tlet self = this;\n\t\treturn new Promise(resolve => {\n\t\t\tlet f = _isFunction(onFulfilled) ? onFulfilled : _passThrough,\n\t\t\t\t_resolve = () => {\n\t\t\t\t\tlet _then = self.then;\n\t\t\t\t\tself.then = null; // temporarily null the then() method to avoid an infinite loop (see https://github.com/greensock/GSAP/issues/322)\n\t\t\t\t\t_isFunction(f) && (f = f(self)) && (f.then || f === self) && (self.then = _then);\n\t\t\t\t\tresolve(f);\n\t\t\t\t\tself.then = _then;\n\t\t\t\t};\n\t\t\tif (self._initted && (self.totalProgress() === 1 && self._ts >= 0) || (!self._tTime && self._ts < 0)) {\n\t\t\t\t_resolve();\n\t\t\t} else {\n\t\t\t\tself._prom = _resolve;\n\t\t\t}\n\t\t});\n\t}\n\n\tkill() {\n\t\t_interrupt(this);\n\t}\n\n}\n\n_setDefaults(Animation.prototype, {_time:0, _start:0, _end:0, _tTime:0, _tDur:0, _dirty:0, _repeat:0, _yoyo:false, parent:null, _initted:false, _rDelay:0, _ts:1, _dp:0, ratio:0, _zTime:-_tinyNum, _prom:0, _ps:false, _rts:1});\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/*\n * -------------------------------------------------\n * TIMELINE\n * -------------------------------------------------\n */\n\nexport class Timeline extends Animation {\n\n\tconstructor(vars = {}, position) {\n\t\tsuper(vars);\n\t\tthis.labels = {};\n\t\tthis.smoothChildTiming = !!vars.smoothChildTiming;\n\t\tthis.autoRemoveChildren = !!vars.autoRemoveChildren;\n\t\tthis._sort = _isNotFalse(vars.sortChildren);\n\t\t_globalTimeline && _addToTimeline(vars.parent || _globalTimeline, this, position);\n\t\tvars.reversed && this.reverse();\n\t\tvars.paused && this.paused(true);\n\t\tvars.scrollTrigger && _scrollTrigger(this, vars.scrollTrigger);\n\t}\n\n\tto(targets, vars, position) {\n\t\t_createTweenType(0, arguments, this);\n\t\treturn this;\n\t}\n\n\tfrom(targets, vars, position) {\n\t\t_createTweenType(1, arguments, this);\n\t\treturn this;\n\t}\n\n\tfromTo(targets, fromVars, toVars, position) {\n\t\t_createTweenType(2, arguments, this);\n\t\treturn this;\n\t}\n\n\tset(targets, vars, position) {\n\t\tvars.duration = 0;\n\t\tvars.parent = this;\n\t\t_inheritDefaults(vars).repeatDelay || (vars.repeat = 0);\n\t\tvars.immediateRender = !!vars.immediateRender;\n\t\tnew Tween(targets, vars, _parsePosition(this, position), 1);\n\t\treturn this;\n\t}\n\n\tcall(callback, params, position) {\n\t\treturn _addToTimeline(this, Tween.delayedCall(0, callback, params), position);\n\t}\n\n\t//ONLY for backward compatibility! Maybe delete?\n\tstaggerTo(targets, duration, vars, stagger, position, onCompleteAll, onCompleteAllParams) {\n\t\tvars.duration = duration;\n\t\tvars.stagger = vars.stagger || stagger;\n\t\tvars.onComplete = onCompleteAll;\n\t\tvars.onCompleteParams = onCompleteAllParams;\n\t\tvars.parent = this;\n\t\tnew Tween(targets, vars, _parsePosition(this, position));\n\t\treturn this;\n\t}\n\n\tstaggerFrom(targets, duration, vars, stagger, position, onCompleteAll, onCompleteAllParams) {\n\t\tvars.runBackwards = 1;\n\t\t_inheritDefaults(vars).immediateRender = _isNotFalse(vars.immediateRender);\n\t\treturn this.staggerTo(targets, duration, vars, stagger, position, onCompleteAll, onCompleteAllParams);\n\t}\n\n\tstaggerFromTo(targets, duration, fromVars, toVars, stagger, position, onCompleteAll, onCompleteAllParams) {\n\t\ttoVars.startAt = fromVars;\n\t\t_inheritDefaults(toVars).immediateRender = _isNotFalse(toVars.immediateRender);\n\t\treturn this.staggerTo(targets, duration, toVars, stagger, position, onCompleteAll, onCompleteAllParams);\n\t}\n\n\trender(totalTime, suppressEvents, force) {\n\t\tlet prevTime = this._time,\n\t\t\ttDur = this._dirty ? this.totalDuration() : this._tDur,\n\t\t\tdur = this._dur,\n\t\t\ttTime = totalTime <= 0 ? 0 : _roundPrecise(totalTime), // if a paused timeline is resumed (or its _start is updated for another reason...which rounds it), that could result in the playhead shifting a **tiny** amount and a zero-duration child at that spot may get rendered at a different ratio, like its totalTime in render() may be 1e-17 instead of 0, for example.\n\t\t\tcrossingStart = (this._zTime < 0) !== (totalTime < 0) && (this._initted || !dur),\n\t\t\ttime, child, next, iteration, cycleDuration, prevPaused, pauseTween, timeScale, prevStart, prevIteration, yoyo, isYoyo;\n\t\tthis !== _globalTimeline && tTime > tDur && totalTime >= 0 && (tTime = tDur);\n\t\tif (tTime !== this._tTime || force || crossingStart) {\n\t\t\tif (prevTime !== this._time && dur) { //if totalDuration() finds a child with a negative startTime and smoothChildTiming is true, things get shifted around internally so we need to adjust the time accordingly. For example, if a tween starts at -30 we must shift EVERYTHING forward 30 seconds and move this timeline's startTime backward by 30 seconds so that things align with the playhead (no jump).\n\t\t\t\ttTime += this._time - prevTime;\n\t\t\t\ttotalTime += this._time - prevTime;\n\t\t\t}\n\t\t\ttime = tTime;\n\t\t\tprevStart = this._start;\n\t\t\ttimeScale = this._ts;\n\t\t\tprevPaused = !timeScale;\n\t\t\tif (crossingStart) {\n\t\t\t\tdur || (prevTime = this._zTime);\n\t\t\t\t //when the playhead arrives at EXACTLY time 0 (right on top) of a zero-duration timeline, we need to discern if events are suppressed so that when the playhead moves again (next time), it'll trigger the callback. If events are NOT suppressed, obviously the callback would be triggered in this render. Basically, the callback should fire either when the playhead ARRIVES or LEAVES this exact spot, not both. Imagine doing a timeline.seek(0) and there's a callback that sits at 0. Since events are suppressed on that seek() by default, nothing will fire, but when the playhead moves off of that position, the callback should fire. This behavior is what people intuitively expect.\n\t\t\t\t(totalTime || !suppressEvents) && (this._zTime = totalTime);\n\t\t\t}\n\t\t\tif (this._repeat) { //adjust the time for repeats and yoyos\n\t\t\t\tyoyo = this._yoyo;\n\t\t\t\tcycleDuration = dur + this._rDelay;\n\t\t\t\tif (this._repeat < -1 && totalTime < 0) {\n\t\t\t\t\treturn this.totalTime(cycleDuration * 100 + totalTime, suppressEvents, force);\n\t\t\t\t}\n\t\t\t\ttime = _roundPrecise(tTime % cycleDuration); //round to avoid floating point errors. (4 % 0.8 should be 0 but some browsers report it as 0.79999999!)\n\t\t\t\tif (tTime === tDur) { // the tDur === tTime is for edge cases where there's a lengthy decimal on the duration and it may reach the very end but the time is rendered as not-quite-there (remember, tDur is rounded to 4 decimals whereas dur isn't)\n\t\t\t\t\titeration = this._repeat;\n\t\t\t\t\ttime = dur;\n\t\t\t\t} else {\n\t\t\t\t\tprevIteration = _roundPrecise(tTime / cycleDuration); // full decimal version of iterations, not the previous iteration (we're reusing prevIteration variable for efficiency)\n\t\t\t\t\titeration = ~~prevIteration;\n\t\t\t\t\tif (iteration && iteration === prevIteration) {\n\t\t\t\t\t\ttime = dur;\n\t\t\t\t\t\titeration--;\n\t\t\t\t\t}\n\t\t\t\t\ttime > dur && (time = dur);\n\t\t\t\t}\n\t\t\t\tprevIteration = _animationCycle(this._tTime, cycleDuration);\n\t\t\t\t!prevTime && this._tTime && prevIteration !== iteration && this._tTime - prevIteration * cycleDuration - this._dur <= 0 && (prevIteration = iteration); // edge case - if someone does addPause() at the very beginning of a repeating timeline, that pause is technically at the same spot as the end which causes this._time to get set to 0 when the totalTime would normally place the playhead at the end. See https://gsap.com/forums/topic/23823-closing-nav-animation-not-working-on-ie-and-iphone-6-maybe-other-older-browser/?tab=comments#comment-113005 also, this._tTime - prevIteration * cycleDuration - this._dur <= 0 just checks to make sure it wasn't previously in the \"repeatDelay\" portion\n\t\t\t\tif (yoyo && (iteration & 1)) {\n\t\t\t\t\ttime = dur - time;\n\t\t\t\t\tisYoyo = 1;\n\t\t\t\t}\n\t\t\t\t/*\n\t\t\t\tmake sure children at the end/beginning of the timeline are rendered properly. If, for example,\n\t\t\t\ta 3-second long timeline rendered at 2.9 seconds previously, and now renders at 3.2 seconds (which\n\t\t\t\twould get translated to 2.8 seconds if the timeline yoyos or 0.2 seconds if it just repeats), there\n\t\t\t\tcould be a callback or a short tween that's at 2.95 or 3 seconds in which wouldn't render. So\n\t\t\t\twe need to push the timeline to the end (and/or beginning depending on its yoyo value). Also we must\n\t\t\t\tensure that zero-duration tweens at the very beginning or end of the Timeline work.\n\t\t\t\t*/\n\t\t\t\tif (iteration !== prevIteration && !this._lock) {\n\t\t\t\t\tlet rewinding = (yoyo && (prevIteration & 1)),\n\t\t\t\t\t\tdoesWrap = (rewinding === (yoyo && (iteration & 1)));\n\t\t\t\t\titeration < prevIteration && (rewinding = !rewinding);\n\t\t\t\t\tprevTime = rewinding ? 0 : tTime % dur ? dur : tTime; // if the playhead is landing exactly at the end of an iteration, use that totalTime rather than only the duration, otherwise it'll skip the 2nd render since it's effectively at the same time.\n\t\t\t\t\tthis._lock = 1;\n\t\t\t\t\tthis.render(prevTime || (isYoyo ? 0 : _roundPrecise(iteration * cycleDuration)), suppressEvents, !dur)._lock = 0;\n\t\t\t\t\tthis._tTime = tTime; // if a user gets the iteration() inside the onRepeat, for example, it should be accurate.\n\t\t\t\t\t!suppressEvents && this.parent && _callback(this, \"onRepeat\");\n\t\t\t\t\tthis.vars.repeatRefresh && !isYoyo && (this.invalidate()._lock = 1);\n\t\t\t\t\tif ((prevTime && prevTime !== this._time) || prevPaused !== !this._ts || (this.vars.onRepeat && !this.parent && !this._act)) { // if prevTime is 0 and we render at the very end, _time will be the end, thus won't match. So in this edge case, prevTime won't match _time but that's okay. If it gets killed in the onRepeat, eject as well.\n\t\t\t\t\t\treturn this;\n\t\t\t\t\t}\n\t\t\t\t\tdur = this._dur; // in case the duration changed in the onRepeat\n\t\t\t\t\ttDur = this._tDur;\n\t\t\t\t\tif (doesWrap) {\n\t\t\t\t\t\tthis._lock = 2;\n\t\t\t\t\t\tprevTime = rewinding ? dur : -0.0001;\n\t\t\t\t\t\tthis.render(prevTime, true);\n\t\t\t\t\t\tthis.vars.repeatRefresh && !isYoyo && this.invalidate();\n\t\t\t\t\t}\n\t\t\t\t\tthis._lock = 0;\n\t\t\t\t\tif (!this._ts && !prevPaused) {\n\t\t\t\t\t\treturn this;\n\t\t\t\t\t}\n\t\t\t\t\t//in order for yoyoEase to work properly when there's a stagger, we must swap out the ease in each sub-tween.\n\t\t\t\t\t_propagateYoyoEase(this, isYoyo);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (this._hasPause && !this._forcing && this._lock < 2) {\n\t\t\t\tpauseTween = _findNextPauseTween(this, _roundPrecise(prevTime), _roundPrecise(time));\n\t\t\t\tif (pauseTween) {\n\t\t\t\t\ttTime -= time - (time = pauseTween._start);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis._tTime = tTime;\n\t\t\tthis._time = time;\n\t\t\tthis._act = !timeScale; //as long as it's not paused, force it to be active so that if the user renders independent of the parent timeline, it'll be forced to re-render on the next tick.\n\n\t\t\tif (!this._initted) {\n\t\t\t\tthis._onUpdate = this.vars.onUpdate;\n\t\t\t\tthis._initted = 1;\n\t\t\t\tthis._zTime = totalTime;\n\t\t\t\tprevTime = 0; // upon init, the playhead should always go forward; someone could invalidate() a completed timeline and then if they restart(), that would make child tweens render in reverse order which could lock in the wrong starting values if they build on each other, like tl.to(obj, {x: 100}).to(obj, {x: 0}).\n\t\t\t}\n\t\t\tif (!prevTime && time && !suppressEvents && !iteration) {\n\t\t\t\t_callback(this, \"onStart\");\n\t\t\t\tif (this._tTime !== tTime) { // in case the onStart triggered a render at a different spot, eject. Like if someone did animation.pause(0.5) or something inside the onStart.\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (time >= prevTime && totalTime >= 0) {\n\t\t\t\tchild = this._first;\n\t\t\t\twhile (child) {\n\t\t\t\t\tnext = child._next;\n\t\t\t\t\tif ((child._act || time >= child._start) && child._ts && pauseTween !== child) {\n\t\t\t\t\t\tif (child.parent !== this) { // an extreme edge case - the child's render could do something like kill() the \"next\" one in the linked list, or reparent it. In that case we must re-initiate the whole render to be safe.\n\t\t\t\t\t\t\treturn this.render(totalTime, suppressEvents, force);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchild.render(child._ts > 0 ? (time - child._start) * child._ts : (child._dirty ? child.totalDuration() : child._tDur) + (time - child._start) * child._ts, suppressEvents, force);\n\t\t\t\t\t\tif (time !== this._time || (!this._ts && !prevPaused)) { //in case a tween pauses or seeks the timeline when rendering, like inside of an onUpdate/onComplete\n\t\t\t\t\t\t\tpauseTween = 0;\n\t\t\t\t\t\t\tnext && (tTime += (this._zTime = -_tinyNum)); // it didn't finish rendering, so flag zTime as negative so that the next time render() is called it'll be forced (to render any remaining children)\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tchild = next;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tchild = this._last;\n\t\t\t\tlet adjustedTime = totalTime < 0 ? totalTime : time; //when the playhead goes backward beyond the start of this timeline, we must pass that information down to the child animations so that zero-duration tweens know whether to render their starting or ending values.\n\t\t\t\twhile (child) {\n\t\t\t\t\tnext = child._prev;\n\t\t\t\t\tif ((child._act || adjustedTime <= child._end) && child._ts && pauseTween !== child) {\n\t\t\t\t\t\tif (child.parent !== this) { // an extreme edge case - the child's render could do something like kill() the \"next\" one in the linked list, or reparent it. In that case we must re-initiate the whole render to be safe.\n\t\t\t\t\t\t\treturn this.render(totalTime, suppressEvents, force);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tchild.render(child._ts > 0 ? (adjustedTime - child._start) * child._ts : (child._dirty ? child.totalDuration() : child._tDur) + (adjustedTime - child._start) * child._ts, suppressEvents, force || (_reverting && (child._initted || child._startAt))); // if reverting, we should always force renders of initted tweens (but remember that .fromTo() or .from() may have a _startAt but not _initted yet). If, for example, a .fromTo() tween with a stagger (which creates an internal timeline) gets reverted BEFORE some of its child tweens render for the first time, it may not properly trigger them to revert.\n\t\t\t\t\t\tif (time !== this._time || (!this._ts && !prevPaused)) { //in case a tween pauses or seeks the timeline when rendering, like inside of an onUpdate/onComplete\n\t\t\t\t\t\t\tpauseTween = 0;\n\t\t\t\t\t\t\tnext && (tTime += (this._zTime = adjustedTime ? -_tinyNum : _tinyNum)); // it didn't finish rendering, so adjust zTime so that so that the next time render() is called it'll be forced (to render any remaining children)\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tchild = next;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (pauseTween && !suppressEvents) {\n\t\t\t\tthis.pause();\n\t\t\t\tpauseTween.render(time >= prevTime ? 0 : -_tinyNum)._zTime = time >= prevTime ? 1 : -1;\n\t\t\t\tif (this._ts) { //the callback resumed playback! So since we may have held back the playhead due to where the pause is positioned, go ahead and jump to where it's SUPPOSED to be (if no pause happened).\n\t\t\t\t\tthis._start = prevStart; //if the pause was at an earlier time and the user resumed in the callback, it could reposition the timeline (changing its startTime), throwing things off slightly, so we make sure the _start doesn't shift.\n\t\t\t\t\t_setEnd(this);\n\t\t\t\t\treturn this.render(totalTime, suppressEvents, force);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis._onUpdate && !suppressEvents && _callback(this, \"onUpdate\", true);\n\t\t\tif ((tTime === tDur && this._tTime >= this.totalDuration()) || (!tTime && prevTime)) if (prevStart === this._start || Math.abs(timeScale) !== Math.abs(this._ts)) if (!this._lock) { // remember, a child's callback may alter this timeline's playhead or timeScale which is why we need to add some of these checks.\n\t\t\t\t(totalTime || !dur) && ((tTime === tDur && this._ts > 0) || (!tTime && this._ts < 0)) && _removeFromParent(this, 1); // don't remove if the timeline is reversed and the playhead isn't at 0, otherwise tl.progress(1).reverse() won't work. Only remove if the playhead is at the end and timeScale is positive, or if the playhead is at 0 and the timeScale is negative.\n\t\t\t\tif (!suppressEvents && !(totalTime < 0 && !prevTime) && (tTime || prevTime || !tDur)) {\n\t\t\t\t\t_callback(this, (tTime === tDur && totalTime >= 0 ? \"onComplete\" : \"onReverseComplete\"), true);\n\t\t\t\t\tthis._prom && !(tTime < tDur && this.timeScale() > 0) && this._prom();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn this;\n\t}\n\n\tadd(child, position) {\n\t\t_isNumber(position) || (position = _parsePosition(this, position, child));\n\t\tif (!(child instanceof Animation)) {\n\t\t\tif (_isArray(child)) {\n\t\t\t\tchild.forEach(obj => this.add(obj, position));\n\t\t\t\treturn this;\n\t\t\t}\n\t\t\tif (_isString(child)) {\n\t\t\t\treturn this.addLabel(child, position);\n\t\t\t}\n\t\t\tif (_isFunction(child)) {\n\t\t\t\tchild = Tween.delayedCall(0, child);\n\t\t\t} else {\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}\n\t\treturn this !== child ? _addToTimeline(this, child, position) : this; //don't allow a timeline to be added to itself as a child!\n\t}\n\n\tgetChildren(nested = true, tweens = true, timelines = true, ignoreBeforeTime = -_bigNum) {\n\t\tlet a = [],\n\t\t\tchild = this._first;\n\t\twhile (child) {\n\t\t\tif (child._start >= ignoreBeforeTime) {\n\t\t\t\tif (child instanceof Tween) {\n\t\t\t\t\ttweens && a.push(child);\n\t\t\t\t} else {\n\t\t\t\t\ttimelines && a.push(child);\n\t\t\t\t\tnested && a.push(...child.getChildren(true, tweens, timelines));\n\t\t\t\t}\n\t\t\t}\n\t\t\tchild = child._next;\n\t\t}\n\t\treturn a;\n\t}\n\n\tgetById(id) {\n\t\tlet animations = this.getChildren(1, 1, 1),\n\t\t\ti = animations.length;\n\t\twhile(i--) {\n\t\t\tif (animations[i].vars.id === id) {\n\t\t\t\treturn animations[i];\n\t\t\t}\n\t\t}\n\t}\n\n\tremove(child) {\n\t\tif (_isString(child)) {\n\t\t\treturn this.removeLabel(child);\n\t\t}\n\t\tif (_isFunction(child)) {\n\t\t\treturn this.killTweensOf(child);\n\t\t}\n\t\tchild.parent === this && _removeLinkedListItem(this, child);\n\t\tif (child === this._recent) {\n\t\t\tthis._recent = this._last;\n\t\t}\n\t\treturn _uncache(this);\n\t}\n\n\ttotalTime(totalTime, suppressEvents) {\n\t\tif (!arguments.length) {\n\t\t\treturn this._tTime;\n\t\t}\n\t\tthis._forcing = 1;\n\t\tif (!this._dp && this._ts) { //special case for the global timeline (or any other that has no parent or detached parent).\n\t\t\tthis._start = _roundPrecise(_ticker.time - (this._ts > 0 ? totalTime / this._ts : (this.totalDuration() - totalTime) / -this._ts));\n\t\t}\n\t\tsuper.totalTime(totalTime, suppressEvents);\n\t\tthis._forcing = 0;\n\t\treturn this;\n\t}\n\n\taddLabel(label, position) {\n\t\tthis.labels[label] = _parsePosition(this, position);\n\t\treturn this;\n\t}\n\n\tremoveLabel(label) {\n\t\tdelete this.labels[label];\n\t\treturn this;\n\t}\n\n\taddPause(position, callback, params) {\n\t\tlet t = Tween.delayedCall(0, callback || _emptyFunc, params);\n\t\tt.data = \"isPause\";\n\t\tthis._hasPause = 1;\n\t\treturn _addToTimeline(this, t, _parsePosition(this, position));\n\t}\n\n\tremovePause(position) {\n\t\tlet child = this._first;\n\t\tposition = _parsePosition(this, position);\n\t\twhile (child) {\n\t\t\tif (child._start === position && child.data === \"isPause\") {\n\t\t\t\t_removeFromParent(child);\n\t\t\t}\n\t\t\tchild = child._next;\n\t\t}\n\t}\n\n\tkillTweensOf(targets, props, onlyActive) {\n\t\tlet tweens = this.getTweensOf(targets, onlyActive),\n\t\t\ti = tweens.length;\n\t\twhile (i--) {\n\t\t\t(_overwritingTween !== tweens[i]) && tweens[i].kill(targets, props);\n\t\t}\n\t\treturn this;\n\t}\n\n\tgetTweensOf(targets, onlyActive) {\n\t\tlet a = [],\n\t\t\tparsedTargets = toArray(targets),\n\t\t\tchild = this._first,\n\t\t\tisGlobalTime = _isNumber(onlyActive), // a number is interpreted as a global time. If the animation spans\n\t\t\tchildren;\n\t\twhile (child) {\n\t\t\tif (child instanceof Tween) {\n\t\t\t\tif (_arrayContainsAny(child._targets, parsedTargets) && (isGlobalTime ? (!_overwritingTween || (child._initted && child._ts)) && child.globalTime(0) <= onlyActive && child.globalTime(child.totalDuration()) > onlyActive : !onlyActive || child.isActive())) { // note: if this is for overwriting, it should only be for tweens that aren't paused and are initted.\n\t\t\t\t\ta.push(child);\n\t\t\t\t}\n\t\t\t} else if ((children = child.getTweensOf(parsedTargets, onlyActive)).length) {\n\t\t\t\ta.push(...children);\n\t\t\t}\n\t\t\tchild = child._next;\n\t\t}\n\t\treturn a;\n\t}\n\n\t// potential future feature - targets() on timelines\n\t// targets() {\n\t// \tlet result = [];\n\t// \tthis.getChildren(true, true, false).forEach(t => result.push(...t.targets()));\n\t// \treturn result.filter((v, i) => result.indexOf(v) === i);\n\t// }\n\n\ttweenTo(position, vars) {\n\t\tvars = vars || {};\n\t\tlet tl = this,\n\t\t\tendTime = _parsePosition(tl, position),\n\t\t\t{ startAt, onStart, onStartParams, immediateRender } = vars,\n\t\t\tinitted,\n\t\t\ttween = Tween.to(tl, _setDefaults({\n\t\t\t\tease: vars.ease || \"none\",\n\t\t\t\tlazy: false,\n\t\t\t\timmediateRender: false,\n\t\t\t\ttime: endTime,\n\t\t\t\toverwrite: \"auto\",\n\t\t\t\tduration: vars.duration || (Math.abs((endTime - ((startAt && \"time\" in startAt) ? startAt.time : tl._time)) / tl.timeScale())) || _tinyNum,\n\t\t\t\tonStart: () => {\n\t\t\t\t\ttl.pause();\n\t\t\t\t\tif (!initted) {\n\t\t\t\t\t\tlet duration = vars.duration || Math.abs((endTime - ((startAt && \"time\" in startAt) ? startAt.time : tl._time)) / tl.timeScale());\n\t\t\t\t\t\t(tween._dur !== duration) && _setDuration(tween, duration, 0, 1).render(tween._time, true, true);\n\t\t\t\t\t\tinitted = 1;\n\t\t\t\t\t}\n\t\t\t\t\tonStart && onStart.apply(tween, onStartParams || []); //in case the user had an onStart in the vars - we don't want to overwrite it.\n\t\t\t\t}\n\t\t\t}, vars));\n\t\treturn immediateRender ? tween.render(0) : tween;\n\t}\n\n\ttweenFromTo(fromPosition, toPosition, vars) {\n\t\treturn this.tweenTo(toPosition, _setDefaults({startAt:{time:_parsePosition(this, fromPosition)}}, vars));\n\t}\n\n\trecent() {\n\t\treturn this._recent;\n\t}\n\n\tnextLabel(afterTime = this._time) {\n\t\treturn _getLabelInDirection(this, _parsePosition(this, afterTime));\n\t}\n\n\tpreviousLabel(beforeTime = this._time) {\n\t\treturn _getLabelInDirection(this, _parsePosition(this, beforeTime), 1);\n\t}\n\n\tcurrentLabel(value) {\n\t\treturn arguments.length ? this.seek(value, true) : this.previousLabel(this._time + _tinyNum);\n\t}\n\n\tshiftChildren(amount, adjustLabels, ignoreBeforeTime = 0) {\n\t\tlet child = this._first,\n\t\t\tlabels = this.labels,\n\t\t\tp;\n\t\twhile (child) {\n\t\t\tif (child._start >= ignoreBeforeTime) {\n\t\t\t\tchild._start += amount;\n\t\t\t\tchild._end += amount;\n\t\t\t}\n\t\t\tchild = child._next;\n\t\t}\n\t\tif (adjustLabels) {\n\t\t\tfor (p in labels) {\n\t\t\t\tif (labels[p] >= ignoreBeforeTime) {\n\t\t\t\t\tlabels[p] += amount;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn _uncache(this);\n\t}\n\n\tinvalidate(soft) {\n\t\tlet child = this._first;\n\t\tthis._lock = 0;\n\t\twhile (child) {\n\t\t\tchild.invalidate(soft);\n\t\t\tchild = child._next;\n\t\t}\n\t\treturn super.invalidate(soft);\n\t}\n\n\tclear(includeLabels = true) {\n\t\tlet child = this._first,\n\t\t\tnext;\n\t\twhile (child) {\n\t\t\tnext = child._next;\n\t\t\tthis.remove(child);\n\t\t\tchild = next;\n\t\t}\n\t\tthis._dp && (this._time = this._tTime = this._pTime = 0);\n\t\tincludeLabels && (this.labels = {});\n\t\treturn _uncache(this);\n\t}\n\n\ttotalDuration(value) {\n\t\tlet max = 0,\n\t\t\tself = this,\n\t\t\tchild = self._last,\n\t\t\tprevStart = _bigNum,\n\t\t\tprev, start, parent;\n\t\tif (arguments.length) {\n\t\t\treturn self.timeScale((self._repeat < 0 ? self.duration() : self.totalDuration()) / (self.reversed() ? -value : value));\n\t\t}\n\t\tif (self._dirty) {\n\t\t\tparent = self.parent;\n\t\t\twhile (child) {\n\t\t\t\tprev = child._prev; //record it here in case the tween changes position in the sequence...\n\t\t\t\tchild._dirty && child.totalDuration(); //could change the tween._startTime, so make sure the animation's cache is clean before analyzing it.\n\t\t\t\tstart = child._start;\n\t\t\t\tif (start > prevStart && self._sort && child._ts && !self._lock) { //in case one of the tweens shifted out of order, it needs to be re-inserted into the correct position in the sequence\n\t\t\t\t\tself._lock = 1; //prevent endless recursive calls - there are methods that get triggered that check duration/totalDuration when we add().\n\t\t\t\t\t_addToTimeline(self, child, start - child._delay, 1)._lock = 0;\n\t\t\t\t} else {\n\t\t\t\t\tprevStart = start;\n\t\t\t\t}\n\t\t\t\tif (start < 0 && child._ts) { //children aren't allowed to have negative startTimes unless smoothChildTiming is true, so adjust here if one is found.\n\t\t\t\t\tmax -= start;\n\t\t\t\t\tif ((!parent && !self._dp) || (parent && parent.smoothChildTiming)) {\n\t\t\t\t\t\tself._start += start / self._ts;\n\t\t\t\t\t\tself._time -= start;\n\t\t\t\t\t\tself._tTime -= start;\n\t\t\t\t\t}\n\t\t\t\t\tself.shiftChildren(-start, false, -1e999);\n\t\t\t\t\tprevStart = 0;\n\t\t\t\t}\n\t\t\t\tchild._end > max && child._ts && (max = child._end);\n\t\t\t\tchild = prev;\n\t\t\t}\n\t\t\t_setDuration(self, (self === _globalTimeline && self._time > max) ? self._time : max, 1, 1);\n\t\t\tself._dirty = 0;\n\t\t}\n\t\treturn self._tDur;\n\t}\n\n\tstatic updateRoot(time) {\n\t\tif (_globalTimeline._ts) {\n\t\t\t_lazySafeRender(_globalTimeline, _parentToChildTotalTime(time, _globalTimeline));\n\t\t\t_lastRenderedFrame = _ticker.frame;\n\t\t}\n\t\tif (_ticker.frame >= _nextGCFrame) {\n\t\t\t_nextGCFrame += _config.autoSleep || 120;\n\t\t\tlet child = _globalTimeline._first;\n\t\t\tif (!child || !child._ts) if (_config.autoSleep && _ticker._listeners.length < 2) {\n\t\t\t\twhile (child && !child._ts) {\n\t\t\t\t\tchild = child._next;\n\t\t\t\t}\n\t\t\t\tchild || _ticker.sleep();\n\t\t\t}\n\t\t}\n\t}\n\n}\n\n_setDefaults(Timeline.prototype, {_lock:0, _hasPause:0, _forcing:0});\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nlet _addComplexStringPropTween = function(target, prop, start, end, setter, stringFilter, funcParam) { //note: we call _addComplexStringPropTween.call(tweenInstance...) to ensure that it's scoped properly. We may call it from within a plugin too, thus \"this\" would refer to the plugin.\n\t\tlet pt = new PropTween(this._pt, target, prop, 0, 1, _renderComplexString, null, setter),\n\t\t\tindex = 0,\n\t\t\tmatchIndex = 0,\n\t\t\tresult,\tstartNums, color, endNum, chunk, startNum, hasRandom, a;\n\t\tpt.b = start;\n\t\tpt.e = end;\n\t\tstart += \"\"; //ensure values are strings\n\t\tend += \"\";\n\t\tif ((hasRandom = ~end.indexOf(\"random(\"))) {\n\t\t\tend = _replaceRandom(end);\n\t\t}\n\t\tif (stringFilter) {\n\t\t\ta = [start, end];\n\t\t\tstringFilter(a, target, prop); //pass an array with the starting and ending values and let the filter do whatever it needs to the values.\n\t\t\tstart = a[0];\n\t\t\tend = a[1];\n\t\t}\n\t\tstartNums = start.match(_complexStringNumExp) || [];\n\t\twhile ((result = _complexStringNumExp.exec(end))) {\n\t\t\tendNum = result[0];\n\t\t\tchunk = end.substring(index, result.index);\n\t\t\tif (color) {\n\t\t\t\tcolor = (color + 1) % 5;\n\t\t\t} else if (chunk.substr(-5) === \"rgba(\") {\n\t\t\t\tcolor = 1;\n\t\t\t}\n\t\t\tif (endNum !== startNums[matchIndex++]) {\n\t\t\t\tstartNum = parseFloat(startNums[matchIndex-1]) || 0;\n\t\t\t\t//these nested PropTweens are handled in a special way - we'll never actually call a render or setter method on them. We'll just loop through them in the parent complex string PropTween's render method.\n\t\t\t\tpt._pt = {\n\t\t\t\t\t_next: pt._pt,\n\t\t\t\t\tp: (chunk || matchIndex === 1) ? chunk : \",\", //note: SVG spec allows omission of comma/space when a negative sign is wedged between two numbers, like 2.5-5.3 instead of 2.5,-5.3 but when tweening, the negative value may switch to positive, so we insert the comma just in case.\n\t\t\t\t\ts: startNum,\n\t\t\t\t\tc: endNum.charAt(1) === \"=\" ? _parseRelative(startNum, endNum) - startNum : parseFloat(endNum) - startNum,\n\t\t\t\t\tm: (color && color < 4) ? Math.round : 0\n\t\t\t\t};\n\t\t\t\tindex = _complexStringNumExp.lastIndex;\n\t\t\t}\n\t\t}\n\t\tpt.c = (index < end.length) ? end.substring(index, end.length) : \"\"; //we use the \"c\" of the PropTween to store the final part of the string (after the last number)\n\t\tpt.fp = funcParam;\n\t\tif (_relExp.test(end) || hasRandom) {\n\t\t\tpt.e = 0; //if the end string contains relative values or dynamic random(...) values, delete the end it so that on the final render we don't actually set it to the string with += or -= characters (forces it to use the calculated value).\n\t\t}\n\t\tthis._pt = pt; //start the linked list with this new PropTween. Remember, we call _addComplexStringPropTween.call(tweenInstance...) to ensure that it's scoped properly. We may call it from within a plugin too, thus \"this\" would refer to the plugin.\n\t\treturn pt;\n\t},\n\t_addPropTween = function(target, prop, start, end, index, targets, modifier, stringFilter, funcParam, optional) {\n\t\t_isFunction(end) && (end = end(index || 0, target, targets));\n\t\tlet currentValue = target[prop],\n\t\t\tparsedStart = (start !== \"get\") ? start : !_isFunction(currentValue) ? currentValue : (funcParam ? target[(prop.indexOf(\"set\") || !_isFunction(target[\"get\" + prop.substr(3)])) ? prop : \"get\" + prop.substr(3)](funcParam) : target[prop]()),\n\t\t\tsetter = !_isFunction(currentValue) ? _setterPlain : funcParam ? _setterFuncWithParam : _setterFunc,\n\t\t\tpt;\n\t\tif (_isString(end)) {\n\t\t\tif (~end.indexOf(\"random(\")) {\n\t\t\t\tend = _replaceRandom(end);\n\t\t\t}\n\t\t\tif (end.charAt(1) === \"=\") {\n\t\t\t\tpt = _parseRelative(parsedStart, end) + (getUnit(parsedStart) || 0);\n\t\t\t\tif (pt || pt === 0) { // to avoid isNaN, like if someone passes in a value like \"!= whatever\"\n\t\t\t\t\tend = pt;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!optional || parsedStart !== end || _forceAllPropTweens) {\n\t\t\tif (!isNaN(parsedStart * end) && end !== \"\") { // fun fact: any number multiplied by \"\" is evaluated as the number 0!\n\t\t\t\tpt = new PropTween(this._pt, target, prop, +parsedStart || 0, end - (parsedStart || 0), typeof(currentValue) === \"boolean\" ? _renderBoolean : _renderPlain, 0, setter);\n\t\t\t\tfuncParam && (pt.fp = funcParam);\n\t\t\t\tmodifier && pt.modifier(modifier, this, target);\n\t\t\t\treturn (this._pt = pt);\n\t\t\t}\n\t\t\t!currentValue && !(prop in target) && _missingPlugin(prop, end);\n\t\t\treturn _addComplexStringPropTween.call(this, target, prop, parsedStart, end, setter, stringFilter || _config.stringFilter, funcParam);\n\t\t}\n\t},\n\t//creates a copy of the vars object and processes any function-based values (putting the resulting values directly into the copy) as well as strings with \"random()\" in them. It does NOT process relative values.\n\t_processVars = (vars, index, target, targets, tween) => {\n\t\t_isFunction(vars) && (vars = _parseFuncOrString(vars, tween, index, target, targets));\n\t\tif (!_isObject(vars) || (vars.style && vars.nodeType) || _isArray(vars) || _isTypedArray(vars)) {\n\t\t\treturn _isString(vars) ? _parseFuncOrString(vars, tween, index, target, targets) : vars;\n\t\t}\n\t\tlet copy = {},\n\t\t\tp;\n\t\tfor (p in vars) {\n\t\t\tcopy[p] = _parseFuncOrString(vars[p], tween, index, target, targets);\n\t\t}\n\t\treturn copy;\n\t},\n\t_checkPlugin = (property, vars, tween, index, target, targets) => {\n\t\tlet plugin, pt, ptLookup, i;\n\t\tif (_plugins[property] && (plugin = new _plugins[property]()).init(target, plugin.rawVars ? vars[property] : _processVars(vars[property], index, target, targets, tween), tween, index, targets) !== false) {\n\t\t\ttween._pt = pt = new PropTween(tween._pt, target, property, 0, 1, plugin.render, plugin, 0, plugin.priority);\n\t\t\tif (tween !== _quickTween) {\n\t\t\t\tptLookup = tween._ptLookup[tween._targets.indexOf(target)]; //note: we can't use tween._ptLookup[index] because for staggered tweens, the index from the fullTargets array won't match what it is in each individual tween that spawns from the stagger.\n\t\t\t\ti = plugin._props.length;\n\t\t\t\twhile (i--) {\n\t\t\t\t\tptLookup[plugin._props[i]] = pt;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn plugin;\n\t},\n\t_overwritingTween, //store a reference temporarily so we can avoid overwriting itself.\n\t_forceAllPropTweens,\n\t_initTween = (tween, time, tTime) => {\n\t\tlet vars = tween.vars,\n\t\t\t{ ease, startAt, immediateRender, lazy, onUpdate, runBackwards, yoyoEase, keyframes, autoRevert } = vars,\n\t\t\tdur = tween._dur,\n\t\t\tprevStartAt = tween._startAt,\n\t\t\ttargets = tween._targets,\n\t\t\tparent = tween.parent,\n\t\t\t//when a stagger (or function-based duration/delay) is on a Tween instance, we create a nested timeline which means that the \"targets\" of that tween don't reflect the parent. This function allows us to discern when it's a nested tween and in that case, return the full targets array so that function-based values get calculated properly. Also remember that if the tween has a stagger AND keyframes, it could be multiple levels deep which is why we store the targets Array in the vars of the timeline.\n\t\t\tfullTargets = (parent && parent.data === \"nested\") ? parent.vars.targets : targets,\n\t\t\tautoOverwrite = (tween._overwrite === \"auto\") && !_suppressOverwrites,\n\t\t\ttl = tween.timeline,\n\t\t\tcleanVars, i, p, pt, target, hasPriority, gsData, harness, plugin, ptLookup, index, harnessVars, overwritten;\n\t\ttl && (!keyframes || !ease) && (ease = \"none\");\n\t\ttween._ease = _parseEase(ease, _defaults.ease);\n\t\ttween._yEase = yoyoEase ? _invertEase(_parseEase(yoyoEase === true ? ease : yoyoEase, _defaults.ease)) : 0;\n\t\tif (yoyoEase && tween._yoyo && !tween._repeat) { //there must have been a parent timeline with yoyo:true that is currently in its yoyo phase, so flip the eases.\n\t\t\tyoyoEase = tween._yEase;\n\t\t\ttween._yEase = tween._ease;\n\t\t\ttween._ease = yoyoEase;\n\t\t}\n\t\ttween._from = !tl && !!vars.runBackwards; //nested timelines should never run backwards - the backwards-ness is in the child tweens.\n\t\tif (!tl || (keyframes && !vars.stagger)) { //if there's an internal timeline, skip all the parsing because we passed that task down the chain.\n\t\t\tharness = targets[0] ? _getCache(targets[0]).harness : 0;\n\t\t\tharnessVars = harness && vars[harness.prop]; //someone may need to specify CSS-specific values AND non-CSS values, like if the element has an \"x\" property plus it's a standard DOM element. We allow people to distinguish by wrapping plugin-specific stuff in a css:{} object for example.\n\t\t\tcleanVars = _copyExcluding(vars, _reservedProps);\n\t\t\tif (prevStartAt) {\n\t\t\t\tprevStartAt._zTime < 0 && prevStartAt.progress(1); // in case it's a lazy startAt that hasn't rendered yet.\n\t\t\t\t(time < 0 && runBackwards && immediateRender && !autoRevert) ? prevStartAt.render(-1, true) : prevStartAt.revert(runBackwards && dur ? _revertConfigNoKill : _startAtRevertConfig); // if it's a \"startAt\" (not \"from()\" or runBackwards: true), we only need to do a shallow revert (keep transforms cached in CSSPlugin)\n\t\t\t\t// don't just _removeFromParent(prevStartAt.render(-1, true)) because that'll leave inline styles. We're creating a new _startAt for \"startAt\" tweens that re-capture things to ensure that if the pre-tween values changed since the tween was created, they're recorded.\n\t\t\t\tprevStartAt._lazy = 0;\n\t\t\t}\n\t\t\tif (startAt) {\n\t\t\t\t_removeFromParent(tween._startAt = Tween.set(targets, _setDefaults({data: \"isStart\", overwrite: false, parent: parent, immediateRender: true, lazy: !prevStartAt && _isNotFalse(lazy), startAt: null, delay: 0, onUpdate: onUpdate && (() => _callback(tween, \"onUpdate\")), stagger: 0}, startAt))); //copy the properties/values into a new object to avoid collisions, like var to = {x:0}, from = {x:500}; timeline.fromTo(e, from, to).fromTo(e, to, from);\n\t\t\t\ttween._startAt._dp = 0; // don't allow it to get put back into root timeline! Like when revert() is called and totalTime() gets set.\n\t\t\t\ttween._startAt._sat = tween; // used in globalTime(). _sat stands for _startAtTween\n\t\t\t\t(time < 0 && (_reverting || (!immediateRender && !autoRevert))) && tween._startAt.revert(_revertConfigNoKill); // rare edge case, like if a render is forced in the negative direction of a non-initted tween.\n\t\t\t\tif (immediateRender) {\n\t\t\t\t\tif (dur && time <= 0 && tTime <= 0) { // check tTime here because in the case of a yoyo tween whose playhead gets pushed to the end like tween.progress(1), we should allow it through so that the onComplete gets fired properly.\n\t\t\t\t\t\ttime && (tween._zTime = time);\n\t\t\t\t\t\treturn; //we skip initialization here so that overwriting doesn't occur until the tween actually begins. Otherwise, if you create several immediateRender:true tweens of the same target/properties to drop into a Timeline, the last one created would overwrite the first ones because they didn't get placed into the timeline yet before the first render occurs and kicks in overwriting.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if (runBackwards && dur) {\n\t\t\t\t//from() tweens must be handled uniquely: their beginning values must be rendered but we don't want overwriting to occur yet (when time is still 0). Wait until the tween actually begins before doing all the routines like overwriting. At that time, we should render at the END of the tween to ensure that things initialize correctly (remember, from() tweens go backwards)\n\t\t\t\tif (!prevStartAt) {\n\t\t\t\t\ttime && (immediateRender = false); //in rare cases (like if a from() tween runs and then is invalidate()-ed), immediateRender could be true but the initial forced-render gets skipped, so there's no need to force the render in this context when the _time is greater than 0\n\t\t\t\t\tp = _setDefaults({\n\t\t\t\t\t\toverwrite: false,\n\t\t\t\t\t\tdata: \"isFromStart\", //we tag the tween with as \"isFromStart\" so that if [inside a plugin] we need to only do something at the very END of a tween, we have a way of identifying this tween as merely the one that's setting the beginning values for a \"from()\" tween. For example, clearProps in CSSPlugin should only get applied at the very END of a tween and without this tag, from(...{height:100, clearProps:\"height\", delay:1}) would wipe the height at the beginning of the tween and after 1 second, it'd kick back in.\n\t\t\t\t\t\tlazy: immediateRender && !prevStartAt && _isNotFalse(lazy),\n\t\t\t\t\t\timmediateRender: immediateRender, //zero-duration tweens render immediately by default, but if we're not specifically instructed to render this tween immediately, we should skip this and merely _init() to record the starting values (rendering them immediately would push them to completion which is wasteful in that case - we'd have to render(-1) immediately after)\n\t\t\t\t\t\tstagger: 0,\n\t\t\t\t\t\tparent: parent //ensures that nested tweens that had a stagger are handled properly, like gsap.from(\".class\", {y: gsap.utils.wrap([-100,100]), stagger: 0.5})\n\t\t\t\t\t}, cleanVars);\n\t\t\t\t\tharnessVars && (p[harness.prop] = harnessVars); // in case someone does something like .from(..., {css:{}})\n\t\t\t\t\t_removeFromParent(tween._startAt = Tween.set(targets, p));\n\t\t\t\t\ttween._startAt._dp = 0; // don't allow it to get put back into root timeline!\n\t\t\t\t\ttween._startAt._sat = tween; // used in globalTime()\n\t\t\t\t\t(time < 0) && (_reverting ? tween._startAt.revert(_revertConfigNoKill) : tween._startAt.render(-1, true));\n\t\t\t\t\ttween._zTime = time;\n\t\t\t\t\tif (!immediateRender) {\n\t\t\t\t\t\t_initTween(tween._startAt, _tinyNum, _tinyNum); //ensures that the initial values are recorded\n\t\t\t\t\t} else if (!time) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\ttween._pt = tween._ptCache = 0;\n\t\t\tlazy = (dur && _isNotFalse(lazy)) || (lazy && !dur);\n\t\t\tfor (i = 0; i < targets.length; i++) {\n\t\t\t\ttarget = targets[i];\n\t\t\t\tgsData = target._gsap || _harness(targets)[i]._gsap;\n\t\t\t\ttween._ptLookup[i] = ptLookup = {};\n\t\t\t\t_lazyLookup[gsData.id] && _lazyTweens.length && _lazyRender(); //if other tweens of the same target have recently initted but haven't rendered yet, we've got to force the render so that the starting values are correct (imagine populating a timeline with a bunch of sequential tweens and then jumping to the end)\n\t\t\t\tindex = fullTargets === targets ? i : fullTargets.indexOf(target);\n\t\t\t\tif (harness && (plugin = new harness()).init(target, harnessVars || cleanVars, tween, index, fullTargets) !== false) {\n\t\t\t\t\ttween._pt = pt = new PropTween(tween._pt, target, plugin.name, 0, 1, plugin.render, plugin, 0, plugin.priority);\n\t\t\t\t\tplugin._props.forEach(name => {ptLookup[name] = pt;});\n\t\t\t\t\tplugin.priority && (hasPriority = 1);\n\t\t\t\t}\n\t\t\t\tif (!harness || harnessVars) {\n\t\t\t\t\tfor (p in cleanVars) {\n\t\t\t\t\t\tif (_plugins[p] && (plugin = _checkPlugin(p, cleanVars, tween, index, target, fullTargets))) {\n\t\t\t\t\t\t\tplugin.priority && (hasPriority = 1);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tptLookup[p] = pt = _addPropTween.call(tween, target, p, \"get\", cleanVars[p], index, fullTargets, 0, vars.stringFilter);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\ttween._op && tween._op[i] && tween.kill(target, tween._op[i]);\n\t\t\t\tif (autoOverwrite && tween._pt) {\n\t\t\t\t\t_overwritingTween = tween;\n\t\t\t\t\t_globalTimeline.killTweensOf(target, ptLookup, tween.globalTime(time)); // make sure the overwriting doesn't overwrite THIS tween!!!\n\t\t\t\t\toverwritten = !tween.parent;\n\t\t\t\t\t_overwritingTween = 0;\n\t\t\t\t}\n\t\t\t\ttween._pt && lazy && (_lazyLookup[gsData.id] = 1);\n\t\t\t}\n\t\t\thasPriority && _sortPropTweensByPriority(tween);\n\t\t\ttween._onInit && tween._onInit(tween); //plugins like RoundProps must wait until ALL of the PropTweens are instantiated. In the plugin's init() function, it sets the _onInit on the tween instance. May not be pretty/intuitive, but it's fast and keeps file size down.\n\t\t}\n\t\ttween._onUpdate = onUpdate;\n\t\ttween._initted = (!tween._op || tween._pt) && !overwritten; // if overwrittenProps resulted in the entire tween being killed, do NOT flag it as initted or else it may render for one tick.\n\t\t(keyframes && time <= 0) && tl.render(_bigNum, true, true); // if there's a 0% keyframe, it'll render in the \"before\" state for any staggered/delayed animations thus when the following tween initializes, it'll use the \"before\" state instead of the \"after\" state as the initial values.\n\t},\n\t_updatePropTweens = (tween, property, value, start, startIsRelative, ratio, time, skipRecursion) => {\n\t\tlet ptCache = ((tween._pt && tween._ptCache) || (tween._ptCache = {}))[property],\n\t\t\tpt, rootPT, lookup, i;\n\t\tif (!ptCache) {\n\t\t\tptCache = tween._ptCache[property] = [];\n\t\t\tlookup = tween._ptLookup;\n\t\t\ti = tween._targets.length;\n\t\t\twhile (i--) {\n\t\t\t\tpt = lookup[i][property];\n\t\t\t\tif (pt && pt.d && pt.d._pt) { // it's a plugin, so find the nested PropTween\n\t\t\t\t\tpt = pt.d._pt;\n\t\t\t\t\twhile (pt && pt.p !== property && pt.fp !== property) { // \"fp\" is functionParam for things like setting CSS variables which require .setProperty(\"--var-name\", value)\n\t\t\t\t\t\tpt = pt._next;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!pt) { // there is no PropTween associated with that property, so we must FORCE one to be created and ditch out of this\n\t\t\t\t\t// if the tween has other properties that already rendered at new positions, we'd normally have to rewind to put them back like tween.render(0, true) before forcing an _initTween(), but that can create another edge case like tweening a timeline's progress would trigger onUpdates to fire which could move other things around. It's better to just inform users that .resetTo() should ONLY be used for tweens that already have that property. For example, you can't gsap.to(...{ y: 0 }) and then tween.restTo(\"x\", 200) for example.\n\t\t\t\t\t_forceAllPropTweens = 1; // otherwise, when we _addPropTween() and it finds no change between the start and end values, it skips creating a PropTween (for efficiency...why tween when there's no difference?) but in this case we NEED that PropTween created so we can edit it.\n\t\t\t\t\ttween.vars[property] = \"+=0\";\n\t\t\t\t\t_initTween(tween, time);\n\t\t\t\t\t_forceAllPropTweens = 0;\n\t\t\t\t\treturn skipRecursion ? _warn(property + \" not eligible for reset\") : 1; // if someone tries to do a quickTo() on a special property like borderRadius which must get split into 4 different properties, that's not eligible for .resetTo().\n\t\t\t\t}\n\t\t\t\tptCache.push(pt);\n\t\t\t}\n\t\t}\n\t\ti = ptCache.length;\n\t\twhile (i--) {\n\t\t\trootPT = ptCache[i];\n\t\t\tpt = rootPT._pt || rootPT; // complex values may have nested PropTweens. We only accommodate the FIRST value.\n\t\t\tpt.s = (start || start === 0) && !startIsRelative ? start : pt.s + (start || 0) + ratio * pt.c;\n\t\t\tpt.c = value - pt.s;\n\t\t\trootPT.e && (rootPT.e = _round(value) + getUnit(rootPT.e)); // mainly for CSSPlugin (end value)\n\t\t\trootPT.b && (rootPT.b = pt.s + getUnit(rootPT.b)); // (beginning value)\n\t\t}\n\t},\n\t_addAliasesToVars = (targets, vars) => {\n\t\tlet harness = targets[0] ? _getCache(targets[0]).harness : 0,\n\t\t\tpropertyAliases = (harness && harness.aliases),\n\t\t\tcopy, p, i, aliases;\n\t\tif (!propertyAliases) {\n\t\t\treturn vars;\n\t\t}\n\t\tcopy = _merge({}, vars);\n\t\tfor (p in propertyAliases) {\n\t\t\tif (p in copy) {\n\t\t\t\taliases = propertyAliases[p].split(\",\");\n\t\t\t\ti = aliases.length;\n\t\t\t\twhile(i--) {\n\t\t\t\t\tcopy[aliases[i]] = copy[p];\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t\treturn copy;\n\t},\n\t// parses multiple formats, like {\"0%\": {x: 100}, {\"50%\": {x: -20}} and { x: {\"0%\": 100, \"50%\": -20} }, and an \"ease\" can be set on any object. We populate an \"allProps\" object with an Array for each property, like {x: [{}, {}], y:[{}, {}]} with data for each property tween. The objects have a \"t\" (time), \"v\", (value), and \"e\" (ease) property. This allows us to piece together a timeline later.\n\t_parseKeyframe = (prop, obj, allProps, easeEach) => {\n\t\tlet ease = obj.ease || easeEach || \"power1.inOut\",\n\t\t\tp, a;\n\t\tif (_isArray(obj)) {\n\t\t\ta = allProps[prop] || (allProps[prop] = []);\n\t\t\t// t = time (out of 100), v = value, e = ease\n\t\t\tobj.forEach((value, i) => a.push({t: i / (obj.length - 1) * 100, v: value, e: ease}));\n\t\t} else {\n\t\t\tfor (p in obj) {\n\t\t\t\ta = allProps[p] || (allProps[p] = []);\n\t\t\t\tp === \"ease\" || a.push({t: parseFloat(prop), v: obj[p], e: ease});\n\t\t\t}\n\t\t}\n\t},\n\t_parseFuncOrString = (value, tween, i, target, targets) => (_isFunction(value) ? value.call(tween, i, target, targets) : (_isString(value) && ~value.indexOf(\"random(\")) ? _replaceRandom(value) : value),\n\t_staggerTweenProps = _callbackNames + \"repeat,repeatDelay,yoyo,repeatRefresh,yoyoEase,autoRevert\",\n\t_staggerPropsToSkip = {};\n_forEachName(_staggerTweenProps + \",id,stagger,delay,duration,paused,scrollTrigger\", name => _staggerPropsToSkip[name] = 1);\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/*\n * --------------------------------------------------------------------------------------\n * TWEEN\n * --------------------------------------------------------------------------------------\n */\n\nexport class Tween extends Animation {\n\n\tconstructor(targets, vars, position, skipInherit) {\n\t\tif (typeof(vars) === \"number\") {\n\t\t\tposition.duration = vars;\n\t\t\tvars = position;\n\t\t\tposition = null;\n\t\t}\n\t\tsuper(skipInherit ? vars : _inheritDefaults(vars));\n\t\tlet { duration, delay, immediateRender, stagger, overwrite, keyframes, defaults, scrollTrigger, yoyoEase } = this.vars,\n\t\t\tparent = vars.parent || _globalTimeline,\n\t\t\tparsedTargets = (_isArray(targets) || _isTypedArray(targets) ? _isNumber(targets[0]) : (\"length\" in vars)) ? [targets] : toArray(targets), // edge case: someone might try animating the \"length\" of an object with a \"length\" property that's initially set to 0 so don't interpret that as an empty Array-like object.\n\t\t\ttl, i, copy, l, p, curTarget, staggerFunc, staggerVarsToMerge;\n\t\tthis._targets = parsedTargets.length ? _harness(parsedTargets) : _warn(\"GSAP target \" + targets + \" not found. https://gsap.com\", !_config.nullTargetWarn) || [];\n\t\tthis._ptLookup = []; //PropTween lookup. An array containing an object for each target, having keys for each tweening property\n\t\tthis._overwrite = overwrite;\n\t\tif (keyframes || stagger || _isFuncOrString(duration) || _isFuncOrString(delay)) {\n\t\t\tvars = this.vars;\n\t\t\ttl = this.timeline = new Timeline({data: \"nested\", defaults: defaults || {}, targets: parent && parent.data === \"nested\" ? parent.vars.targets : parsedTargets}); // we need to store the targets because for staggers and keyframes, we end up creating an individual tween for each but function-based values need to know the index and the whole Array of targets.\n\t\t\ttl.kill();\n\t\t\ttl.parent = tl._dp = this;\n\t\t\ttl._start = 0;\n\t\t\tif (stagger || _isFuncOrString(duration) || _isFuncOrString(delay)) {\n\t\t\t\tl = parsedTargets.length;\n\t\t\t\tstaggerFunc = stagger && distribute(stagger);\n\t\t\t\tif (_isObject(stagger)) { //users can pass in callbacks like onStart/onComplete in the stagger object. These should fire with each individual tween.\n\t\t\t\t\tfor (p in stagger) {\n\t\t\t\t\t\tif (~_staggerTweenProps.indexOf(p)) {\n\t\t\t\t\t\t\tstaggerVarsToMerge || (staggerVarsToMerge = {});\n\t\t\t\t\t\t\tstaggerVarsToMerge[p] = stagger[p];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (i = 0; i < l; i++) {\n\t\t\t\t\tcopy = _copyExcluding(vars, _staggerPropsToSkip);\n\t\t\t\t\tcopy.stagger = 0;\n\t\t\t\t\tyoyoEase && (copy.yoyoEase = yoyoEase);\n\t\t\t\t\tstaggerVarsToMerge && _merge(copy, staggerVarsToMerge);\n\t\t\t\t\tcurTarget = parsedTargets[i];\n\t\t\t\t\t//don't just copy duration or delay because if they're a string or function, we'd end up in an infinite loop because _isFuncOrString() would evaluate as true in the child tweens, entering this loop, etc. So we parse the value straight from vars and default to 0.\n\t\t\t\t\tcopy.duration = +_parseFuncOrString(duration, this, i, curTarget, parsedTargets);\n\t\t\t\t\tcopy.delay = (+_parseFuncOrString(delay, this, i, curTarget, parsedTargets) || 0) - this._delay;\n\t\t\t\t\tif (!stagger && l === 1 && copy.delay) { // if someone does delay:\"random(1, 5)\", repeat:-1, for example, the delay shouldn't be inside the repeat.\n\t\t\t\t\t\tthis._delay = delay = copy.delay;\n\t\t\t\t\t\tthis._start += delay;\n\t\t\t\t\t\tcopy.delay = 0;\n\t\t\t\t\t}\n\t\t\t\t\ttl.to(curTarget, copy, staggerFunc ? staggerFunc(i, curTarget, parsedTargets) : 0);\n\t\t\t\t\ttl._ease = _easeMap.none;\n\t\t\t\t}\n\t\t\t\ttl.duration() ? (duration = delay = 0) : (this.timeline = 0); // if the timeline's duration is 0, we don't need a timeline internally!\n\t\t\t} else if (keyframes) {\n\t\t\t\t_inheritDefaults(_setDefaults(tl.vars.defaults, {ease:\"none\"}));\n\t\t\t\ttl._ease = _parseEase(keyframes.ease || vars.ease || \"none\");\n\t\t\t\tlet time = 0,\n\t\t\t\t\ta, kf, v;\n\t\t\t\tif (_isArray(keyframes)) {\n\t\t\t\t\tkeyframes.forEach(frame => tl.to(parsedTargets, frame, \">\"));\n\t\t\t\t\ttl.duration(); // to ensure tl._dur is cached because we tap into it for performance purposes in the render() method.\n\t\t\t\t} else {\n\t\t\t\t\tcopy = {};\n\t\t\t\t\tfor (p in keyframes) {\n\t\t\t\t\t\tp === \"ease\" || p === \"easeEach\" || _parseKeyframe(p, keyframes[p], copy, keyframes.easeEach);\n\t\t\t\t\t}\n\t\t\t\t\tfor (p in copy) {\n\t\t\t\t\t\ta = copy[p].sort((a, b) => a.t - b.t);\n\t\t\t\t\t\ttime = 0;\n\t\t\t\t\t\tfor (i = 0; i < a.length; i++) {\n\t\t\t\t\t\t\tkf = a[i];\n\t\t\t\t\t\t\tv = {ease: kf.e, duration: (kf.t - (i ? a[i - 1].t : 0)) / 100 * duration};\n\t\t\t\t\t\t\tv[p] = kf.v;\n\t\t\t\t\t\t\ttl.to(parsedTargets, v, time);\n\t\t\t\t\t\t\ttime += v.duration;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ttl.duration() < duration && tl.to({}, {duration: duration - tl.duration()}); // in case keyframes didn't go to 100%\n\t\t\t\t}\n\t\t\t}\n\t\t\tduration || this.duration((duration = tl.duration()));\n\n\t\t} else {\n\t\t\tthis.timeline = 0; //speed optimization, faster lookups (no going up the prototype chain)\n\t\t}\n\n\t\tif (overwrite === true && !_suppressOverwrites) {\n\t\t\t_overwritingTween = this;\n\t\t\t_globalTimeline.killTweensOf(parsedTargets);\n\t\t\t_overwritingTween = 0;\n\t\t}\n\t\t_addToTimeline(parent, this, position);\n\t\tvars.reversed && this.reverse();\n\t\tvars.paused && this.paused(true);\n\t\tif (immediateRender || (!duration && !keyframes && this._start === _roundPrecise(parent._time) && _isNotFalse(immediateRender) && _hasNoPausedAncestors(this) && parent.data !== \"nested\")) {\n\t\t\tthis._tTime = -_tinyNum; //forces a render without having to set the render() \"force\" parameter to true because we want to allow lazying by default (using the \"force\" parameter always forces an immediate full render)\n\t\t\tthis.render(Math.max(0, -delay) || 0); //in case delay is negative\n\t\t}\n\t\tscrollTrigger && _scrollTrigger(this, scrollTrigger);\n\t}\n\n\trender(totalTime, suppressEvents, force) {\n\t\tlet prevTime = this._time,\n\t\t\ttDur = this._tDur,\n\t\t\tdur = this._dur,\n\t\t\tisNegative = totalTime < 0,\n\t\t\ttTime = (totalTime > tDur - _tinyNum && !isNegative) ? tDur : (totalTime < _tinyNum) ? 0 : totalTime,\n\t\t\ttime, pt, iteration, cycleDuration, prevIteration, isYoyo, ratio, timeline, yoyoEase;\n\t\tif (!dur) {\n\t\t\t_renderZeroDurationTween(this, totalTime, suppressEvents, force);\n\t\t} else if (tTime !== this._tTime || !totalTime || force || (!this._initted && this._tTime) || (this._startAt && (this._zTime < 0) !== isNegative) || this._lazy) { // this senses if we're crossing over the start time, in which case we must record _zTime and force the render, but we do it in this lengthy conditional way for performance reasons (usually we can skip the calculations): this._initted && (this._zTime < 0) !== (totalTime < 0)\n\t\t\ttime = tTime;\n\t\t\ttimeline = this.timeline;\n\t\t\tif (this._repeat) { //adjust the time for repeats and yoyos\n\t\t\t\tcycleDuration = dur + this._rDelay;\n\t\t\t\tif (this._repeat < -1 && isNegative) {\n\t\t\t\t\treturn this.totalTime(cycleDuration * 100 + totalTime, suppressEvents, force);\n\t\t\t\t}\n\t\t\t\ttime = _roundPrecise(tTime % cycleDuration); //round to avoid floating point errors. (4 % 0.8 should be 0 but some browsers report it as 0.79999999!)\n\t\t\t\tif (tTime === tDur) { // the tDur === tTime is for edge cases where there's a lengthy decimal on the duration and it may reach the very end but the time is rendered as not-quite-there (remember, tDur is rounded to 4 decimals whereas dur isn't)\n\t\t\t\t\titeration = this._repeat;\n\t\t\t\t\ttime = dur;\n\t\t\t\t} else {\n\t\t\t\t\tprevIteration = _roundPrecise(tTime / cycleDuration); // full decimal version of iterations, not the previous iteration (we're reusing prevIteration variable for efficiency)\n\t\t\t\t\titeration = ~~prevIteration;\n\t\t\t\t\tif (iteration && iteration === prevIteration) {\n\t\t\t\t\t\ttime = dur;\n\t\t\t\t\t\titeration--;\n\t\t\t\t\t} else if (time > dur) {\n\t\t\t\t\t\ttime = dur;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tisYoyo = this._yoyo && (iteration & 1);\n\t\t\t\tif (isYoyo) {\n\t\t\t\t\tyoyoEase = this._yEase;\n\t\t\t\t\ttime = dur - time;\n\t\t\t\t}\n\t\t\t\tprevIteration = _animationCycle(this._tTime, cycleDuration);\n\t\t\t\tif (time === prevTime && !force && this._initted && iteration === prevIteration) {\n\t\t\t\t\t//could be during the repeatDelay part. No need to render and fire callbacks.\n\t\t\t\t\tthis._tTime = tTime;\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tif (iteration !== prevIteration) {\n\t\t\t\t\ttimeline && this._yEase && _propagateYoyoEase(timeline, isYoyo);\n\t\t\t\t\t//repeatRefresh functionality\n\t\t\t\t\tif (this.vars.repeatRefresh && !isYoyo && !this._lock && time !== cycleDuration && this._initted) { // this._time will === cycleDuration when we render at EXACTLY the end of an iteration. Without this condition, it'd often do the repeatRefresh render TWICE (again on the very next tick).\n\t\t\t\t\t\tthis._lock = force = 1; //force, otherwise if lazy is true, the _attemptInitTween() will return and we'll jump out and get caught bouncing on each tick.\n\t\t\t\t\t\tthis.render(_roundPrecise(cycleDuration * iteration), true).invalidate()._lock = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!this._initted) {\n\t\t\t\tif (_attemptInitTween(this, isNegative ? totalTime : time, force, suppressEvents, tTime)) {\n\t\t\t\t\tthis._tTime = 0; // in constructor if immediateRender is true, we set _tTime to -_tinyNum to have the playhead cross the starting point but we can't leave _tTime as a negative number.\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tif (prevTime !== this._time && !(force && this.vars.repeatRefresh && iteration !== prevIteration)) { // rare edge case - during initialization, an onUpdate in the _startAt (.fromTo()) might force this tween to render at a different spot in which case we should ditch this render() call so that it doesn't revert the values. But we also don't want to dump if we're doing a repeatRefresh render!\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t\tif (dur !== this._dur) { // while initting, a plugin like InertiaPlugin might alter the duration, so rerun from the start to ensure everything renders as it should.\n\t\t\t\t\treturn this.render(totalTime, suppressEvents, force);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis._tTime = tTime;\n\t\t\tthis._time = time;\n\n\t\t\tif (!this._act && this._ts) {\n\t\t\t\tthis._act = 1; //as long as it's not paused, force it to be active so that if the user renders independent of the parent timeline, it'll be forced to re-render on the next tick.\n\t\t\t\tthis._lazy = 0;\n\t\t\t}\n\n\t\t\tthis.ratio = ratio = (yoyoEase || this._ease)(time / dur);\n\t\t\tif (this._from) {\n\t\t\t\tthis.ratio = ratio = 1 - ratio;\n\t\t\t}\n\n\t\t\tif (time && !prevTime && !suppressEvents && !iteration) {\n\t\t\t\t_callback(this, \"onStart\");\n\t\t\t\tif (this._tTime !== tTime) { // in case the onStart triggered a render at a different spot, eject. Like if someone did animation.pause(0.5) or something inside the onStart.\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t}\n\t\t\tpt = this._pt;\n\t\t\twhile (pt) {\n\t\t\t\tpt.r(ratio, pt.d);\n\t\t\t\tpt = pt._next;\n\t\t\t}\n\t\t\t(timeline && timeline.render(totalTime < 0 ? totalTime : timeline._dur * timeline._ease(time / this._dur), suppressEvents, force)) || (this._startAt && (this._zTime = totalTime));\n\n\t\t\tif (this._onUpdate && !suppressEvents) {\n\t\t\t\tisNegative && _rewindStartAt(this, totalTime, suppressEvents, force); //note: for performance reasons, we tuck this conditional logic inside less traveled areas (most tweens don't have an onUpdate). We'd just have it at the end before the onComplete, but the values should be updated before any onUpdate is called, so we ALSO put it here and then if it's not called, we do so later near the onComplete.\n\t\t\t\t_callback(this, \"onUpdate\");\n\t\t\t}\n\n\t\t\tthis._repeat && iteration !== prevIteration && this.vars.onRepeat && !suppressEvents && this.parent && _callback(this, \"onRepeat\");\n\n\t\t\tif ((tTime === this._tDur || !tTime) && this._tTime === tTime) {\n\t\t\t\tisNegative && !this._onUpdate && _rewindStartAt(this, totalTime, true, true);\n\t\t\t\t(totalTime || !dur) && ((tTime === this._tDur && this._ts > 0) || (!tTime && this._ts < 0)) && _removeFromParent(this, 1); // don't remove if we're rendering at exactly a time of 0, as there could be autoRevert values that should get set on the next tick (if the playhead goes backward beyond the startTime, negative totalTime). Don't remove if the timeline is reversed and the playhead isn't at 0, otherwise tl.progress(1).reverse() won't work. Only remove if the playhead is at the end and timeScale is positive, or if the playhead is at 0 and the timeScale is negative.\n\t\t\t if (!suppressEvents && !(isNegative && !prevTime) && (tTime || prevTime || isYoyo)) { // if prevTime and tTime are zero, we shouldn't fire the onReverseComplete. This could happen if you gsap.to(... {paused:true}).play();\n\t\t\t\t\t_callback(this, (tTime === tDur ? \"onComplete\" : \"onReverseComplete\"), true);\n\t\t\t\t\tthis._prom && !(tTime < tDur && this.timeScale() > 0) && this._prom();\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t\treturn this;\n\t}\n\n\ttargets() {\n\t\treturn this._targets;\n\t}\n\n\tinvalidate(soft) { // \"soft\" gives us a way to clear out everything EXCEPT the recorded pre-\"from\" portion of from() tweens. Otherwise, for example, if you tween.progress(1).render(0, true true).invalidate(), the \"from\" values would persist and then on the next render, the from() tweens would initialize and the current value would match the \"from\" values, thus animate from the same value to the same value (no animation). We tap into this in ScrollTrigger's refresh() where we must push a tween to completion and then back again but honor its init state in case the tween is dependent on another tween further up on the page.\n\t\t(!soft || !this.vars.runBackwards) && (this._startAt = 0)\n\t\tthis._pt = this._op = this._onUpdate = this._lazy = this.ratio = 0;\n\t\tthis._ptLookup = [];\n\t\tthis.timeline && this.timeline.invalidate(soft);\n\t\treturn super.invalidate(soft);\n\t}\n\n\tresetTo(property, value, start, startIsRelative, skipRecursion) {\n\t\t_tickerActive || _ticker.wake();\n\t\tthis._ts || this.play();\n\t\tlet time = Math.min(this._dur, (this._dp._time - this._start) * this._ts),\n\t\t\tratio;\n\t\tthis._initted || _initTween(this, time);\n\t\tratio = this._ease(time / this._dur); // don't just get tween.ratio because it may not have rendered yet.\n\t\t// possible future addition to allow an object with multiple values to update, like tween.resetTo({x: 100, y: 200}); At this point, it doesn't seem worth the added kb given the fact that most users will likely opt for the convenient gsap.quickTo() way of interacting with this method.\n\t\t// if (_isObject(property)) { // performance optimization\n\t\t// \tfor (p in property) {\n\t\t// \t\tif (_updatePropTweens(this, p, property[p], value ? value[p] : null, start, ratio, time)) {\n\t\t// \t\t\treturn this.resetTo(property, value, start, startIsRelative); // if a PropTween wasn't found for the property, it'll get forced with a re-initialization so we need to jump out and start over again.\n\t\t// \t\t}\n\t\t// \t}\n\t\t// } else {\n\t\t\tif (_updatePropTweens(this, property, value, start, startIsRelative, ratio, time, skipRecursion)) {\n\t\t\t\treturn this.resetTo(property, value, start, startIsRelative, 1); // if a PropTween wasn't found for the property, it'll get forced with a re-initialization so we need to jump out and start over again.\n\t\t\t}\n\t\t//}\n\t\t_alignPlayhead(this, 0);\n\t\tthis.parent || _addLinkedListItem(this._dp, this, \"_first\", \"_last\", this._dp._sort ? \"_start\" : 0);\n\t\treturn this.render(0);\n\t}\n\n\tkill(targets, vars = \"all\") {\n\t\tif (!targets && (!vars || vars === \"all\")) {\n\t\t\tthis._lazy = this._pt = 0;\n\t\t\tthis.parent ? _interrupt(this) : this.scrollTrigger && this.scrollTrigger.kill(!!_reverting);\n\t\t\treturn this;\n\t\t}\n\t\tif (this.timeline) {\n\t\t\tlet tDur = this.timeline.totalDuration();\n\t\t\tthis.timeline.killTweensOf(targets, vars, _overwritingTween && _overwritingTween.vars.overwrite !== true)._first || _interrupt(this); // if nothing is left tweening, interrupt.\n\t\t\tthis.parent && tDur !== this.timeline.totalDuration() && _setDuration(this, this._dur * this.timeline._tDur / tDur, 0, 1); // if a nested tween is killed that changes the duration, it should affect this tween's duration. We must use the ratio, though, because sometimes the internal timeline is stretched like for keyframes where they don't all add up to whatever the parent tween's duration was set to.\n\t\t\treturn this;\n\t\t}\n\t\tlet parsedTargets = this._targets,\n\t\t\tkillingTargets = targets ? toArray(targets) : parsedTargets,\n\t\t\tpropTweenLookup = this._ptLookup,\n\t\t\tfirstPT = this._pt,\n\t\t\toverwrittenProps, curLookup, curOverwriteProps, props, p, pt, i;\n\t\tif ((!vars || vars === \"all\") && _arraysMatch(parsedTargets, killingTargets)) {\n\t\t\tvars === \"all\" && (this._pt = 0);\n\t\t\treturn _interrupt(this);\n\t\t}\n\t\toverwrittenProps = this._op = this._op || [];\n\t\tif (vars !== \"all\") { //so people can pass in a comma-delimited list of property names\n\t\t\tif (_isString(vars)) {\n\t\t\t\tp = {};\n\t\t\t\t_forEachName(vars, name => p[name] = 1);\n\t\t\t\tvars = p;\n\t\t\t}\n\t\t\tvars = _addAliasesToVars(parsedTargets, vars);\n\t\t}\n\t\ti = parsedTargets.length;\n\t\twhile (i--) {\n\t\t\tif (~killingTargets.indexOf(parsedTargets[i])) {\n\t\t\t\tcurLookup = propTweenLookup[i];\n\t\t\t\tif (vars === \"all\") {\n\t\t\t\t\toverwrittenProps[i] = vars;\n\t\t\t\t\tprops = curLookup;\n\t\t\t\t\tcurOverwriteProps = {};\n\t\t\t\t} else {\n\t\t\t\t\tcurOverwriteProps = overwrittenProps[i] = overwrittenProps[i] || {};\n\t\t\t\t\tprops = vars;\n\t\t\t\t}\n\t\t\t\tfor (p in props) {\n\t\t\t\t\tpt = curLookup && curLookup[p];\n\t\t\t\t\tif (pt) {\n\t\t\t\t\t\tif (!(\"kill\" in pt.d) || pt.d.kill(p) === true) {\n\t\t\t\t\t\t\t_removeLinkedListItem(this, pt, \"_pt\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdelete curLookup[p];\n\t\t\t\t\t}\n\t\t\t\t\tif (curOverwriteProps !== \"all\") {\n\t\t\t\t\t\tcurOverwriteProps[p] = 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis._initted && !this._pt && firstPT && _interrupt(this); //if all tweening properties are killed, kill the tween. Without this line, if there's a tween with multiple targets and then you killTweensOf() each target individually, the tween would technically still remain active and fire its onComplete even though there aren't any more properties tweening.\n\t\treturn this;\n\t}\n\n\n\tstatic to(targets, vars) {\n\t\treturn new Tween(targets, vars, arguments[2]);\n\t}\n\n\tstatic from(targets, vars) {\n\t\treturn _createTweenType(1, arguments);\n\t}\n\n\tstatic delayedCall(delay, callback, params, scope) {\n\t\treturn new Tween(callback, 0, {immediateRender:false, lazy:false, overwrite:false, delay:delay, onComplete:callback, onReverseComplete:callback, onCompleteParams:params, onReverseCompleteParams:params, callbackScope:scope}); // we must use onReverseComplete too for things like timeline.add(() => {...}) which should be triggered in BOTH directions (forward and reverse)\n\t}\n\n\tstatic fromTo(targets, fromVars, toVars) {\n\t\treturn _createTweenType(2, arguments);\n\t}\n\n\tstatic set(targets, vars) {\n\t\tvars.duration = 0;\n\t\tvars.repeatDelay || (vars.repeat = 0);\n\t\treturn new Tween(targets, vars);\n\t}\n\n\tstatic killTweensOf(targets, props, onlyActive) {\n\t\treturn _globalTimeline.killTweensOf(targets, props, onlyActive);\n\t}\n}\n\n_setDefaults(Tween.prototype, {_targets:[], _lazy:0, _startAt:0, _op:0, _onInit:0});\n\n//add the pertinent timeline methods to Tween instances so that users can chain conveniently and create a timeline automatically. (removed due to concerns that it'd ultimately add to more confusion especially for beginners)\n// _forEachName(\"to,from,fromTo,set,call,add,addLabel,addPause\", name => {\n// \tTween.prototype[name] = function() {\n// \t\tlet tl = new Timeline();\n// \t\treturn _addToTimeline(tl, this)[name].apply(tl, toArray(arguments));\n// \t}\n// });\n\n//for backward compatibility. Leverage the timeline calls.\n_forEachName(\"staggerTo,staggerFrom,staggerFromTo\", name => {\n\tTween[name] = function() {\n\t\tlet tl = new Timeline(),\n\t\t\tparams = _slice.call(arguments, 0);\n\t\tparams.splice(name === \"staggerFromTo\" ? 5 : 4, 0, 0);\n\t\treturn tl[name].apply(tl, params);\n\t}\n});\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/*\n * --------------------------------------------------------------------------------------\n * PROPTWEEN\n * --------------------------------------------------------------------------------------\n */\nlet _setterPlain = (target, property, value) => target[property] = value,\n\t_setterFunc = (target, property, value) => target[property](value),\n\t_setterFuncWithParam = (target, property, value, data) => target[property](data.fp, value),\n\t_setterAttribute = (target, property, value) => target.setAttribute(property, value),\n\t_getSetter = (target, property) => _isFunction(target[property]) ? _setterFunc : _isUndefined(target[property]) && target.setAttribute ? _setterAttribute : _setterPlain,\n\t_renderPlain = (ratio, data) => data.set(data.t, data.p, Math.round((data.s + data.c * ratio) * 1000000) / 1000000, data),\n\t_renderBoolean = (ratio, data) => data.set(data.t, data.p, !!(data.s + data.c * ratio), data),\n\t_renderComplexString = function(ratio, data) {\n\t\tlet pt = data._pt,\n\t\t\ts = \"\";\n\t\tif (!ratio && data.b) { //b = beginning string\n\t\t\ts = data.b;\n\t\t} else if (ratio === 1 && data.e) { //e = ending string\n\t\t\ts = data.e;\n\t\t} else {\n\t\t\twhile (pt) {\n\t\t\t\ts = pt.p + (pt.m ? pt.m(pt.s + pt.c * ratio) : (Math.round((pt.s + pt.c * ratio) * 10000) / 10000)) + s; //we use the \"p\" property for the text inbetween (like a suffix). And in the context of a complex string, the modifier (m) is typically just Math.round(), like for RGB colors.\n\t\t\t\tpt = pt._next;\n\t\t\t}\n\t\t\ts += data.c; //we use the \"c\" of the PropTween to store the final chunk of non-numeric text.\n\t\t}\n\t\tdata.set(data.t, data.p, s, data);\n\t},\n\t_renderPropTweens = function(ratio, data) {\n\t\tlet pt = data._pt;\n\t\twhile (pt) {\n\t\t\tpt.r(ratio, pt.d);\n\t\t\tpt = pt._next;\n\t\t}\n\t},\n\t_addPluginModifier = function(modifier, tween, target, property) {\n\t\tlet pt = this._pt,\n\t\t\tnext;\n\t\twhile (pt) {\n\t\t\tnext = pt._next;\n\t\t\tpt.p === property && pt.modifier(modifier, tween, target);\n\t\t\tpt = next;\n\t\t}\n\t},\n\t_killPropTweensOf = function(property) {\n\t\tlet pt = this._pt,\n\t\t\thasNonDependentRemaining, next;\n\t\twhile (pt) {\n\t\t\tnext = pt._next;\n\t\t\tif ((pt.p === property && !pt.op) || pt.op === property) {\n\t\t\t\t_removeLinkedListItem(this, pt, \"_pt\");\n\t\t\t} else if (!pt.dep) {\n\t\t\t\thasNonDependentRemaining = 1;\n\t\t\t}\n\t\t\tpt = next;\n\t\t}\n\t\treturn !hasNonDependentRemaining;\n\t},\n\t_setterWithModifier = (target, property, value, data) => {\n\t\tdata.mSet(target, property, data.m.call(data.tween, value, data.mt), data);\n\t},\n\t_sortPropTweensByPriority = parent => {\n\t\tlet pt = parent._pt,\n\t\t\tnext, pt2, first, last;\n\t\t//sorts the PropTween linked list in order of priority because some plugins need to do their work after ALL of the PropTweens were created (like RoundPropsPlugin and ModifiersPlugin)\n\t\twhile (pt) {\n\t\t\tnext = pt._next;\n\t\t\tpt2 = first;\n\t\t\twhile (pt2 && pt2.pr > pt.pr) {\n\t\t\t\tpt2 = pt2._next;\n\t\t\t}\n\t\t\tif ((pt._prev = pt2 ? pt2._prev : last)) {\n\t\t\t\tpt._prev._next = pt;\n\t\t\t} else {\n\t\t\t\tfirst = pt;\n\t\t\t}\n\t\t\tif ((pt._next = pt2)) {\n\t\t\t\tpt2._prev = pt;\n\t\t\t} else {\n\t\t\t\tlast = pt;\n\t\t\t}\n\t\t\tpt = next;\n\t\t}\n\t\tparent._pt = first;\n\t};\n\n//PropTween key: t = target, p = prop, r = renderer, d = data, s = start, c = change, op = overwriteProperty (ONLY populated when it's different than p), pr = priority, _next/_prev for the linked list siblings, set = setter, m = modifier, mSet = modifierSetter (the original setter, before a modifier was added)\nexport class PropTween {\n\n\tconstructor(next, target, prop, start, change, renderer, data, setter, priority) {\n\t\tthis.t = target;\n\t\tthis.s = start;\n\t\tthis.c = change;\n\t\tthis.p = prop;\n\t\tthis.r = renderer || _renderPlain;\n\t\tthis.d = data || this;\n\t\tthis.set = setter || _setterPlain;\n\t\tthis.pr = priority || 0;\n\t\tthis._next = next;\n\t\tif (next) {\n\t\t\tnext._prev = this;\n\t\t}\n\t}\n\n\tmodifier(func, tween, target) {\n\t\tthis.mSet = this.mSet || this.set; //in case it was already set (a PropTween can only have one modifier)\n\t\tthis.set = _setterWithModifier;\n\t\tthis.m = func;\n\t\tthis.mt = target; //modifier target\n\t\tthis.tween = tween;\n\t}\n}\n\n\n\n//Initialization tasks\n_forEachName(_callbackNames + \"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger\", name => _reservedProps[name] = 1);\n_globals.TweenMax = _globals.TweenLite = Tween;\n_globals.TimelineLite = _globals.TimelineMax = Timeline;\n_globalTimeline = new Timeline({sortChildren: false, defaults: _defaults, autoRemoveChildren: true, id:\"root\", smoothChildTiming: true});\n_config.stringFilter = _colorStringFilter;\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nlet _media = [],\n\t_listeners = {},\n\t_emptyArray = [],\n\t_lastMediaTime = 0,\n\t_contextID = 0,\n\t_dispatch = type => (_listeners[type] || _emptyArray).map(f => f()),\n\t_onMediaChange = () => {\n\t\tlet time = Date.now(),\n\t\t\tmatches = [];\n\t\tif (time - _lastMediaTime > 2) {\n\t\t\t_dispatch(\"matchMediaInit\");\n\t\t\t_media.forEach(c => {\n\t\t\t\tlet queries = c.queries,\n\t\t\t\t\tconditions = c.conditions,\n\t\t\t\t\tmatch, p, anyMatch, toggled;\n\t\t\t\tfor (p in queries) {\n\t\t\t\t\tmatch = _win.matchMedia(queries[p]).matches; // Firefox doesn't update the \"matches\" property of the MediaQueryList object correctly - it only does so as it calls its change handler - so we must re-create a media query here to ensure it's accurate.\n\t\t\t\t\tmatch && (anyMatch = 1);\n\t\t\t\t\tif (match !== conditions[p]) {\n\t\t\t\t\t\tconditions[p] = match;\n\t\t\t\t\t\ttoggled = 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (toggled) {\n\t\t\t\t\tc.revert();\n\t\t\t\t\tanyMatch && matches.push(c);\n\t\t\t\t}\n\t\t\t});\n\t\t\t_dispatch(\"matchMediaRevert\");\n\t\t\tmatches.forEach(c => c.onMatch(c, func => c.add(null, func)));\n\t\t\t_lastMediaTime = time;\n\t\t\t_dispatch(\"matchMedia\");\n\t\t}\n\t};\n\nclass Context {\n\tconstructor(func, scope) {\n\t\tthis.selector = scope && selector(scope);\n\t\tthis.data = [];\n\t\tthis._r = []; // returned/cleanup functions\n\t\tthis.isReverted = false;\n\t\tthis.id = _contextID++; // to work around issues that frameworks like Vue cause by making things into Proxies which make it impossible to do something like _media.indexOf(this) because \"this\" would no longer refer to the Context instance itself - it'd refer to a Proxy! We needed a way to identify the context uniquely\n\t\tfunc && this.add(func);\n\t}\n\tadd(name, func, scope) {\n\t\t// possible future addition if we need the ability to add() an animation to a context and for whatever reason cannot create that animation inside of a context.add(() => {...}) function.\n\t\t// if (name && _isFunction(name.revert)) {\n\t\t// \tthis.data.push(name);\n\t\t// \treturn (name._ctx = this);\n\t\t// }\n\t\tif (_isFunction(name)) {\n\t\t\tscope = func;\n\t\t\tfunc = name;\n\t\t\tname = _isFunction;\n\t\t}\n\t\tlet self = this,\n\t\t\tf = function() {\n\t\t\t\tlet prev = _context,\n\t\t\t\t\tprevSelector = self.selector,\n\t\t\t\t\tresult;\n\t\t\t\tprev && prev !== self && prev.data.push(self);\n\t\t\t\tscope && (self.selector = selector(scope));\n\t\t\t\t_context = self;\n\t\t\t\tresult = func.apply(self, arguments);\n\t\t\t\t_isFunction(result) && self._r.push(result);\n\t\t\t\t_context = prev;\n\t\t\t\tself.selector = prevSelector;\n\t\t\t\tself.isReverted = false;\n\t\t\t\treturn result;\n\t\t\t};\n\t\tself.last = f;\n\t\treturn name === _isFunction ? f(self, func => self.add(null, func)) : name ? (self[name] = f) : f;\n\t}\n\tignore(func) {\n\t\tlet prev = _context;\n\t\t_context = null;\n\t\tfunc(this);\n\t\t_context = prev;\n\t}\n\tgetTweens() {\n\t\tlet a = [];\n\t\tthis.data.forEach(e => (e instanceof Context) ? a.push(...e.getTweens()) : (e instanceof Tween) && !(e.parent && e.parent.data === \"nested\") && a.push(e));\n\t\treturn a;\n\t}\n\tclear() {\n\t\tthis._r.length = this.data.length = 0;\n\t}\n\tkill(revert, matchMedia) {\n\t\tif (revert) {\n\t\t\tlet tweens = this.getTweens(),\n\t\t\t\ti = this.data.length,\n\t\t\t\tt;\n\t\t\twhile (i--) { // Flip plugin tweens are very different in that they should actually be pushed to their end. The plugin replaces the timeline's .revert() method to do exactly that. But we also need to remove any of those nested tweens inside the flip timeline so that they don't get individually reverted.\n\t\t\t\tt = this.data[i];\n\t\t\t\tif (t.data === \"isFlip\") {\n\t\t\t\t\tt.revert();\n\t\t\t\t\tt.getChildren(true, true, false).forEach(tween => tweens.splice(tweens.indexOf(tween), 1));\n\t\t\t\t}\n\t\t\t}\n\t\t\t// save as an object so that we can cache the globalTime for each tween to optimize performance during the sort\n\t\t\ttweens.map(t => { return {g: t._dur || t._delay || (t._sat && !t._sat.vars.immediateRender) ? t.globalTime(0) : -Infinity, t}}).sort((a, b) => b.g - a.g || -Infinity).forEach(o => o.t.revert(revert)); // note: all of the _startAt tweens should be reverted in reverse order that they were created, and they'll all have the same globalTime (-1) so the \" || -1\" in the sort keeps the order properly.\n\t\t\ti = this.data.length;\n\t\t\twhile (i--) { // make sure we loop backwards so that, for example, SplitTexts that were created later on the same element get reverted first\n\t\t\t\tt = this.data[i];\n\t\t\t\tif (t instanceof Timeline) {\n\t\t\t\t\tif (t.data !== \"nested\") {\n\t\t\t\t\t\tt.scrollTrigger && t.scrollTrigger.revert();\n\t\t\t\t\t\tt.kill(); // don't revert() the timeline because that's duplicating efforts since we already reverted all the tweens\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t!(t instanceof Tween) && t.revert && t.revert(revert)\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis._r.forEach(f => f(revert, this));\n\t\t\tthis.isReverted = true;\n\t\t} else {\n\t\t\tthis.data.forEach(e => e.kill && e.kill());\n\t\t}\n\t\tthis.clear();\n\t\tif (matchMedia) {\n\t\t\tlet i = _media.length;\n\t\t\twhile (i--) { // previously, we checked _media.indexOf(this), but some frameworks like Vue enforce Proxy objects that make it impossible to get the proper result that way, so we must use a unique ID number instead.\n\t\t\t\t_media[i].id === this.id && _media.splice(i, 1);\n\t\t\t}\n\t\t}\n\t}\n\n\t// killWithCleanup() {\n\t// \tthis.kill();\n\t// \tthis._r.forEach(f => f(false, this));\n\t// }\n\n\trevert(config) {\n\t\tthis.kill(config || {});\n\t}\n}\n\n\n\n\nclass MatchMedia {\n\tconstructor(scope) {\n\t\tthis.contexts = [];\n\t\tthis.scope = scope;\n\t\t_context && _context.data.push(this);\n\t}\n\tadd(conditions, func, scope) {\n\t\t_isObject(conditions) || (conditions = {matches: conditions});\n\t\tlet context = new Context(0, scope || this.scope),\n\t\t\tcond = context.conditions = {},\n\t\t\tmq, p, active;\n\t\t_context && !context.selector && (context.selector = _context.selector); // in case a context is created inside a context. Like a gsap.matchMedia() that's inside a scoped gsap.context()\n\t\tthis.contexts.push(context);\n\t\tfunc = context.add(\"onMatch\", func);\n\t\tcontext.queries = conditions;\n\t\tfor (p in conditions) {\n\t\t\tif (p === \"all\") {\n\t\t\t\tactive = 1;\n\t\t\t} else {\n\t\t\t\tmq = _win.matchMedia(conditions[p]);\n\t\t\t\tif (mq) {\n\t\t\t\t\t_media.indexOf(context) < 0 && _media.push(context);\n\t\t\t\t\t(cond[p] = mq.matches) && (active = 1);\n\t\t\t\t\tmq.addListener ? mq.addListener(_onMediaChange) : mq.addEventListener(\"change\", _onMediaChange);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tactive && func(context, f => context.add(null, f));\n\t\treturn this;\n\t}\n\t// refresh() {\n\t// \tlet time = _lastMediaTime,\n\t// \t\tmedia = _media;\n\t// \t_lastMediaTime = -1;\n\t// \t_media = this.contexts;\n\t// \t_onMediaChange();\n\t// \t_lastMediaTime = time;\n\t// \t_media = media;\n\t// }\n\trevert(config) {\n\t\tthis.kill(config || {});\n\t}\n\tkill(revert) {\n\t\tthis.contexts.forEach(c => c.kill(revert, true));\n\t}\n}\n\n\n\n/*\n * --------------------------------------------------------------------------------------\n * GSAP\n * --------------------------------------------------------------------------------------\n */\nconst _gsap = {\n\tregisterPlugin(...args) {\n\t\targs.forEach(config => _createPlugin(config));\n\t},\n\ttimeline(vars) {\n\t\treturn new Timeline(vars);\n\t},\n\tgetTweensOf(targets, onlyActive) {\n\t\treturn _globalTimeline.getTweensOf(targets, onlyActive);\n\t},\n\tgetProperty(target, property, unit, uncache) {\n\t\t_isString(target) && (target = toArray(target)[0]); //in case selector text or an array is passed in\n\t\tlet getter = _getCache(target || {}).get,\n\t\t\tformat = unit ? _passThrough : _numericIfPossible;\n\t\tunit === \"native\" && (unit = \"\");\n\t\treturn !target ? target : !property ? (property, unit, uncache) => format(((_plugins[property] && _plugins[property].get) || getter)(target, property, unit, uncache)) : format(((_plugins[property] && _plugins[property].get) || getter)(target, property, unit, uncache));\n\t},\n\tquickSetter(target, property, unit) {\n\t\ttarget = toArray(target);\n\t\tif (target.length > 1) {\n\t\t\tlet setters = target.map(t => gsap.quickSetter(t, property, unit)),\n\t\t\t\tl = setters.length;\n\t\t\treturn value => {\n\t\t\t\tlet i = l;\n\t\t\t\twhile(i--) {\n\t\t\t\t\tsetters[i](value);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\ttarget = target[0] || {};\n\t\tlet Plugin = _plugins[property],\n\t\t\tcache = _getCache(target),\n\t\t\tp = (cache.harness && (cache.harness.aliases || {})[property]) || property, // in case it's an alias, like \"rotate\" for \"rotation\".\n\t\t\tsetter = Plugin ? value => {\n\t\t\t\tlet p = new Plugin();\n\t\t\t\t_quickTween._pt = 0;\n\t\t\t\tp.init(target, unit ? value + unit : value, _quickTween, 0, [target]);\n\t\t\t\tp.render(1, p);\n\t\t\t\t_quickTween._pt && _renderPropTweens(1, _quickTween);\n\t\t\t} : cache.set(target, p);\n\t\treturn Plugin ? setter : value => setter(target, p, unit ? value + unit : value, cache, 1);\n\t},\n\tquickTo(target, property, vars) {\n\t\tlet tween = gsap.to(target, _setDefaults({[property]: \"+=0.1\", paused: true, stagger: 0}, vars || {})),\n\t\t\tfunc = (value, start, startIsRelative) => tween.resetTo(property, value, start, startIsRelative);\n\t\tfunc.tween = tween;\n\t\treturn func;\n\t},\n\tisTweening(targets) {\n\t\treturn _globalTimeline.getTweensOf(targets, true).length > 0;\n\t},\n\tdefaults(value) {\n\t\tvalue && value.ease && (value.ease = _parseEase(value.ease, _defaults.ease));\n\t\treturn _mergeDeep(_defaults, value || {});\n\t},\n\tconfig(value) {\n\t\treturn _mergeDeep(_config, value || {});\n\t},\n\tregisterEffect({name, effect, plugins, defaults, extendTimeline}) {\n\t\t(plugins || \"\").split(\",\").forEach(pluginName => pluginName && !_plugins[pluginName] && !_globals[pluginName] && _warn(name + \" effect requires \" + pluginName + \" plugin.\"));\n\t\t_effects[name] = (targets, vars, tl) => effect(toArray(targets), _setDefaults(vars || {}, defaults), tl);\n\t\tif (extendTimeline) {\n\t\t\tTimeline.prototype[name] = function(targets, vars, position) {\n\t\t\t\treturn this.add(_effects[name](targets, _isObject(vars) ? vars : (position = vars) && {}, this), position);\n\t\t\t};\n\t\t}\n\t},\n\tregisterEase(name, ease) {\n\t\t_easeMap[name] = _parseEase(ease);\n\t},\n\tparseEase(ease, defaultEase) {\n\t\treturn arguments.length ? _parseEase(ease, defaultEase) : _easeMap;\n\t},\n\tgetById(id) {\n\t\treturn _globalTimeline.getById(id);\n\t},\n\texportRoot(vars = {}, includeDelayedCalls) {\n\t\tlet tl = new Timeline(vars),\n\t\t\tchild, next;\n\t\ttl.smoothChildTiming = _isNotFalse(vars.smoothChildTiming);\n\t\t_globalTimeline.remove(tl);\n\t\ttl._dp = 0; //otherwise it'll get re-activated when adding children and be re-introduced into _globalTimeline's linked list (then added to itself).\n\t\ttl._time = tl._tTime = _globalTimeline._time;\n\t\tchild = _globalTimeline._first;\n\t\twhile (child) {\n\t\t\tnext = child._next;\n\t\t\tif (includeDelayedCalls || !(!child._dur && child instanceof Tween && child.vars.onComplete === child._targets[0])) {\n\t\t\t\t_addToTimeline(tl, child, child._start - child._delay);\n\t\t\t}\n\t\t\tchild = next;\n\t\t}\n\t\t_addToTimeline(_globalTimeline, tl, 0);\n\t\treturn tl;\n\t},\n\tcontext: (func, scope) => func ? new Context(func, scope) : _context,\n\tmatchMedia: scope => new MatchMedia(scope),\n\tmatchMediaRefresh: () => _media.forEach(c => {\n\t\tlet cond = c.conditions,\n\t\t\tfound, p;\n\t\tfor (p in cond) {\n\t\t\tif (cond[p]) {\n\t\t\t\tcond[p] = false;\n\t\t\t\tfound = 1;\n\t\t\t}\n\t\t}\n\t\tfound && c.revert();\n\t}) || _onMediaChange(),\n\taddEventListener(type, callback) {\n\t\tlet a = _listeners[type] || (_listeners[type] = []);\n\t\t~a.indexOf(callback) || a.push(callback);\n\t},\n\tremoveEventListener(type, callback) {\n\t\tlet a = _listeners[type],\n\t\t\ti = a && a.indexOf(callback);\n\t\ti >= 0 && a.splice(i, 1);\n\t},\n\tutils: { wrap, wrapYoyo, distribute, random, snap, normalize, getUnit, clamp, splitColor, toArray, selector, mapRange, pipe, unitize, interpolate, shuffle },\n\tinstall: _install,\n\teffects: _effects,\n\tticker: _ticker,\n\tupdateRoot: Timeline.updateRoot,\n\tplugins: _plugins,\n\tglobalTimeline: _globalTimeline,\n\tcore: {PropTween, globals: _addGlobal, Tween, Timeline, Animation, getCache: _getCache, _removeLinkedListItem, reverting: () => _reverting, context: toAdd => {if (toAdd && _context) { _context.data.push(toAdd); toAdd._ctx = _context} return _context; }, suppressOverwrites: value => _suppressOverwrites = value}\n};\n\n_forEachName(\"to,from,fromTo,delayedCall,set,killTweensOf\", name => _gsap[name] = Tween[name]);\n_ticker.add(Timeline.updateRoot);\n_quickTween = _gsap.to({}, {duration:0});\n\n\n\n\n// ---- EXTRA PLUGINS --------------------------------------------------------\n\n\nlet _getPluginPropTween = (plugin, prop) => {\n\t\tlet pt = plugin._pt;\n\t\twhile (pt && pt.p !== prop && pt.op !== prop && pt.fp !== prop) {\n\t\t\tpt = pt._next;\n\t\t}\n\t\treturn pt;\n\t},\n\t_addModifiers = (tween, modifiers) => {\n\t\t\tlet\ttargets = tween._targets,\n\t\t\t\tp, i, pt;\n\t\t\tfor (p in modifiers) {\n\t\t\t\ti = targets.length;\n\t\t\t\twhile (i--) {\n\t\t\t\t\tpt = tween._ptLookup[i][p];\n\t\t\t\t\tif (pt && (pt = pt.d)) {\n\t\t\t\t\t\tif (pt._pt) { // is a plugin\n\t\t\t\t\t\t\tpt = _getPluginPropTween(pt, p);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpt && pt.modifier && pt.modifier(modifiers[p], tween, targets[i], p);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t},\n\t_buildModifierPlugin = (name, modifier) => {\n\t\treturn {\n\t\t\tname: name,\n\t\t\trawVars: 1, //don't pre-process function-based values or \"random()\" strings.\n\t\t\tinit(target, vars, tween) {\n\t\t\t\ttween._onInit = tween => {\n\t\t\t\t\tlet temp, p;\n\t\t\t\t\tif (_isString(vars)) {\n\t\t\t\t\t\ttemp = {};\n\t\t\t\t\t\t_forEachName(vars, name => temp[name] = 1); //if the user passes in a comma-delimited list of property names to roundProps, like \"x,y\", we round to whole numbers.\n\t\t\t\t\t\tvars = temp;\n\t\t\t\t\t}\n\t\t\t\t\tif (modifier) {\n\t\t\t\t\t\ttemp = {};\n\t\t\t\t\t\tfor (p in vars) {\n\t\t\t\t\t\t\ttemp[p] = modifier(vars[p]);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvars = temp;\n\t\t\t\t\t}\n\t\t\t\t\t_addModifiers(tween, vars);\n\t\t\t\t};\n\t\t\t}\n\t\t};\n\t};\n\n//register core plugins\nexport const gsap = _gsap.registerPlugin({\n\t\tname:\"attr\",\n\t\tinit(target, vars, tween, index, targets) {\n\t\t\tlet p, pt, v;\n\t\t\tthis.tween = tween;\n\t\t\tfor (p in vars) {\n\t\t\t\tv = target.getAttribute(p) || \"\";\n\t\t\t\tpt = this.add(target, \"setAttribute\", (v || 0) + \"\", vars[p], index, targets, 0, 0, p);\n\t\t\t\tpt.op = p;\n\t\t\t\tpt.b = v; // record the beginning value so we can revert()\n\t\t\t\tthis._props.push(p);\n\t\t\t}\n\t\t},\n\t\trender(ratio, data) {\n\t\t\tlet pt = data._pt;\n\t\t\twhile (pt) {\n\t\t\t\t_reverting ? pt.set(pt.t, pt.p, pt.b, pt) : pt.r(ratio, pt.d); // if reverting, go back to the original (pt.b)\n\t\t\t\tpt = pt._next;\n\t\t\t}\n\t\t}\n\t}, {\n\t\tname:\"endArray\",\n\t\tinit(target, value) {\n\t\t\tlet i = value.length;\n\t\t\twhile (i--) {\n\t\t\t\tthis.add(target, i, target[i] || 0, value[i], 0, 0, 0, 0, 0, 1);\n\t\t\t}\n\t\t}\n\t},\n\t_buildModifierPlugin(\"roundProps\", _roundModifier),\n\t_buildModifierPlugin(\"modifiers\"),\n\t_buildModifierPlugin(\"snap\", snap)\n) || _gsap; //to prevent the core plugins from being dropped via aggressive tree shaking, we must include them in the variable declaration in this way.\n\nTween.version = Timeline.version = gsap.version = \"3.12.7\";\n_coreReady = 1;\n_windowExists() && _wake();\n\nexport const { Power0, Power1, Power2, Power3, Power4, Linear, Quad, Cubic, Quart, Quint, Strong, Elastic, Back, SteppedEase, Bounce, Sine, Expo, Circ } = _easeMap;\nexport { Tween as TweenMax, Tween as TweenLite, Timeline as TimelineMax, Timeline as TimelineLite, gsap as default, wrap, wrapYoyo, distribute, random, snap, normalize, getUnit, clamp, splitColor, toArray, selector, mapRange, pipe, unitize, interpolate, shuffle };\n//export some internal methods/orojects for use in CSSPlugin so that we can externalize that file and allow custom builds that exclude it.\nexport { _getProperty, _numExp, _numWithUnitExp, _isString, _isUndefined, _renderComplexString, _relExp, _setDefaults, _removeLinkedListItem, _forEachName, _sortPropTweensByPriority, _colorStringFilter, _replaceRandom, _checkPlugin, _plugins, _ticker, _config, _roundModifier, _round, _missingPlugin, _getSetter, _getCache, _colorExp, _parseRelative }","/*!\n * CSSPlugin 3.12.7\n * https://gsap.com\n *\n * Copyright 2008-2025, GreenSock. All rights reserved.\n * Subject to the terms at https://gsap.com/standard-license or for\n * Club GSAP members, the agreement issued with that membership.\n * @author: Jack Doyle, jack@greensock.com\n*/\n/* eslint-disable */\n\nimport {gsap, _getProperty, _numExp, _numWithUnitExp, getUnit, _isString, _isUndefined, _renderComplexString, _relExp, _forEachName, _sortPropTweensByPriority, _colorStringFilter, _checkPlugin, _replaceRandom, _plugins, GSCache, PropTween, _config, _ticker, _round, _missingPlugin, _getSetter, _getCache, _colorExp, _parseRelative,\n\t_setDefaults, _removeLinkedListItem //for the commented-out className feature.\n} from \"./gsap-core.js\";\n\nlet _win, _doc, _docElement, _pluginInitted, _tempDiv, _tempDivStyler, _recentSetterPlugin, _reverting,\n\t_windowExists = () => typeof(window) !== \"undefined\",\n\t_transformProps = {},\n\t_RAD2DEG = 180 / Math.PI,\n\t_DEG2RAD = Math.PI / 180,\n\t_atan2 = Math.atan2,\n\t_bigNum = 1e8,\n\t_capsExp = /([A-Z])/g,\n\t_horizontalExp = /(left|right|width|margin|padding|x)/i,\n\t_complexExp = /[\\s,\\(]\\S/,\n\t_propertyAliases = {autoAlpha:\"opacity,visibility\", scale:\"scaleX,scaleY\", alpha:\"opacity\"},\n\t_renderCSSProp = (ratio, data) => data.set(data.t, data.p, (Math.round((data.s + data.c * ratio) * 10000) / 10000) + data.u, data),\n\t_renderPropWithEnd = (ratio, data) => data.set(data.t, data.p, ratio === 1 ? data.e : (Math.round((data.s + data.c * ratio) * 10000) / 10000) + data.u, data),\n\t_renderCSSPropWithBeginning = (ratio, data) => data.set(data.t, data.p, ratio ? (Math.round((data.s + data.c * ratio) * 10000) / 10000) + data.u : data.b, data), //if units change, we need a way to render the original unit/value when the tween goes all the way back to the beginning (ratio:0)\n\t_renderRoundedCSSProp = (ratio, data) => {\n\t\tlet value = data.s + data.c * ratio;\n\t\tdata.set(data.t, data.p, ~~(value + (value < 0 ? -.5 : .5)) + data.u, data);\n\t},\n\t_renderNonTweeningValue = (ratio, data) => data.set(data.t, data.p, ratio ? data.e : data.b, data),\n\t_renderNonTweeningValueOnlyAtEnd = (ratio, data) => data.set(data.t, data.p, ratio !== 1 ? data.b : data.e, data),\n\t_setterCSSStyle = (target, property, value) => target.style[property] = value,\n\t_setterCSSProp = (target, property, value) => target.style.setProperty(property, value),\n\t_setterTransform = (target, property, value) => target._gsap[property] = value,\n\t_setterScale = (target, property, value) => target._gsap.scaleX = target._gsap.scaleY = value,\n\t_setterScaleWithRender = (target, property, value, data, ratio) => {\n\t\tlet cache = target._gsap;\n\t\tcache.scaleX = cache.scaleY = value;\n\t\tcache.renderTransform(ratio, cache);\n\t},\n\t_setterTransformWithRender = (target, property, value, data, ratio) => {\n\t\tlet cache = target._gsap;\n\t\tcache[property] = value;\n\t\tcache.renderTransform(ratio, cache);\n\t},\n\t_transformProp = \"transform\",\n\t_transformOriginProp = _transformProp + \"Origin\",\n\t_saveStyle = function(property, isNotCSS) {\n\t\tlet target = this.target,\n\t\t\tstyle = target.style,\n\t\t\tcache = target._gsap;\n\t\tif ((property in _transformProps) && style) {\n\t\t\tthis.tfm = this.tfm || {};\n\t\t\tif (property !== \"transform\") {\n\t\t\t\tproperty = _propertyAliases[property] || property;\n\t\t\t\t~property.indexOf(\",\") ? property.split(\",\").forEach(a => this.tfm[a] = _get(target, a)) : (this.tfm[property] = cache.x ? cache[property] : _get(target, property)); // note: scale would map to \"scaleX,scaleY\", thus we loop and apply them both.\n\t\t\t\tproperty === _transformOriginProp && (this.tfm.zOrigin = cache.zOrigin);\n\t\t\t} else {\n\t\t\t\treturn _propertyAliases.transform.split(\",\").forEach(p => _saveStyle.call(this, p, isNotCSS));\n\t\t\t}\n\t\t\tif (this.props.indexOf(_transformProp) >= 0) { return; }\n\t\t\tif (cache.svg) {\n\t\t\t\tthis.svgo = target.getAttribute(\"data-svg-origin\");\n\t\t\t\tthis.props.push(_transformOriginProp, isNotCSS, \"\");\n\t\t\t}\n\t\t\tproperty = _transformProp;\n\t\t}\n\t\t(style || isNotCSS) && this.props.push(property, isNotCSS, style[property]);\n\t},\n\t_removeIndependentTransforms = style => {\n\t\tif (style.translate) {\n\t\t\tstyle.removeProperty(\"translate\");\n\t\t\tstyle.removeProperty(\"scale\");\n\t\t\tstyle.removeProperty(\"rotate\");\n\t\t}\n\t},\n\t_revertStyle = function() {\n\t\tlet props = this.props,\n\t\t\ttarget = this.target,\n\t\t\tstyle = target.style,\n\t\t\tcache = target._gsap,\n\t\t\ti, p;\n\t\tfor (i = 0; i < props.length; i+=3) { // stored like this: property, isNotCSS, value\n\t\t\tif (!props[i+1]) {\n\t\t\t\tprops[i+2] ? (style[props[i]] = props[i+2]) : style.removeProperty(props[i].substr(0,2) === \"--\" ? props[i] : props[i].replace(_capsExp, \"-$1\").toLowerCase());\n\t\t\t} else if (props[i+1] === 2) { // non-CSS value (function-based)\n\t\t\t\ttarget[props[i]](props[i+2]);\n\t\t\t} else { // non-CSS value (not function-based)\n\t\t\t\ttarget[props[i]] = props[i+2];\n\t\t\t}\n\t\t}\n\t\tif (this.tfm) {\n\t\t\tfor (p in this.tfm) {\n\t\t\t\tcache[p] = this.tfm[p];\n\t\t\t}\n\t\t\tif (cache.svg) {\n\t\t\t\tcache.renderTransform();\n\t\t\t\ttarget.setAttribute(\"data-svg-origin\", this.svgo || \"\");\n\t\t\t}\n\t\t\ti = _reverting();\n\t\t\tif ((!i || !i.isStart) && !style[_transformProp]) {\n\t\t\t\t_removeIndependentTransforms(style);\n\t\t\t\tif (cache.zOrigin && style[_transformOriginProp]) {\n\t\t\t\t\tstyle[_transformOriginProp] += \" \" + cache.zOrigin + \"px\"; // since we're uncaching, we must put the zOrigin back into the transformOrigin so that we can pull it out accurately when we parse again. Otherwise, we'd lose the z portion of the origin since we extract it to protect from Safari bugs.\n\t\t\t\t\tcache.zOrigin = 0;\n\t\t\t\t\tcache.renderTransform();\n\t\t\t\t}\n\t\t\t\tcache.uncache = 1; // if it's a startAt that's being reverted in the _initTween() of the core, we don't need to uncache transforms. This is purely a performance optimization.\n\t\t\t}\n\t\t}\n\t},\n\t_getStyleSaver = (target, properties) => {\n\t\tlet saver = {\n\t\t\ttarget,\n\t\t\tprops: [],\n\t\t\trevert: _revertStyle,\n\t\t\tsave: _saveStyle\n\t\t};\n\t\ttarget._gsap || gsap.core.getCache(target); // just make sure there's a _gsap cache defined because we read from it in _saveStyle() and it's more efficient to just check it here once.\n\t\tproperties && target.style && target.nodeType && properties.split(\",\").forEach(p => saver.save(p)); // make sure it's a DOM node too.\n\t\treturn saver;\n\t},\n\t_supports3D,\n\t_createElement = (type, ns) => {\n\t\tlet e = _doc.createElementNS ? _doc.createElementNS((ns || \"http://www.w3.org/1999/xhtml\").replace(/^https/, \"http\"), type) : _doc.createElement(type); //some servers swap in https for http in the namespace which can break things, making \"style\" inaccessible.\n\t\treturn e && e.style ? e : _doc.createElement(type); //some environments won't allow access to the element's style when created with a namespace in which case we default to the standard createElement() to work around the issue. Also note that when GSAP is embedded directly inside an SVG file, createElement() won't allow access to the style object in Firefox (see https://gsap.com/forums/topic/20215-problem-using-tweenmax-in-standalone-self-containing-svg-file-err-cannot-set-property-csstext-of-undefined/).\n\t},\n\t_getComputedProperty = (target, property, skipPrefixFallback) => {\n\t\tlet cs = getComputedStyle(target);\n\t\treturn cs[property] || cs.getPropertyValue(property.replace(_capsExp, \"-$1\").toLowerCase()) || cs.getPropertyValue(property) || (!skipPrefixFallback && _getComputedProperty(target, _checkPropPrefix(property) || property, 1)) || \"\"; //css variables may not need caps swapped out for dashes and lowercase.\n\t},\n\t_prefixes = \"O,Moz,ms,Ms,Webkit\".split(\",\"),\n\t_checkPropPrefix = (property, element, preferPrefix) => {\n\t\tlet e = element || _tempDiv,\n\t\t\ts = e.style,\n\t\t\ti = 5;\n\t\tif (property in s && !preferPrefix) {\n\t\t\treturn property;\n\t\t}\n\t\tproperty = property.charAt(0).toUpperCase() + property.substr(1);\n\t\twhile (i-- && !((_prefixes[i]+property) in s)) { }\n\t\treturn (i < 0) ? null : ((i === 3) ? \"ms\" : (i >= 0) ? _prefixes[i] : \"\") + property;\n\t},\n\t_initCore = () => {\n\t\tif (_windowExists() && window.document) {\n\t\t\t_win = window;\n\t\t\t_doc = _win.document;\n\t\t\t_docElement = _doc.documentElement;\n\t\t\t_tempDiv = _createElement(\"div\") || {style:{}};\n\t\t\t_tempDivStyler = _createElement(\"div\");\n\t\t\t_transformProp = _checkPropPrefix(_transformProp);\n\t\t\t_transformOriginProp = _transformProp + \"Origin\";\n\t\t\t_tempDiv.style.cssText = \"border-width:0;line-height:0;position:absolute;padding:0\"; //make sure to override certain properties that may contaminate measurements, in case the user has overreaching style sheets.\n\t\t\t_supports3D = !!_checkPropPrefix(\"perspective\");\n\t\t\t_reverting = gsap.core.reverting;\n\t\t\t_pluginInitted = 1;\n\t\t}\n\t},\n\t_getReparentedCloneBBox = target => { //works around issues in some browsers (like Firefox) that don't correctly report getBBox() on SVG elements inside a element and/or . We try creating an SVG, adding it to the documentElement and toss the element in there so that it's definitely part of the rendering tree, then grab the bbox and if it works, we actually swap out the original getBBox() method for our own that does these extra steps whenever getBBox is needed. This helps ensure that performance is optimal (only do all these extra steps when absolutely necessary...most elements don't need it).\n\t\tlet owner = target.ownerSVGElement,\n\t\t\tsvg = _createElement(\"svg\", (owner && owner.getAttribute(\"xmlns\")) || \"http://www.w3.org/2000/svg\"),\n\t\t\tclone = target.cloneNode(true),\n\t\t\tbbox;\n\t\tclone.style.display = \"block\";\n\t\tsvg.appendChild(clone);\n\t\t_docElement.appendChild(svg);\n\t\ttry {\n\t\t\tbbox = clone.getBBox();\n\t\t} catch (e) { }\n\t\tsvg.removeChild(clone);\n\t\t_docElement.removeChild(svg);\n\t\treturn bbox;\n\t},\n\t_getAttributeFallbacks = (target, attributesArray) => {\n\t\tlet i = attributesArray.length;\n\t\twhile (i--) {\n\t\t\tif (target.hasAttribute(attributesArray[i])) {\n\t\t\t\treturn target.getAttribute(attributesArray[i]);\n\t\t\t}\n\t\t}\n\t},\n\t_getBBox = target => {\n\t\tlet bounds, cloned;\n\t\ttry {\n\t\t\tbounds = target.getBBox(); //Firefox throws errors if you try calling getBBox() on an SVG element that's not rendered (like in a or ). https://bugzilla.mozilla.org/show_bug.cgi?id=612118\n\t\t} catch (error) {\n\t\t\tbounds = _getReparentedCloneBBox(target);\n\t\t\tcloned = 1;\n\t\t}\n\t\t(bounds && (bounds.width || bounds.height)) || cloned || (bounds = _getReparentedCloneBBox(target));\n\t\t//some browsers (like Firefox) misreport the bounds if the element has zero width and height (it just assumes it's at x:0, y:0), thus we need to manually grab the position in that case.\n\t\treturn (bounds && !bounds.width && !bounds.x && !bounds.y) ? {x: +_getAttributeFallbacks(target, [\"x\",\"cx\",\"x1\"]) || 0, y:+_getAttributeFallbacks(target, [\"y\",\"cy\",\"y1\"]) || 0, width:0, height:0} : bounds;\n\t},\n\t_isSVG = e => !!(e.getCTM && (!e.parentNode || e.ownerSVGElement) && _getBBox(e)), //reports if the element is an SVG on which getBBox() actually works\n\t_removeProperty = (target, property) => {\n\t\tif (property) {\n\t\t\tlet style = target.style,\n\t\t\t\tfirst2Chars;\n\t\t\tif (property in _transformProps && property !== _transformOriginProp) {\n\t\t\t\tproperty = _transformProp;\n\t\t\t}\n\t\t\tif (style.removeProperty) {\n\t\t\t\tfirst2Chars = property.substr(0,2);\n\t\t\t\tif (first2Chars === \"ms\" || property.substr(0,6) === \"webkit\") { //Microsoft and some Webkit browsers don't conform to the standard of capitalizing the first prefix character, so we adjust so that when we prefix the caps with a dash, it's correct (otherwise it'd be \"ms-transform\" instead of \"-ms-transform\" for IE9, for example)\n\t\t\t\t\tproperty = \"-\" + property;\n\t\t\t\t}\n\t\t\t\tstyle.removeProperty(first2Chars === \"--\" ? property : property.replace(_capsExp, \"-$1\").toLowerCase());\n\t\t\t} else { //note: old versions of IE use \"removeAttribute()\" instead of \"removeProperty()\"\n\t\t\t\tstyle.removeAttribute(property);\n\t\t\t}\n\t\t}\n\t},\n\t_addNonTweeningPT = (plugin, target, property, beginning, end, onlySetAtEnd) => {\n\t\tlet pt = new PropTween(plugin._pt, target, property, 0, 1, onlySetAtEnd ? _renderNonTweeningValueOnlyAtEnd : _renderNonTweeningValue);\n\t\tplugin._pt = pt;\n\t\tpt.b = beginning;\n\t\tpt.e = end;\n\t\tplugin._props.push(property);\n\t\treturn pt;\n\t},\n\t_nonConvertibleUnits = {deg:1, rad:1, turn:1},\n\t_nonStandardLayouts = {grid:1, flex:1},\n\t//takes a single value like 20px and converts it to the unit specified, like \"%\", returning only the numeric amount.\n\t_convertToUnit = (target, property, value, unit) => {\n\t\tlet curValue = parseFloat(value) || 0,\n\t\t\tcurUnit = (value + \"\").trim().substr((curValue + \"\").length) || \"px\", // some browsers leave extra whitespace at the beginning of CSS variables, hence the need to trim()\n\t\t\tstyle = _tempDiv.style,\n\t\t\thorizontal = _horizontalExp.test(property),\n\t\t\tisRootSVG = target.tagName.toLowerCase() === \"svg\",\n\t\t\tmeasureProperty = (isRootSVG ? \"client\" : \"offset\") + (horizontal ? \"Width\" : \"Height\"),\n\t\t\tamount = 100,\n\t\t\ttoPixels = unit === \"px\",\n\t\t\ttoPercent = unit === \"%\",\n\t\t\tpx, parent, cache, isSVG;\n\t\tif (unit === curUnit || !curValue || _nonConvertibleUnits[unit] || _nonConvertibleUnits[curUnit]) {\n\t\t\treturn curValue;\n\t\t}\n\t\t(curUnit !== \"px\" && !toPixels) && (curValue = _convertToUnit(target, property, value, \"px\"));\n\t\tisSVG = target.getCTM && _isSVG(target);\n\t\tif ((toPercent || curUnit === \"%\") && (_transformProps[property] || ~property.indexOf(\"adius\"))) {\n\t\t\tpx = isSVG ? target.getBBox()[horizontal ? \"width\" : \"height\"] : target[measureProperty];\n\t\t\treturn _round(toPercent ? curValue / px * amount : curValue / 100 * px);\n\t\t}\n\t\tstyle[horizontal ? \"width\" : \"height\"] = amount + (toPixels ? curUnit : unit);\n\t\tparent = ((unit !== \"rem\" && ~property.indexOf(\"adius\")) || (unit === \"em\" && target.appendChild && !isRootSVG)) ? target : target.parentNode;\n\t\tif (isSVG) {\n\t\t\tparent = (target.ownerSVGElement || {}).parentNode;\n\t\t}\n\t\tif (!parent || parent === _doc || !parent.appendChild) {\n\t\t\tparent = _doc.body;\n\t\t}\n\t\tcache = parent._gsap;\n\t\tif (cache && toPercent && cache.width && horizontal && cache.time === _ticker.time && !cache.uncache) {\n\t\t\treturn _round(curValue / cache.width * amount);\n\t\t} else {\n\t\t\tif (toPercent && (property === \"height\" || property === \"width\")) { // if we're dealing with width/height that's inside a container with padding and/or it's a flexbox/grid container, we must apply it to the target itself rather than the _tempDiv in order to ensure complete accuracy, factoring in the parent's padding.\n\t\t\t\tlet v = target.style[property];\n\t\t\t\ttarget.style[property] = amount + unit;\n\t\t\t\tpx = target[measureProperty];\n\t\t\t\tv ? (target.style[property] = v) : _removeProperty(target, property);\n\t\t\t} else {\n\t\t\t\t(toPercent || curUnit === \"%\") && !_nonStandardLayouts[_getComputedProperty(parent, \"display\")] && (style.position = _getComputedProperty(target, \"position\"));\n\t\t\t\t(parent === target) && (style.position = \"static\"); // like for borderRadius, if it's a % we must have it relative to the target itself but that may not have position: relative or position: absolute in which case it'd go up the chain until it finds its offsetParent (bad). position: static protects against that.\n\t\t\t\tparent.appendChild(_tempDiv);\n\t\t\t\tpx = _tempDiv[measureProperty];\n\t\t\t\tparent.removeChild(_tempDiv);\n\t\t\t\tstyle.position = \"absolute\";\n\t\t\t}\n\t\t\tif (horizontal && toPercent) {\n\t\t\t\tcache = _getCache(parent);\n\t\t\t\tcache.time = _ticker.time;\n\t\t\t\tcache.width = parent[measureProperty];\n\t\t\t}\n\t\t}\n\t\treturn _round(toPixels ? px * curValue / amount : px && curValue ? amount / px * curValue : 0);\n\t},\n\t_get = (target, property, unit, uncache) => {\n\t\tlet value;\n\t\t_pluginInitted || _initCore();\n\t\tif ((property in _propertyAliases) && property !== \"transform\") {\n\t\t\tproperty = _propertyAliases[property];\n\t\t\tif (~property.indexOf(\",\")) {\n\t\t\t\tproperty = property.split(\",\")[0];\n\t\t\t}\n\t\t}\n\t\tif (_transformProps[property] && property !== \"transform\") {\n\t\t\tvalue = _parseTransform(target, uncache);\n\t\t\tvalue = (property !== \"transformOrigin\") ? value[property] : value.svg ? value.origin : _firstTwoOnly(_getComputedProperty(target, _transformOriginProp)) + \" \" + value.zOrigin + \"px\";\n\t\t} else {\n\t\t\tvalue = target.style[property];\n\t\t\tif (!value || value === \"auto\" || uncache || ~(value + \"\").indexOf(\"calc(\")) {\n\t\t\t\tvalue = (_specialProps[property] && _specialProps[property](target, property, unit)) || _getComputedProperty(target, property) || _getProperty(target, property) || (property === \"opacity\" ? 1 : 0); // note: some browsers, like Firefox, don't report borderRadius correctly! Instead, it only reports every corner like borderTopLeftRadius\n\t\t\t}\n\t\t}\n\t\treturn unit && !~(value + \"\").trim().indexOf(\" \") ? _convertToUnit(target, property, value, unit) + unit : value;\n\n\t},\n\t_tweenComplexCSSString = function(target, prop, start, end) { // note: we call _tweenComplexCSSString.call(pluginInstance...) to ensure that it's scoped properly. We may call it from within a plugin too, thus \"this\" would refer to the plugin.\n\t\tif (!start || start === \"none\") { // some browsers like Safari actually PREFER the prefixed property and mis-report the unprefixed value like clipPath (BUG). In other words, even though clipPath exists in the style (\"clipPath\" in target.style) and it's set in the CSS properly (along with -webkit-clip-path), Safari reports clipPath as \"none\" whereas WebkitClipPath reports accurately like \"ellipse(100% 0% at 50% 0%)\", so in this case we must SWITCH to using the prefixed property instead. See https://gsap.com/forums/topic/18310-clippath-doesnt-work-on-ios/\n\t\t\tlet p = _checkPropPrefix(prop, target, 1),\n\t\t\t\ts = p && _getComputedProperty(target, p, 1);\n\t\t\tif (s && s !== start) {\n\t\t\t\tprop = p;\n\t\t\t\tstart = s;\n\t\t\t} else if (prop === \"borderColor\") {\n\t\t\t\tstart = _getComputedProperty(target, \"borderTopColor\"); // Firefox bug: always reports \"borderColor\" as \"\", so we must fall back to borderTopColor. See https://gsap.com/forums/topic/24583-how-to-return-colors-that-i-had-after-reverse/\n\t\t\t}\n\t\t}\n\t\tlet pt = new PropTween(this._pt, target.style, prop, 0, 1, _renderComplexString),\n\t\t\tindex = 0,\n\t\t\tmatchIndex = 0,\n\t\t\ta, result,\tstartValues, startNum, color, startValue, endValue, endNum, chunk, endUnit, startUnit, endValues;\n\t\tpt.b = start;\n\t\tpt.e = end;\n\t\tstart += \"\"; // ensure values are strings\n\t\tend += \"\";\n\t\tif (end === \"auto\") {\n\t\t\tstartValue = target.style[prop];\n\t\t\ttarget.style[prop] = end;\n\t\t\tend = _getComputedProperty(target, prop) || end;\n\t\t\tstartValue ? (target.style[prop] = startValue) : _removeProperty(target, prop);\n\t\t}\n\t\ta = [start, end];\n\t\t_colorStringFilter(a); // pass an array with the starting and ending values and let the filter do whatever it needs to the values. If colors are found, it returns true and then we must match where the color shows up order-wise because for things like boxShadow, sometimes the browser provides the computed values with the color FIRST, but the user provides it with the color LAST, so flip them if necessary. Same for drop-shadow().\n\t\tstart = a[0];\n\t\tend = a[1];\n\t\tstartValues = start.match(_numWithUnitExp) || [];\n\t\tendValues = end.match(_numWithUnitExp) || [];\n\t\tif (endValues.length) {\n\t\t\twhile ((result = _numWithUnitExp.exec(end))) {\n\t\t\t\tendValue = result[0];\n\t\t\t\tchunk = end.substring(index, result.index);\n\t\t\t\tif (color) {\n\t\t\t\t\tcolor = (color + 1) % 5;\n\t\t\t\t} else if (chunk.substr(-5) === \"rgba(\" || chunk.substr(-5) === \"hsla(\") {\n\t\t\t\t\tcolor = 1;\n\t\t\t\t}\n\t\t\t\tif (endValue !== (startValue = startValues[matchIndex++] || \"\")) {\n\t\t\t\t\tstartNum = parseFloat(startValue) || 0;\n\t\t\t\t\tstartUnit = startValue.substr((startNum + \"\").length);\n\t\t\t\t\t(endValue.charAt(1) === \"=\") && (endValue = _parseRelative(startNum, endValue) + startUnit);\n\t\t\t\t\tendNum = parseFloat(endValue);\n\t\t\t\t\tendUnit = endValue.substr((endNum + \"\").length);\n\t\t\t\t\tindex = _numWithUnitExp.lastIndex - endUnit.length;\n\t\t\t\t\tif (!endUnit) { //if something like \"perspective:300\" is passed in and we must add a unit to the end\n\t\t\t\t\t\tendUnit = endUnit || _config.units[prop] || startUnit;\n\t\t\t\t\t\tif (index === end.length) {\n\t\t\t\t\t\t\tend += endUnit;\n\t\t\t\t\t\t\tpt.e += endUnit;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (startUnit !== endUnit) {\n\t\t\t\t\t\tstartNum = _convertToUnit(target, prop, startValue, endUnit) || 0;\n\t\t\t\t\t}\n\t\t\t\t\t// these nested PropTweens are handled in a special way - we'll never actually call a render or setter method on them. We'll just loop through them in the parent complex string PropTween's render method.\n\t\t\t\t\tpt._pt = {\n\t\t\t\t\t\t_next: pt._pt,\n\t\t\t\t\t\tp: (chunk || (matchIndex === 1)) ? chunk : \",\", //note: SVG spec allows omission of comma/space when a negative sign is wedged between two numbers, like 2.5-5.3 instead of 2.5,-5.3 but when tweening, the negative value may switch to positive, so we insert the comma just in case.\n\t\t\t\t\t\ts: startNum,\n\t\t\t\t\t\tc: endNum - startNum,\n\t\t\t\t\t\tm: (color && color < 4) || prop === \"zIndex\" ? Math.round : 0\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\tpt.c = (index < end.length) ? end.substring(index, end.length) : \"\"; //we use the \"c\" of the PropTween to store the final part of the string (after the last number)\n\t\t} else {\n\t\t\tpt.r = prop === \"display\" && end === \"none\" ? _renderNonTweeningValueOnlyAtEnd : _renderNonTweeningValue;\n\t\t}\n\t\t_relExp.test(end) && (pt.e = 0); //if the end string contains relative values or dynamic random(...) values, delete the end it so that on the final render we don't actually set it to the string with += or -= characters (forces it to use the calculated value).\n\t\tthis._pt = pt; //start the linked list with this new PropTween. Remember, we call _tweenComplexCSSString.call(pluginInstance...) to ensure that it's scoped properly. We may call it from within another plugin too, thus \"this\" would refer to the plugin.\n\t\treturn pt;\n\t},\n\t_keywordToPercent = {top:\"0%\", bottom:\"100%\", left:\"0%\", right:\"100%\", center:\"50%\"},\n\t_convertKeywordsToPercentages = value => {\n\t\tlet split = value.split(\" \"),\n\t\t\tx = split[0],\n\t\t\ty = split[1] || \"50%\";\n\t\tif (x === \"top\" || x === \"bottom\" || y === \"left\" || y === \"right\") { //the user provided them in the wrong order, so flip them\n\t\t\tvalue = x;\n\t\t\tx = y;\n\t\t\ty = value;\n\t\t}\n\t\tsplit[0] = _keywordToPercent[x] || x;\n\t\tsplit[1] = _keywordToPercent[y] || y;\n\t\treturn split.join(\" \");\n\t},\n\t_renderClearProps = (ratio, data) => {\n\t\tif (data.tween && data.tween._time === data.tween._dur) {\n\t\t\tlet target = data.t,\n\t\t\t\tstyle = target.style,\n\t\t\t\tprops = data.u,\n\t\t\t\tcache = target._gsap,\n\t\t\t\tprop, clearTransforms, i;\n\t\t\tif (props === \"all\" || props === true) {\n\t\t\t\tstyle.cssText = \"\";\n\t\t\t\tclearTransforms = 1;\n\t\t\t} else {\n\t\t\t\tprops = props.split(\",\");\n\t\t\t\ti = props.length;\n\t\t\t\twhile (--i > -1) {\n\t\t\t\t\tprop = props[i];\n\t\t\t\t\tif (_transformProps[prop]) {\n\t\t\t\t\t\tclearTransforms = 1;\n\t\t\t\t\t\tprop = (prop === \"transformOrigin\") ? _transformOriginProp : _transformProp;\n\t\t\t\t\t}\n\t\t\t\t\t_removeProperty(target, prop);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (clearTransforms) {\n\t\t\t\t_removeProperty(target, _transformProp);\n\t\t\t\tif (cache) {\n\t\t\t\t\tcache.svg && target.removeAttribute(\"transform\");\n\t\t\t\t\tstyle.scale = style.rotate = style.translate = \"none\";\n\t\t\t\t\t_parseTransform(target, 1); // force all the cached values back to \"normal\"/identity, otherwise if there's another tween that's already set to render transforms on this element, it could display the wrong values.\n\t\t\t\t\tcache.uncache = 1;\n\t\t\t\t\t_removeIndependentTransforms(style);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\t// note: specialProps should return 1 if (and only if) they have a non-zero priority. It indicates we need to sort the linked list.\n\t_specialProps = {\n\t\tclearProps(plugin, target, property, endValue, tween) {\n\t\t\tif (tween.data !== \"isFromStart\") {\n\t\t\t\tlet pt = plugin._pt = new PropTween(plugin._pt, target, property, 0, 0, _renderClearProps);\n\t\t\t\tpt.u = endValue;\n\t\t\t\tpt.pr = -10;\n\t\t\t\tpt.tween = tween;\n\t\t\t\tplugin._props.push(property);\n\t\t\t\treturn 1;\n\t\t\t}\n\t\t}\n\t\t/* className feature (about 0.4kb gzipped).\n\t\t, className(plugin, target, property, endValue, tween) {\n\t\t\tlet _renderClassName = (ratio, data) => {\n\t\t\t\t\tdata.css.render(ratio, data.css);\n\t\t\t\t\tif (!ratio || ratio === 1) {\n\t\t\t\t\t\tlet inline = data.rmv,\n\t\t\t\t\t\t\ttarget = data.t,\n\t\t\t\t\t\t\tp;\n\t\t\t\t\t\ttarget.setAttribute(\"class\", ratio ? data.e : data.b);\n\t\t\t\t\t\tfor (p in inline) {\n\t\t\t\t\t\t\t_removeProperty(target, p);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t_getAllStyles = (target) => {\n\t\t\t\t\tlet styles = {},\n\t\t\t\t\t\tcomputed = getComputedStyle(target),\n\t\t\t\t\t\tp;\n\t\t\t\t\tfor (p in computed) {\n\t\t\t\t\t\tif (isNaN(p) && p !== \"cssText\" && p !== \"length\") {\n\t\t\t\t\t\t\tstyles[p] = computed[p];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t_setDefaults(styles, _parseTransform(target, 1));\n\t\t\t\t\treturn styles;\n\t\t\t\t},\n\t\t\t\tstartClassList = target.getAttribute(\"class\"),\n\t\t\t\tstyle = target.style,\n\t\t\t\tcssText = style.cssText,\n\t\t\t\tcache = target._gsap,\n\t\t\t\tclassPT = cache.classPT,\n\t\t\t\tinlineToRemoveAtEnd = {},\n\t\t\t\tdata = {t:target, plugin:plugin, rmv:inlineToRemoveAtEnd, b:startClassList, e:(endValue.charAt(1) !== \"=\") ? endValue : startClassList.replace(new RegExp(\"(?:\\\\s|^)\" + endValue.substr(2) + \"(?![\\\\w-])\"), \"\") + ((endValue.charAt(0) === \"+\") ? \" \" + endValue.substr(2) : \"\")},\n\t\t\t\tchangingVars = {},\n\t\t\t\tstartVars = _getAllStyles(target),\n\t\t\t\ttransformRelated = /(transform|perspective)/i,\n\t\t\t\tendVars, p;\n\t\t\tif (classPT) {\n\t\t\t\tclassPT.r(1, classPT.d);\n\t\t\t\t_removeLinkedListItem(classPT.d.plugin, classPT, \"_pt\");\n\t\t\t}\n\t\t\ttarget.setAttribute(\"class\", data.e);\n\t\t\tendVars = _getAllStyles(target, true);\n\t\t\ttarget.setAttribute(\"class\", startClassList);\n\t\t\tfor (p in endVars) {\n\t\t\t\tif (endVars[p] !== startVars[p] && !transformRelated.test(p)) {\n\t\t\t\t\tchangingVars[p] = endVars[p];\n\t\t\t\t\tif (!style[p] && style[p] !== \"0\") {\n\t\t\t\t\t\tinlineToRemoveAtEnd[p] = 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcache.classPT = plugin._pt = new PropTween(plugin._pt, target, \"className\", 0, 0, _renderClassName, data, 0, -11);\n\t\t\tif (style.cssText !== cssText) { //only apply if things change. Otherwise, in cases like a background-image that's pulled dynamically, it could cause a refresh. See https://gsap.com/forums/topic/20368-possible-gsap-bug-switching-classnames-in-chrome/.\n\t\t\t\tstyle.cssText = cssText; //we recorded cssText before we swapped classes and ran _getAllStyles() because in cases when a className tween is overwritten, we remove all the related tweening properties from that class change (otherwise class-specific stuff can't override properties we've directly set on the target's style object due to specificity).\n\t\t\t}\n\t\t\t_parseTransform(target, true); //to clear the caching of transforms\n\t\t\tdata.css = new gsap.plugins.css();\n\t\t\tdata.css.init(target, changingVars, tween);\n\t\t\tplugin._props.push(...data.css._props);\n\t\t\treturn 1;\n\t\t}\n\t\t*/\n\t},\n\n\n\n\n\n\t/*\n\t * --------------------------------------------------------------------------------------\n\t * TRANSFORMS\n\t * --------------------------------------------------------------------------------------\n\t */\n\t_identity2DMatrix = [1,0,0,1,0,0],\n\t_rotationalProperties = {},\n\t_isNullTransform = value => (value === \"matrix(1, 0, 0, 1, 0, 0)\" || value === \"none\" || !value),\n\t_getComputedTransformMatrixAsArray = target => {\n\t\tlet matrixString = _getComputedProperty(target, _transformProp);\n\t\treturn _isNullTransform(matrixString) ? _identity2DMatrix : matrixString.substr(7).match(_numExp).map(_round);\n\t},\n\t_getMatrix = (target, force2D) => {\n\t\tlet cache = target._gsap || _getCache(target),\n\t\t\tstyle = target.style,\n\t\t\tmatrix = _getComputedTransformMatrixAsArray(target),\n\t\t\tparent, nextSibling, temp, addedToDOM;\n\t\tif (cache.svg && target.getAttribute(\"transform\")) {\n\t\t\ttemp = target.transform.baseVal.consolidate().matrix; //ensures that even complex values like \"translate(50,60) rotate(135,0,0)\" are parsed because it mashes it into a matrix.\n\t\t\tmatrix = [temp.a, temp.b, temp.c, temp.d, temp.e, temp.f];\n\t\t\treturn (matrix.join(\",\") === \"1,0,0,1,0,0\") ? _identity2DMatrix : matrix;\n\t\t} else if (matrix === _identity2DMatrix && !target.offsetParent && target !== _docElement && !cache.svg) { //note: if offsetParent is null, that means the element isn't in the normal document flow, like if it has display:none or one of its ancestors has display:none). Firefox returns null for getComputedStyle() if the element is in an iframe that has display:none. https://bugzilla.mozilla.org/show_bug.cgi?id=548397\n\t\t\t//browsers don't report transforms accurately unless the element is in the DOM and has a display value that's not \"none\". Firefox and Microsoft browsers have a partial bug where they'll report transforms even if display:none BUT not any percentage-based values like translate(-50%, 8px) will be reported as if it's translate(0, 8px).\n\t\t\ttemp = style.display;\n\t\t\tstyle.display = \"block\";\n\t\t\tparent = target.parentNode;\n\t\t\tif (!parent || (!target.offsetParent && !target.getBoundingClientRect().width)) { // note: in 3.3.0 we switched target.offsetParent to _doc.body.contains(target) to avoid [sometimes unnecessary] MutationObserver calls but that wasn't adequate because there are edge cases where nested position: fixed elements need to get reparented to accurately sense transforms. See https://github.com/greensock/GSAP/issues/388 and https://github.com/greensock/GSAP/issues/375. Note: position: fixed elements report a null offsetParent but they could also be invisible because they're in an ancestor with display: none, so we check getBoundingClientRect(). We only want to alter the DOM if we absolutely have to because it can cause iframe content to reload, like a Vimeo video.\n\t\t\t\taddedToDOM = 1; //flag\n\t\t\t\tnextSibling = target.nextElementSibling;\n\t\t\t\t_docElement.appendChild(target); //we must add it to the DOM in order to get values properly\n\t\t\t}\n\t\t\tmatrix = _getComputedTransformMatrixAsArray(target);\n\t\t\ttemp ? (style.display = temp) : _removeProperty(target, \"display\");\n\t\t\tif (addedToDOM) {\n\t\t\t\tnextSibling ? parent.insertBefore(target, nextSibling) : parent ? parent.appendChild(target) : _docElement.removeChild(target);\n\t\t\t}\n\t\t}\n\t\treturn (force2D && matrix.length > 6) ? [matrix[0], matrix[1], matrix[4], matrix[5], matrix[12], matrix[13]] : matrix;\n\t},\n\t_applySVGOrigin = (target, origin, originIsAbsolute, smooth, matrixArray, pluginToAddPropTweensTo) => {\n\t\tlet cache = target._gsap,\n\t\t\tmatrix = matrixArray || _getMatrix(target, true),\n\t\t\txOriginOld = cache.xOrigin || 0,\n\t\t\tyOriginOld = cache.yOrigin || 0,\n\t\t\txOffsetOld = cache.xOffset || 0,\n\t\t\tyOffsetOld = cache.yOffset || 0,\n\t\t\t[a, b, c, d, tx, ty] = matrix,\n\t\t\toriginSplit = origin.split(\" \"),\n\t\t\txOrigin = parseFloat(originSplit[0]) || 0,\n\t\t\tyOrigin = parseFloat(originSplit[1]) || 0,\n\t\t\tbounds, determinant, x, y;\n\t\tif (!originIsAbsolute) {\n\t\t\tbounds = _getBBox(target);\n\t\t\txOrigin = bounds.x + (~originSplit[0].indexOf(\"%\") ? xOrigin / 100 * bounds.width : xOrigin);\n\t\t\tyOrigin = bounds.y + (~((originSplit[1] || originSplit[0]).indexOf(\"%\")) ? yOrigin / 100 * bounds.height : yOrigin);\n\t\t\t// if (!(\"xOrigin\" in cache) && (xOrigin || yOrigin)) { // added in 3.12.3, reverted in 3.12.4; requires more exploration\n\t\t\t// \txOrigin -= bounds.x;\n\t\t\t// \tyOrigin -= bounds.y;\n\t\t\t// }\n\t\t} else if (matrix !== _identity2DMatrix && (determinant = (a * d - b * c))) { //if it's zero (like if scaleX and scaleY are zero), skip it to avoid errors with dividing by zero.\n\t\t\tx = xOrigin * (d / determinant) + yOrigin * (-c / determinant) + ((c * ty - d * tx) / determinant);\n\t\t\ty = xOrigin * (-b / determinant) + yOrigin * (a / determinant) - ((a * ty - b * tx) / determinant);\n\t\t\txOrigin = x;\n\t\t\tyOrigin = y;\n\t\t\t// theory: we only had to do this for smoothing and it assumes that the previous one was not originIsAbsolute.\n\t\t}\n\t\tif (smooth || (smooth !== false && cache.smooth)) {\n\t\t\ttx = xOrigin - xOriginOld;\n\t\t\tty = yOrigin - yOriginOld;\n\t\t\tcache.xOffset = xOffsetOld + (tx * a + ty * c) - tx;\n\t\t\tcache.yOffset = yOffsetOld + (tx * b + ty * d) - ty;\n\t\t} else {\n\t\t\tcache.xOffset = cache.yOffset = 0;\n\t\t}\n\t\tcache.xOrigin = xOrigin;\n\t\tcache.yOrigin = yOrigin;\n\t\tcache.smooth = !!smooth;\n\t\tcache.origin = origin;\n\t\tcache.originIsAbsolute = !!originIsAbsolute;\n\t\ttarget.style[_transformOriginProp] = \"0px 0px\"; //otherwise, if someone sets an origin via CSS, it will likely interfere with the SVG transform attribute ones (because remember, we're baking the origin into the matrix() value).\n\t\tif (pluginToAddPropTweensTo) {\n\t\t\t_addNonTweeningPT(pluginToAddPropTweensTo, cache, \"xOrigin\", xOriginOld, xOrigin);\n\t\t\t_addNonTweeningPT(pluginToAddPropTweensTo, cache, \"yOrigin\", yOriginOld, yOrigin);\n\t\t\t_addNonTweeningPT(pluginToAddPropTweensTo, cache, \"xOffset\", xOffsetOld, cache.xOffset);\n\t\t\t_addNonTweeningPT(pluginToAddPropTweensTo, cache, \"yOffset\", yOffsetOld, cache.yOffset);\n\t\t}\n\t\ttarget.setAttribute(\"data-svg-origin\", xOrigin + \" \" + yOrigin);\n\t},\n\t_parseTransform = (target, uncache) => {\n\t\tlet cache = target._gsap || new GSCache(target);\n\t\tif (\"x\" in cache && !uncache && !cache.uncache) {\n\t\t\treturn cache;\n\t\t}\n\t\tlet style = target.style,\n\t\t\tinvertedScaleX = cache.scaleX < 0,\n\t\t\tpx = \"px\",\n\t\t\tdeg = \"deg\",\n\t\t\tcs = getComputedStyle(target),\n\t\t\torigin = _getComputedProperty(target, _transformOriginProp) || \"0\",\n\t\t\tx, y, z, scaleX, scaleY, rotation, rotationX, rotationY, skewX, skewY, perspective, xOrigin, yOrigin,\n\t\t\tmatrix, angle, cos, sin, a, b, c, d, a12, a22, t1, t2, t3, a13, a23, a33, a42, a43, a32;\n\t\tx = y = z = rotation = rotationX = rotationY = skewX = skewY = perspective = 0;\n\t\tscaleX = scaleY = 1;\n\t\tcache.svg = !!(target.getCTM && _isSVG(target));\n\n\t\tif (cs.translate) { // accommodate independent transforms by combining them into normal ones.\n\t\t\tif (cs.translate !== \"none\" || cs.scale !== \"none\" || cs.rotate !== \"none\") {\n\t\t\t\tstyle[_transformProp] = (cs.translate !== \"none\" ? \"translate3d(\" + (cs.translate + \" 0 0\").split(\" \").slice(0, 3).join(\", \") + \") \" : \"\") + (cs.rotate !== \"none\" ? \"rotate(\" + cs.rotate + \") \" : \"\") + (cs.scale !== \"none\" ? \"scale(\" + cs.scale.split(\" \").join(\",\") + \") \" : \"\") + (cs[_transformProp] !== \"none\" ? cs[_transformProp] : \"\");\n\t\t\t}\n\t\t\tstyle.scale = style.rotate = style.translate = \"none\";\n\t\t}\n\n\t\tmatrix = _getMatrix(target, cache.svg);\n\t\tif (cache.svg) {\n\t\t\tif (cache.uncache) { // if cache.uncache is true (and maybe if origin is 0,0), we need to set element.style.transformOrigin = (cache.xOrigin - bbox.x) + \"px \" + (cache.yOrigin - bbox.y) + \"px\". Previously we let the data-svg-origin stay instead, but when introducing revert(), it complicated things.\n\t\t\t\tt2 = target.getBBox();\n\t\t\t\torigin = (cache.xOrigin - t2.x) + \"px \" + (cache.yOrigin - t2.y) + \"px\";\n\t\t\t\tt1 = \"\";\n\t\t\t} else {\n\t\t\t\tt1 = !uncache && target.getAttribute(\"data-svg-origin\"); // Remember, to work around browser inconsistencies we always force SVG elements' transformOrigin to 0,0 and offset the translation accordingly.\n\t\t\t}\n\t\t\t_applySVGOrigin(target, t1 || origin, !!t1 || cache.originIsAbsolute, cache.smooth !== false, matrix);\n\t\t}\n\t\txOrigin = cache.xOrigin || 0;\n\t\tyOrigin = cache.yOrigin || 0;\n\t\tif (matrix !== _identity2DMatrix) {\n\t\t\ta = matrix[0]; //a11\n\t\t\tb = matrix[1]; //a21\n\t\t\tc = matrix[2]; //a31\n\t\t\td = matrix[3]; //a41\n\t\t\tx = a12 = matrix[4];\n\t\t\ty = a22 = matrix[5];\n\n\t\t\t//2D matrix\n\t\t\tif (matrix.length === 6) {\n\t\t\t\tscaleX = Math.sqrt(a * a + b * b);\n\t\t\t\tscaleY = Math.sqrt(d * d + c * c);\n\t\t\t\trotation = (a || b) ? _atan2(b, a) * _RAD2DEG : 0; //note: if scaleX is 0, we cannot accurately measure rotation. Same for skewX with a scaleY of 0. Therefore, we default to the previously recorded value (or zero if that doesn't exist).\n\t\t\t\tskewX = (c || d) ? _atan2(c, d) * _RAD2DEG + rotation : 0;\n\t\t\t\tskewX && (scaleY *= Math.abs(Math.cos(skewX * _DEG2RAD)));\n\t\t\t\tif (cache.svg) {\n\t\t\t\t\tx -= xOrigin - (xOrigin * a + yOrigin * c);\n\t\t\t\t\ty -= yOrigin - (xOrigin * b + yOrigin * d);\n\t\t\t\t}\n\n\t\t\t//3D matrix\n\t\t\t} else {\n\t\t\t\ta32 = matrix[6];\n\t\t\t\ta42 = matrix[7];\n\t\t\t\ta13 = matrix[8];\n\t\t\t\ta23 = matrix[9];\n\t\t\t\ta33 = matrix[10];\n\t\t\t\ta43 = matrix[11];\n\t\t\t\tx = matrix[12];\n\t\t\t\ty = matrix[13];\n\t\t\t\tz = matrix[14];\n\n\t\t\t\tangle = _atan2(a32, a33);\n\t\t\t\trotationX = angle * _RAD2DEG;\n\t\t\t\t//rotationX\n\t\t\t\tif (angle) {\n\t\t\t\t\tcos = Math.cos(-angle);\n\t\t\t\t\tsin = Math.sin(-angle);\n\t\t\t\t\tt1 = a12*cos+a13*sin;\n\t\t\t\t\tt2 = a22*cos+a23*sin;\n\t\t\t\t\tt3 = a32*cos+a33*sin;\n\t\t\t\t\ta13 = a12*-sin+a13*cos;\n\t\t\t\t\ta23 = a22*-sin+a23*cos;\n\t\t\t\t\ta33 = a32*-sin+a33*cos;\n\t\t\t\t\ta43 = a42*-sin+a43*cos;\n\t\t\t\t\ta12 = t1;\n\t\t\t\t\ta22 = t2;\n\t\t\t\t\ta32 = t3;\n\t\t\t\t}\n\t\t\t\t//rotationY\n\t\t\t\tangle = _atan2(-c, a33);\n\t\t\t\trotationY = angle * _RAD2DEG;\n\t\t\t\tif (angle) {\n\t\t\t\t\tcos = Math.cos(-angle);\n\t\t\t\t\tsin = Math.sin(-angle);\n\t\t\t\t\tt1 = a*cos-a13*sin;\n\t\t\t\t\tt2 = b*cos-a23*sin;\n\t\t\t\t\tt3 = c*cos-a33*sin;\n\t\t\t\t\ta43 = d*sin+a43*cos;\n\t\t\t\t\ta = t1;\n\t\t\t\t\tb = t2;\n\t\t\t\t\tc = t3;\n\t\t\t\t}\n\t\t\t\t//rotationZ\n\t\t\t\tangle = _atan2(b, a);\n\t\t\t\trotation = angle * _RAD2DEG;\n\t\t\t\tif (angle) {\n\t\t\t\t\tcos = Math.cos(angle);\n\t\t\t\t\tsin = Math.sin(angle);\n\t\t\t\t\tt1 = a*cos+b*sin;\n\t\t\t\t\tt2 = a12*cos+a22*sin;\n\t\t\t\t\tb = b*cos-a*sin;\n\t\t\t\t\ta22 = a22*cos-a12*sin;\n\t\t\t\t\ta = t1;\n\t\t\t\t\ta12 = t2;\n\t\t\t\t}\n\n\t\t\t\tif (rotationX && Math.abs(rotationX) + Math.abs(rotation) > 359.9) { //when rotationY is set, it will often be parsed as 180 degrees different than it should be, and rotationX and rotation both being 180 (it looks the same), so we adjust for that here.\n\t\t\t\t\trotationX = rotation = 0;\n\t\t\t\t\trotationY = 180 - rotationY;\n\t\t\t\t}\n\t\t\t\tscaleX = _round(Math.sqrt(a * a + b * b + c * c));\n\t\t\t\tscaleY = _round(Math.sqrt(a22 * a22 + a32 * a32));\n\t\t\t\tangle = _atan2(a12, a22);\n\t\t\t\tskewX = (Math.abs(angle) > 0.0002) ? angle * _RAD2DEG : 0;\n\t\t\t\tperspective = a43 ? 1 / ((a43 < 0) ? -a43 : a43) : 0;\n\t\t\t}\n\n\t\t\tif (cache.svg) { //sense if there are CSS transforms applied on an SVG element in which case we must overwrite them when rendering. The transform attribute is more reliable cross-browser, but we can't just remove the CSS ones because they may be applied in a CSS rule somewhere (not just inline).\n\t\t\t\tt1 = target.getAttribute(\"transform\");\n\t\t\t\tcache.forceCSS = target.setAttribute(\"transform\", \"\") || (!_isNullTransform(_getComputedProperty(target, _transformProp)));\n\t\t\t\tt1 && target.setAttribute(\"transform\", t1);\n\t\t\t}\n\t\t}\n\n\t\tif (Math.abs(skewX) > 90 && Math.abs(skewX) < 270) {\n\t\t\tif (invertedScaleX) {\n\t\t\t\tscaleX *= -1;\n\t\t\t\tskewX += (rotation <= 0) ? 180 : -180;\n\t\t\t\trotation += (rotation <= 0) ? 180 : -180;\n\t\t\t} else {\n\t\t\t\tscaleY *= -1;\n\t\t\t\tskewX += (skewX <= 0) ? 180 : -180;\n\t\t\t}\n\t\t}\n\t\tuncache = uncache || cache.uncache;\n\t\tcache.x = x - ((cache.xPercent = x && ((!uncache && cache.xPercent) || (Math.round(target.offsetWidth / 2) === Math.round(-x) ? -50 : 0))) ? target.offsetWidth * cache.xPercent / 100 : 0) + px;\n\t\tcache.y = y - ((cache.yPercent = y && ((!uncache && cache.yPercent) || (Math.round(target.offsetHeight / 2) === Math.round(-y) ? -50 : 0))) ? target.offsetHeight * cache.yPercent / 100 : 0) + px;\n\t\tcache.z = z + px;\n\t\tcache.scaleX = _round(scaleX);\n\t\tcache.scaleY = _round(scaleY);\n\t\tcache.rotation = _round(rotation) + deg;\n\t\tcache.rotationX = _round(rotationX) + deg;\n\t\tcache.rotationY = _round(rotationY) + deg;\n\t\tcache.skewX = skewX + deg;\n\t\tcache.skewY = skewY + deg;\n\t\tcache.transformPerspective = perspective + px;\n\t\tif ((cache.zOrigin = parseFloat(origin.split(\" \")[2]) || (!uncache && cache.zOrigin) || 0)) {\n\t\t\tstyle[_transformOriginProp] = _firstTwoOnly(origin);\n\t\t}\n\t\tcache.xOffset = cache.yOffset = 0;\n\t\tcache.force3D = _config.force3D;\n\t\tcache.renderTransform = cache.svg ? _renderSVGTransforms : _supports3D ? _renderCSSTransforms : _renderNon3DTransforms;\n\t\tcache.uncache = 0;\n\t\treturn cache;\n\t},\n\t_firstTwoOnly = value => (value = value.split(\" \"))[0] + \" \" + value[1], //for handling transformOrigin values, stripping out the 3rd dimension\n\t_addPxTranslate = (target, start, value) => {\n\t\tlet unit = getUnit(start);\n\t\treturn _round(parseFloat(start) + parseFloat(_convertToUnit(target, \"x\", value + \"px\", unit))) + unit;\n\t},\n\t_renderNon3DTransforms = (ratio, cache) => {\n\t\tcache.z = \"0px\";\n\t\tcache.rotationY = cache.rotationX = \"0deg\";\n\t\tcache.force3D = 0;\n\t\t_renderCSSTransforms(ratio, cache);\n\t},\n\t_zeroDeg = \"0deg\",\n\t_zeroPx = \"0px\",\n\t_endParenthesis = \") \",\n\t_renderCSSTransforms = function(ratio, cache) {\n\t\tlet {xPercent, yPercent, x, y, z, rotation, rotationY, rotationX, skewX, skewY, scaleX, scaleY, transformPerspective, force3D, target, zOrigin} = cache || this,\n\t\t\ttransforms = \"\",\n\t\t\tuse3D = (force3D === \"auto\" && ratio && ratio !== 1) || force3D === true;\n\n\t\t// Safari has a bug that causes it not to render 3D transform-origin values properly, so we force the z origin to 0, record it in the cache, and then do the math here to offset the translate values accordingly (basically do the 3D transform-origin part manually)\n\t\tif (zOrigin && (rotationX !== _zeroDeg || rotationY !== _zeroDeg)) {\n\t\t\tlet angle = parseFloat(rotationY) * _DEG2RAD,\n\t\t\t\ta13 = Math.sin(angle),\n\t\t\t\ta33 = Math.cos(angle),\n\t\t\t\tcos;\n\t\t\tangle = parseFloat(rotationX) * _DEG2RAD;\n\t\t\tcos = Math.cos(angle);\n\t\t\tx = _addPxTranslate(target, x, a13 * cos * -zOrigin);\n\t\t\ty = _addPxTranslate(target, y, -Math.sin(angle) * -zOrigin);\n\t\t\tz = _addPxTranslate(target, z, a33 * cos * -zOrigin + zOrigin);\n\t\t}\n\n\t\tif (transformPerspective !== _zeroPx) {\n\t\t\ttransforms += \"perspective(\" + transformPerspective + _endParenthesis;\n\t\t}\n\t\tif (xPercent || yPercent) {\n\t\t\ttransforms += \"translate(\" + xPercent + \"%, \" + yPercent + \"%) \";\n\t\t}\n\t\tif (use3D || x !== _zeroPx || y !== _zeroPx || z !== _zeroPx) {\n\t\t\ttransforms += (z !== _zeroPx || use3D) ? \"translate3d(\" + x + \", \" + y + \", \" + z + \") \" : \"translate(\" + x + \", \" + y + _endParenthesis;\n\t\t}\n\t\tif (rotation !== _zeroDeg) {\n\t\t\ttransforms += \"rotate(\" + rotation + _endParenthesis;\n\t\t}\n\t\tif (rotationY !== _zeroDeg) {\n\t\t\ttransforms += \"rotateY(\" + rotationY + _endParenthesis;\n\t\t}\n\t\tif (rotationX !== _zeroDeg) {\n\t\t\ttransforms += \"rotateX(\" + rotationX + _endParenthesis;\n\t\t}\n\t\tif (skewX !== _zeroDeg || skewY !== _zeroDeg) {\n\t\t\ttransforms += \"skew(\" + skewX + \", \" + skewY + _endParenthesis;\n\t\t}\n\t\tif (scaleX !== 1 || scaleY !== 1) {\n\t\t\ttransforms += \"scale(\" + scaleX + \", \" + scaleY + _endParenthesis;\n\t\t}\n\t\ttarget.style[_transformProp] = transforms || \"translate(0, 0)\";\n\t},\n\t_renderSVGTransforms = function(ratio, cache) {\n\t\tlet {xPercent, yPercent, x, y, rotation, skewX, skewY, scaleX, scaleY, target, xOrigin, yOrigin, xOffset, yOffset, forceCSS} = cache || this,\n\t\t\ttx = parseFloat(x),\n\t\t\tty = parseFloat(y),\n\t\t\ta11, a21, a12, a22, temp;\n\t\trotation = parseFloat(rotation);\n\t\tskewX = parseFloat(skewX);\n\t\tskewY = parseFloat(skewY);\n\t\tif (skewY) { //for performance reasons, we combine all skewing into the skewX and rotation values. Remember, a skewY of 10 degrees looks the same as a rotation of 10 degrees plus a skewX of 10 degrees.\n\t\t\tskewY = parseFloat(skewY);\n\t\t\tskewX += skewY;\n\t\t\trotation += skewY;\n\t\t}\n\t\tif (rotation || skewX) {\n\t\t\trotation *= _DEG2RAD;\n\t\t\tskewX *= _DEG2RAD;\n\t\t\ta11 = Math.cos(rotation) * scaleX;\n\t\t\ta21 = Math.sin(rotation) * scaleX;\n\t\t\ta12 = Math.sin(rotation - skewX) * -scaleY;\n\t\t\ta22 = Math.cos(rotation - skewX) * scaleY;\n\t\t\tif (skewX) {\n\t\t\t\tskewY *= _DEG2RAD;\n\t\t\t\ttemp = Math.tan(skewX - skewY);\n\t\t\t\ttemp = Math.sqrt(1 + temp * temp);\n\t\t\t\ta12 *= temp;\n\t\t\t\ta22 *= temp;\n\t\t\t\tif (skewY) {\n\t\t\t\t\ttemp = Math.tan(skewY);\n\t\t\t\t\ttemp = Math.sqrt(1 + temp * temp);\n\t\t\t\t\ta11 *= temp;\n\t\t\t\t\ta21 *= temp;\n\t\t\t\t}\n\t\t\t}\n\t\t\ta11 = _round(a11);\n\t\t\ta21 = _round(a21);\n\t\t\ta12 = _round(a12);\n\t\t\ta22 = _round(a22);\n\t\t} else {\n\t\t\ta11 = scaleX;\n\t\t\ta22 = scaleY;\n\t\t\ta21 = a12 = 0;\n\t\t}\n\t\tif ((tx && !~(x + \"\").indexOf(\"px\")) || (ty && !~(y + \"\").indexOf(\"px\"))) {\n\t\t\ttx = _convertToUnit(target, \"x\", x, \"px\");\n\t\t\tty = _convertToUnit(target, \"y\", y, \"px\");\n\t\t}\n\t\tif (xOrigin || yOrigin || xOffset || yOffset) {\n\t\t\ttx = _round(tx + xOrigin - (xOrigin * a11 + yOrigin * a12) + xOffset);\n\t\t\tty = _round(ty + yOrigin - (xOrigin * a21 + yOrigin * a22) + yOffset);\n\t\t}\n\t\tif (xPercent || yPercent) {\n\t\t\t//The SVG spec doesn't support percentage-based translation in the \"transform\" attribute, so we merge it into the translation to simulate it.\n\t\t\ttemp = target.getBBox();\n\t\t\ttx = _round(tx + xPercent / 100 * temp.width);\n\t\t\tty = _round(ty + yPercent / 100 * temp.height);\n\t\t}\n\t\ttemp = \"matrix(\" + a11 + \",\" + a21 + \",\" + a12 + \",\" + a22 + \",\" + tx + \",\" + ty + \")\";\n\t\ttarget.setAttribute(\"transform\", temp);\n\t\tforceCSS && (target.style[_transformProp] = temp); //some browsers prioritize CSS transforms over the transform attribute. When we sense that the user has CSS transforms applied, we must overwrite them this way (otherwise some browser simply won't render the transform attribute changes!)\n\t},\n\t_addRotationalPropTween = function(plugin, target, property, startNum, endValue) {\n\t\tlet cap = 360,\n\t\t\tisString = _isString(endValue),\n\t\t\tendNum = parseFloat(endValue) * ((isString && ~endValue.indexOf(\"rad\")) ? _RAD2DEG : 1),\n\t\t\tchange = endNum - startNum,\n\t\t\tfinalValue = (startNum + change) + \"deg\",\n\t\t\tdirection, pt;\n\t\tif (isString) {\n\t\t\tdirection = endValue.split(\"_\")[1];\n\t\t\tif (direction === \"short\") {\n\t\t\t\tchange %= cap;\n\t\t\t\tif (change !== change % (cap / 2)) {\n\t\t\t\t\tchange += (change < 0) ? cap : -cap;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (direction === \"cw\" && change < 0) {\n\t\t\t\tchange = ((change + cap * _bigNum) % cap) - ~~(change / cap) * cap;\n\t\t\t} else if (direction === \"ccw\" && change > 0) {\n\t\t\t\tchange = ((change - cap * _bigNum) % cap) - ~~(change / cap) * cap;\n\t\t\t}\n\t\t}\n\t\tplugin._pt = pt = new PropTween(plugin._pt, target, property, startNum, change, _renderPropWithEnd);\n\t\tpt.e = finalValue;\n\t\tpt.u = \"deg\";\n\t\tplugin._props.push(property);\n\t\treturn pt;\n\t},\n\t_assign = (target, source) => { // Internet Explorer doesn't have Object.assign(), so we recreate it here.\n\t\tfor (let p in source) {\n\t\t\ttarget[p] = source[p];\n\t\t}\n\t\treturn target;\n\t},\n\t_addRawTransformPTs = (plugin, transforms, target) => { //for handling cases where someone passes in a whole transform string, like transform: \"scale(2, 3) rotate(20deg) translateY(30em)\"\n\t\tlet startCache = _assign({}, target._gsap),\n\t\t\texclude = \"perspective,force3D,transformOrigin,svgOrigin\",\n\t\t\tstyle = target.style,\n\t\t\tendCache, p, startValue, endValue, startNum, endNum, startUnit, endUnit;\n\t\tif (startCache.svg) {\n\t\t\tstartValue = target.getAttribute(\"transform\");\n\t\t\ttarget.setAttribute(\"transform\", \"\");\n\t\t\tstyle[_transformProp] = transforms;\n\t\t\tendCache = _parseTransform(target, 1);\n\t\t\t_removeProperty(target, _transformProp);\n\t\t\ttarget.setAttribute(\"transform\", startValue);\n\t\t} else {\n\t\t\tstartValue = getComputedStyle(target)[_transformProp];\n\t\t\tstyle[_transformProp] = transforms;\n\t\t\tendCache = _parseTransform(target, 1);\n\t\t\tstyle[_transformProp] = startValue;\n\t\t}\n\t\tfor (p in _transformProps) {\n\t\t\tstartValue = startCache[p];\n\t\t\tendValue = endCache[p];\n\t\t\tif (startValue !== endValue && exclude.indexOf(p) < 0) { //tweening to no perspective gives very unintuitive results - just keep the same perspective in that case.\n\t\t\t\tstartUnit = getUnit(startValue);\n\t\t\t\tendUnit = getUnit(endValue);\n\t\t\t\tstartNum = (startUnit !== endUnit) ? _convertToUnit(target, p, startValue, endUnit) : parseFloat(startValue);\n\t\t\t\tendNum = parseFloat(endValue);\n\t\t\t\tplugin._pt = new PropTween(plugin._pt, endCache, p, startNum, endNum - startNum, _renderCSSProp);\n\t\t\t\tplugin._pt.u = endUnit || 0;\n\t\t\t\tplugin._props.push(p);\n\t\t\t}\n\t\t}\n\t\t_assign(endCache, startCache);\n\t};\n\n// handle splitting apart padding, margin, borderWidth, and borderRadius into their 4 components. Firefox, for example, won't report borderRadius correctly - it will only do borderTopLeftRadius and the other corners. We also want to handle paddingTop, marginLeft, borderRightWidth, etc.\n_forEachName(\"padding,margin,Width,Radius\", (name, index) => {\n\tlet t = \"Top\",\n\t\tr = \"Right\",\n\t\tb = \"Bottom\",\n\t\tl = \"Left\",\n\t\tprops = (index < 3 ? [t,r,b,l] : [t+l, t+r, b+r, b+l]).map(side => index < 2 ? name + side : \"border\" + side + name);\n\t_specialProps[(index > 1 ? \"border\" + name : name)] = function(plugin, target, property, endValue, tween) {\n\t\tlet a, vars;\n\t\tif (arguments.length < 4) { // getter, passed target, property, and unit (from _get())\n\t\t\ta = props.map(prop => _get(plugin, prop, property));\n\t\t\tvars = a.join(\" \");\n\t\t\treturn vars.split(a[0]).length === 5 ? a[0] : vars;\n\t\t}\n\t\ta = (endValue + \"\").split(\" \");\n\t\tvars = {};\n\t\tprops.forEach((prop, i) => vars[prop] = a[i] = a[i] || a[(((i - 1) / 2) | 0)]);\n\t\tplugin.init(target, vars, tween);\n\t}\n});\n\n\nexport const CSSPlugin = {\n\tname: \"css\",\n\tregister: _initCore,\n\ttargetTest(target) {\n\t\treturn target.style && target.nodeType;\n\t},\n\tinit(target, vars, tween, index, targets) {\n\t\tlet props = this._props,\n\t\t\tstyle = target.style,\n\t\t\tstartAt = tween.vars.startAt,\n\t\t\tstartValue, endValue, endNum, startNum, type, specialProp, p, startUnit, endUnit, relative, isTransformRelated, transformPropTween, cache, smooth, hasPriority, inlineProps;\n\t\t_pluginInitted || _initCore();\n\t\t// we may call init() multiple times on the same plugin instance, like when adding special properties, so make sure we don't overwrite the revert data or inlineProps\n\t\tthis.styles = this.styles || _getStyleSaver(target);\n\t\tinlineProps = this.styles.props;\n\t\tthis.tween = tween;\n\t\tfor (p in vars) {\n\t\t\tif (p === \"autoRound\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tendValue = vars[p];\n\t\t\tif (_plugins[p] && _checkPlugin(p, vars, tween, index, target, targets)) { // plugins\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttype = typeof(endValue);\n\t\t\tspecialProp = _specialProps[p];\n\t\t\tif (type === \"function\") {\n\t\t\t\tendValue = endValue.call(tween, index, target, targets);\n\t\t\t\ttype = typeof(endValue);\n\t\t\t}\n\t\t\tif (type === \"string\" && ~endValue.indexOf(\"random(\")) {\n\t\t\t\tendValue = _replaceRandom(endValue);\n\t\t\t}\n\t\t\tif (specialProp) {\n\t\t\t\tspecialProp(this, target, p, endValue, tween) && (hasPriority = 1);\n\t\t\t} else if (p.substr(0,2) === \"--\") { //CSS variable\n\t\t\t\tstartValue = (getComputedStyle(target).getPropertyValue(p) + \"\").trim();\n\t\t\t\tendValue += \"\";\n\t\t\t\t_colorExp.lastIndex = 0;\n\t\t\t\tif (!_colorExp.test(startValue)) { // colors don't have units\n\t\t\t\t\tstartUnit = getUnit(startValue);\n\t\t\t\t\tendUnit = getUnit(endValue);\n\t\t\t\t}\n\t\t\t\tendUnit ? startUnit !== endUnit && (startValue = _convertToUnit(target, p, startValue, endUnit) + endUnit) : startUnit && (endValue += startUnit);\n\t\t\t\tthis.add(style, \"setProperty\", startValue, endValue, index, targets, 0, 0, p);\n\t\t\t\tprops.push(p);\n\t\t\t\tinlineProps.push(p, 0, style[p]);\n\t\t\t} else if (type !== \"undefined\") {\n\t\t\t\tif (startAt && p in startAt) { // in case someone hard-codes a complex value as the start, like top: \"calc(2vh / 2)\". Without this, it'd use the computed value (always in px)\n\t\t\t\t\tstartValue = typeof(startAt[p]) === \"function\" ? startAt[p].call(tween, index, target, targets) : startAt[p];\n\t\t\t\t\t_isString(startValue) && ~startValue.indexOf(\"random(\") && (startValue = _replaceRandom(startValue));\n\t\t\t\t\tgetUnit(startValue + \"\") || startValue === \"auto\" || (startValue += _config.units[p] || getUnit(_get(target, p)) || \"\"); // for cases when someone passes in a unitless value like {x: 100}; if we try setting translate(100, 0px) it won't work.\n\t\t\t\t\t(startValue + \"\").charAt(1) === \"=\" && (startValue = _get(target, p)); // can't work with relative values\n\t\t\t\t} else {\n\t\t\t\t\tstartValue = _get(target, p);\n\t\t\t\t}\n\t\t\t\tstartNum = parseFloat(startValue);\n\t\t\t\trelative = (type === \"string\" && endValue.charAt(1) === \"=\") && endValue.substr(0, 2);\n\t\t\t\trelative && (endValue = endValue.substr(2));\n\t\t\t\tendNum = parseFloat(endValue);\n\t\t\t\tif (p in _propertyAliases) {\n\t\t\t\t\tif (p === \"autoAlpha\") { //special case where we control the visibility along with opacity. We still allow the opacity value to pass through and get tweened.\n\t\t\t\t\t\tif (startNum === 1 && _get(target, \"visibility\") === \"hidden\" && endNum) { //if visibility is initially set to \"hidden\", we should interpret that as intent to make opacity 0 (a convenience)\n\t\t\t\t\t\t\tstartNum = 0;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tinlineProps.push(\"visibility\", 0, style.visibility);\n\t\t\t\t\t\t_addNonTweeningPT(this, style, \"visibility\", startNum ? \"inherit\" : \"hidden\", endNum ? \"inherit\" : \"hidden\", !endNum);\n\t\t\t\t\t}\n\t\t\t\t\tif (p !== \"scale\" && p !== \"transform\") {\n\t\t\t\t\t\tp = _propertyAliases[p];\n\t\t\t\t\t\t~p.indexOf(\",\") && (p = p.split(\",\")[0]);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tisTransformRelated = (p in _transformProps);\n\n\t\t\t\t//--- TRANSFORM-RELATED ---\n\t\t\t\tif (isTransformRelated) {\n\t\t\t\t\tthis.styles.save(p);\n\t\t\t\t\tif (!transformPropTween) {\n\t\t\t\t\t\tcache = target._gsap;\n\t\t\t\t\t\t(cache.renderTransform && !vars.parseTransform) || _parseTransform(target, vars.parseTransform); // if, for example, gsap.set(... {transform:\"translateX(50vw)\"}), the _get() call doesn't parse the transform, thus cache.renderTransform won't be set yet so force the parsing of the transform here.\n\t\t\t\t\t\tsmooth = (vars.smoothOrigin !== false && cache.smooth);\n\t\t\t\t\t\ttransformPropTween = this._pt = new PropTween(this._pt, style, _transformProp, 0, 1, cache.renderTransform, cache, 0, -1); //the first time through, create the rendering PropTween so that it runs LAST (in the linked list, we keep adding to the beginning)\n\t\t\t\t\t\ttransformPropTween.dep = 1; //flag it as dependent so that if things get killed/overwritten and this is the only PropTween left, we can safely kill the whole tween.\n\t\t\t\t\t}\n\t\t\t\t\tif (p === \"scale\") {\n\t\t\t\t\t\tthis._pt = new PropTween(this._pt, cache, \"scaleY\", cache.scaleY, ((relative ? _parseRelative(cache.scaleY, relative + endNum) : endNum) - cache.scaleY) || 0, _renderCSSProp);\n\t\t\t\t\t\tthis._pt.u = 0;\n\t\t\t\t\t\tprops.push(\"scaleY\", p);\n\t\t\t\t\t\tp += \"X\";\n\t\t\t\t\t} else if (p === \"transformOrigin\") {\n\t\t\t\t\t\tinlineProps.push(_transformOriginProp, 0, style[_transformOriginProp]);\n\t\t\t\t\t\tendValue = _convertKeywordsToPercentages(endValue); //in case something like \"left top\" or \"bottom right\" is passed in. Convert to percentages.\n\t\t\t\t\t\tif (cache.svg) {\n\t\t\t\t\t\t\t_applySVGOrigin(target, endValue, 0, smooth, 0, this);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tendUnit = parseFloat(endValue.split(\" \")[2]) || 0; //handle the zOrigin separately!\n\t\t\t\t\t\t\tendUnit !== cache.zOrigin && _addNonTweeningPT(this, cache, \"zOrigin\", cache.zOrigin, endUnit);\n\t\t\t\t\t\t\t_addNonTweeningPT(this, style, p, _firstTwoOnly(startValue), _firstTwoOnly(endValue));\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if (p === \"svgOrigin\") {\n\t\t\t\t\t\t_applySVGOrigin(target, endValue, 1, smooth, 0, this);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if (p in _rotationalProperties) {\n\t\t\t\t\t\t_addRotationalPropTween(this, cache, p, startNum, relative ? _parseRelative(startNum, relative + endValue) : endValue);\n\t\t\t\t\t\tcontinue;\n\n\t\t\t\t\t} else if (p === \"smoothOrigin\") {\n\t\t\t\t\t\t_addNonTweeningPT(this, cache, \"smooth\", cache.smooth, endValue);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if (p === \"force3D\") {\n\t\t\t\t\t\tcache[p] = endValue;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t} else if (p === \"transform\") {\n\t\t\t\t\t\t_addRawTransformPTs(this, endValue, target);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else if (!(p in style)) {\n\t\t\t\t\tp = _checkPropPrefix(p) || p;\n\t\t\t\t}\n\n\t\t\t\tif (isTransformRelated || ((endNum || endNum === 0) && (startNum || startNum === 0) && !_complexExp.test(endValue) && (p in style))) {\n\t\t\t\t\tstartUnit = (startValue + \"\").substr((startNum + \"\").length);\n\t\t\t\t\tendNum || (endNum = 0); // protect against NaN\n\t\t\t\t\tendUnit = getUnit(endValue) || ((p in _config.units) ? _config.units[p] : startUnit);\n\t\t\t\t\tstartUnit !== endUnit && (startNum = _convertToUnit(target, p, startValue, endUnit));\n\t\t\t\t\tthis._pt = new PropTween(this._pt, isTransformRelated ? cache : style, p, startNum, (relative ? _parseRelative(startNum, relative + endNum) : endNum) - startNum, (!isTransformRelated && (endUnit === \"px\" || p === \"zIndex\") && vars.autoRound !== false) ? _renderRoundedCSSProp : _renderCSSProp);\n\t\t\t\t\tthis._pt.u = endUnit || 0;\n\t\t\t\t\tif (startUnit !== endUnit && endUnit !== \"%\") { //when the tween goes all the way back to the beginning, we need to revert it to the OLD/ORIGINAL value (with those units). We record that as a \"b\" (beginning) property and point to a render method that handles that. (performance optimization)\n\t\t\t\t\t\tthis._pt.b = startValue;\n\t\t\t\t\t\tthis._pt.r = _renderCSSPropWithBeginning;\n\t\t\t\t\t}\n\t\t\t\t} else if (!(p in style)) {\n\t\t\t\t\tif (p in target) { //maybe it's not a style - it could be a property added directly to an element in which case we'll try to animate that.\n\t\t\t\t\t\tthis.add(target, p, startValue || target[p], relative ? relative + endValue : endValue, index, targets);\n\t\t\t\t\t} else if (p !== \"parseTransform\") {\n\t\t\t\t\t\t_missingPlugin(p, endValue);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t_tweenComplexCSSString.call(this, target, p, startValue, relative ? relative + endValue : endValue);\n\t\t\t\t}\n\t\t\t\tisTransformRelated || (p in style ? inlineProps.push(p, 0, style[p]) : typeof(target[p]) === \"function\" ? inlineProps.push(p, 2, target[p]()) : inlineProps.push(p, 1, startValue || target[p]));\n\t\t\t\tprops.push(p);\n\t\t\t}\n\t\t}\n\t\thasPriority && _sortPropTweensByPriority(this);\n\n\t},\n\trender(ratio, data) {\n\t\tif (data.tween._time || !_reverting()) {\n\t\t\tlet pt = data._pt;\n\t\t\twhile (pt) {\n\t\t\t\tpt.r(ratio, pt.d);\n\t\t\t\tpt = pt._next;\n\t\t\t}\n\t\t} else {\n\t\t\tdata.styles.revert();\n\t\t}\n\t},\n\tget: _get,\n\taliases: _propertyAliases,\n\tgetSetter(target, property, plugin) { //returns a setter function that accepts target, property, value and applies it accordingly. Remember, properties like \"x\" aren't as simple as target.style.property = value because they've got to be applied to a proxy object and then merged into a transform string in a renderer.\n\t\tlet p = _propertyAliases[property];\n\t\t(p && p.indexOf(\",\") < 0) && (property = p);\n\t\treturn (property in _transformProps && property !== _transformOriginProp && (target._gsap.x || _get(target, \"x\"))) ? (plugin && _recentSetterPlugin === plugin ? (property === \"scale\" ? _setterScale : _setterTransform) : (_recentSetterPlugin = plugin || {}) && (property === \"scale\" ? _setterScaleWithRender : _setterTransformWithRender)) : target.style && !_isUndefined(target.style[property]) ? _setterCSSStyle : ~property.indexOf(\"-\") ? _setterCSSProp : _getSetter(target, property);\n\t},\n\tcore: { _removeProperty, _getMatrix }\n\n};\n\ngsap.utils.checkPrefix = _checkPropPrefix;\ngsap.core.getStyleSaver = _getStyleSaver;\n(function(positionAndScale, rotation, others, aliases) {\n\tlet all = _forEachName(positionAndScale + \",\" + rotation + \",\" + others, name => {_transformProps[name] = 1});\n\t_forEachName(rotation, name => {_config.units[name] = \"deg\"; _rotationalProperties[name] = 1});\n\t_propertyAliases[all[13]] = positionAndScale + \",\" + rotation;\n\t_forEachName(aliases, name => {\n\t\tlet split = name.split(\":\");\n\t\t_propertyAliases[split[1]] = all[split[0]];\n\t});\n})(\"x,y,z,scale,scaleX,scaleY,xPercent,yPercent\", \"rotation,rotationX,rotationY,skewX,skewY\", \"transform,transformOrigin,svgOrigin,force3D,smoothOrigin,transformPerspective\", \"0:translateX,1:translateY,2:translateZ,8:rotate,8:rotationZ,8:rotateZ,9:rotateX,10:rotateY\");\n_forEachName(\"x,y,z,top,right,bottom,left,width,height,fontSize,padding,margin,perspective\", name => {_config.units[name] = \"px\"});\n\ngsap.registerPlugin(CSSPlugin);\n\nexport { CSSPlugin as default, _getBBox, _createElement, _checkPropPrefix as checkPrefix };","import { gsap, Power0, Power1, Power2, Power3, Power4, Linear, Quad, Cubic, Quart, Quint, Strong, Elastic, Back, SteppedEase, Bounce, Sine, Expo, Circ, TweenLite, TimelineLite, TimelineMax } from \"./gsap-core.js\";\nimport { CSSPlugin } from \"./CSSPlugin.js\";\n\nconst gsapWithCSS = gsap.registerPlugin(CSSPlugin) || gsap, // to protect from tree shaking\n\tTweenMaxWithCSS = gsapWithCSS.core.Tween;\n\nexport {\n\tgsapWithCSS as gsap,\n\tgsapWithCSS as default,\n\tCSSPlugin,\n\tTweenMaxWithCSS as TweenMax,\n\tTweenLite,\n\tTimelineMax,\n\tTimelineLite,\n\tPower0,\n\tPower1,\n\tPower2,\n\tPower3,\n\tPower4,\n\tLinear,\n\tQuad,\n\tCubic,\n\tQuart,\n\tQuint,\n\tStrong,\n\tElastic,\n\tBack,\n\tSteppedEase,\n\tBounce,\n\tSine,\n\tExpo,\n\tCirc\n};"],"names":["_isString","value","_isFunction","_isNumber","_isUndefined","_isObject","_isNotFalse","_windowExists","window","_isFuncOrString","_install","scope","_installScope","_merge","_globals","gsap","_missingPlugin","property","console","warn","_warn","message","suppress","_addGlobal","name","obj","_emptyFunc","_harness","targets","harnessPlugin","i","target","_gsap","harness","_harnessPlugins","length","targetTest","GSCache","splice","_getCache","toArray","_getProperty","v","getAttribute","_forEachName","names","func","split","forEach","_round","Math","round","_roundPrecise","_parseRelative","start","operator","charAt","end","parseFloat","substr","_arrayContainsAny","toSearch","toFind","l","indexOf","_lazyRender","tween","_lazyTweens","a","slice","_lazyLookup","_lazy","render","_lazySafeRender","animation","time","suppressEvents","force","_reverting","_initted","_startAt","_numericIfPossible","n","match","_delimitedValueExp","trim","_passThrough","p","_setDefaults","defaults","_mergeDeep","base","toMerge","_copyExcluding","excluding","copy","_inheritDefaults","vars","parent","_globalTimeline","keyframes","_setKeyframeDefaults","excludeDuration","_isArray","inherit","_dp","_addLinkedListItem","child","firstProp","lastProp","sortBy","t","prev","_prev","_next","_removeLinkedListItem","next","_removeFromParent","onlyIfParentHasAutoRemove","autoRemoveChildren","remove","_act","_uncache","_end","_dur","_start","_dirty","_rewindStartAt","totalTime","revert","_revertConfigNoKill","immediateRender","autoRevert","_elapsedCycleDuration","_repeat","_animationCycle","_tTime","duration","_rDelay","_parentToChildTotalTime","parentTime","_ts","totalDuration","_tDur","_setEnd","abs","_rts","_tinyNum","_alignPlayhead","smoothChildTiming","_time","_postAddChecks","timeline","add","rawTime","_clamp","_zTime","_addToTimeline","position","skipChecks","_parsePosition","_delay","timeScale","_sort","_isFromOrFromStart","_recent","_scrollTrigger","trigger","ScrollTrigger","create","_attemptInitTween","tTime","_initTween","_pt","lazy","_lastRenderedFrame","_ticker","frame","push","_setDuration","skipUncache","leavePlayhead","repeat","dur","totalProgress","_onUpdateTotalDuration","Timeline","_createTweenType","type","params","irVars","isLegacy","varsIndex","runBackwards","startAt","Tween","_conditionalReturn","getUnit","_unitExp","exec","_isArrayLike","nonEmpty","nodeType","_win","selector","el","current","nativeElement","querySelectorAll","_doc","createElement","shuffle","sort","random","distribute","each","ease","_parseEase","from","cache","isDecimal","ratios","isNaN","axis","ratioX","ratioY","center","edges","originX","originY","x","y","d","j","max","min","wrapAt","distances","grid","_bigNum","getBoundingClientRect","left","_sqrt","amount","b","u","_invertEase","_roundModifier","pow","raw","snap","snapTo","radius","is2D","isArray","values","increment","dx","dy","closest","roundingIncrement","returnFunction","floor","_wrapArray","wrapper","index","_replaceRandom","nums","s","_strictNumExp","_getLabelInDirection","fromTime","backward","distance","label","labels","_interrupt","scrollTrigger","kill","progress","_callback","_createPlugin","config","headless","isFunc","Plugin","init","_props","instanceDefaults","_renderPropTweens","_addPropTween","_killPropTweensOf","modifier","_addPluginModifier","rawVars","statics","get","getSetter","_getSetter","aliases","register","_wake","_plugins","prototype","prop","_reservedProps","toUpperCase","PropTween","_registerPluginQueue","_hue","h","m1","m2","_255","splitColor","toHSL","forceAlpha","r","g","wasHSL","_colorLookup","black","parseInt","_numExp","transparent","map","Number","_colorOrderData","c","_colorExp","_numWithUnitExp","_formatColors","orderMatchData","shell","result","colors","color","join","replace","shift","_colorStringFilter","combined","lastIndex","test","_hslExp","_configEaseFromString","_easeMap","apply","_parseObjectInString","val","parsedVal","key","lastIndexOf","_quotesExp","_valueInParentheses","open","close","nested","substring","_CE","_customEaseExp","_propagateYoyoEase","isYoyo","_first","yoyoEase","_yoyo","_ease","_yEase","_insertEase","easeIn","easeOut","easeInOut","lowercaseName","toLowerCase","_easeInOutFromOut","_configElastic","amplitude","period","p1","_sin","p3","p2","_2PI","asin","_configBack","overshoot","_suppressOverwrites","_context","_coreInitted","_coreReady","_quickTween","_tickerActive","_id","_req","_raf","_self","_delta","_i","_getTime","_lagThreshold","_adjustedLag","_startTime","_lastUpdate","_gap","_nextTime","_listeners","n1","_config","autoSleep","force3D","nullTargetWarn","units","lineHeight","_defaults","overwrite","delay","PI","_HALF_PI","_gsID","sqrt","_cos","cos","sin","_isTypedArray","ArrayBuffer","isView","Array","_complexStringNumExp","_relExp","_startAtRevertConfig","isStart","_revertConfig","_effects","_nextGCFrame","_callbackNames","cycleDuration","whole","data","_zeroPosition","endTime","percentAnimation","offset","isPercent","recent","clippedDuration","_slice","leaveStrings","_flatten","ar","accumulator","call","mapRange","inMin","inMax","outMin","outMax","inRange","outRange","executeLazyFirst","callback","prevContext","context","_ctx","callbackScope","aqua","lime","silver","maroon","teal","blue","navy","white","olive","yellow","orange","gray","purple","green","red","pink","cyan","RegExp","Date","now","tick","_tick","deltaRatio","fps","wake","document","gsapVersions","version","GreenSockGlobals","requestAnimationFrame","sleep","f","setTimeout","cancelAnimationFrame","clearTimeout","lagSmoothing","threshold","adjustedLag","Infinity","once","prioritize","defaultEase","overlap","dispatch","elapsed","manual","power","Linear","easeNone","none","SteppedEase","steps","immediateStart","id","this","set","Animation","startTime","arguments","_ptLookup","_pTime","iteration","_ps","_recacheAncestors","paused","includeRepeats","wrapRepeats","prevIsReverting","globalTime","_sat","repeatDelay","yoyo","seek","restart","includeDelay","play","reversed","reverse","pause","atTime","resume","invalidate","isActive","eventCallback","_onUpdate","then","onFulfilled","self","Promise","resolve","_resolve","_then","_prom","ratio","sortChildren","_this","to","fromTo","fromVars","toVars","delayedCall","staggerTo","stagger","onCompleteAll","onCompleteAllParams","onComplete","onCompleteParams","staggerFrom","staggerFromTo","prevPaused","pauseTween","prevStart","prevIteration","prevTime","tDur","crossingStart","_lock","rewinding","doesWrap","repeatRefresh","onRepeat","_hasPause","_forcing","_findNextPauseTween","_last","onUpdate","adjustedTime","_this2","addLabel","getChildren","tweens","timelines","ignoreBeforeTime","getById","animations","removeLabel","killTweensOf","addPause","removePause","props","onlyActive","getTweensOf","_overwritingTween","children","parsedTargets","isGlobalTime","_targets","tweenTo","initted","tl","onStart","onStartParams","tweenFromTo","fromPosition","toPosition","nextLabel","afterTime","previousLabel","beforeTime","currentLabel","shiftChildren","adjustLabels","soft","clear","includeLabels","updateRoot","_checkPlugin","plugin","pt","ptLookup","_processVars","_parseFuncOrString","style","priority","_parseKeyframe","allProps","easeEach","e","_forceAllPropTweens","stringFilter","funcParam","optional","currentValue","parsedStart","setter","_setterFuncWithParam","_setterFunc","_setterPlain","_addComplexStringPropTween","startNums","endNum","chunk","startNum","hasRandom","_renderComplexString","matchIndex","m","fp","_renderBoolean","_renderPlain","cleanVars","hasPriority","gsData","harnessVars","overwritten","prevStartAt","fullTargets","autoOverwrite","_overwrite","_from","_ptCache","_op","_sortPropTweensByPriority","_onInit","_staggerTweenProps","_staggerPropsToSkip","skipInherit","curTarget","staggerFunc","staggerVarsToMerge","_this3","kf","_hasNoPausedAncestors","isNegative","_renderZeroDurationTween","prevRatio","_parentPlayheadIsBeforeStart","resetTo","startIsRelative","skipRecursion","_updatePropTweens","rootPT","lookup","ptCache","overwrittenProps","curLookup","curOverwriteProps","killingTargets","propTweenLookup","firstPT","_arraysMatch","a1","a2","_addAliasesToVars","propertyAliases","onReverseComplete","onReverseCompleteParams","_setterAttribute","setAttribute","_setterWithModifier","mSet","mt","hasNonDependentRemaining","op","dep","pt2","first","last","pr","change","renderer","TweenMax","TweenLite","TimelineLite","TimelineMax","_dispatch","_emptyArray","_onMediaChange","matches","_lastMediaTime","_media","anyMatch","toggled","queries","conditions","matchMedia","onMatch","_contextID","Context","prevSelector","_r","isReverted","ignore","getTweens","_this4","o","MatchMedia","mq","active","cond","contexts","addListener","addEventListener","registerPlugin","args","getProperty","unit","uncache","getter","format","quickSetter","setters","quickTo","isTweening","registerEffect","effect","plugins","extendTimeline","pluginName","registerEase","parseEase","exportRoot","includeDelayedCalls","matchMediaRefresh","found","removeEventListener","utils","wrap","range","wrapYoyo","total","normalize","clamp","pipe","functions","reduce","unitize","interpolate","mutate","interpolators","il","isString","master","install","effects","ticker","globalTimeline","core","globals","getCache","reverting","toAdd","suppressOverwrites","_getPluginPropTween","_buildModifierPlugin","temp","_addModifiers","modifiers","_renderCSSProp","_renderPropWithEnd","_renderCSSPropWithBeginning","_renderRoundedCSSProp","_renderNonTweeningValue","_renderNonTweeningValueOnlyAtEnd","_setterCSSStyle","_setterCSSProp","setProperty","_setterTransform","_setterScale","scaleX","scaleY","_setterScaleWithRender","renderTransform","_setterTransformWithRender","_saveStyle","isNotCSS","_transformProps","tfm","_propertyAliases","transform","_get","_transformOriginProp","zOrigin","_transformProp","svg","svgo","_removeIndependentTransforms","translate","removeProperty","_revertStyle","_capsExp","_getStyleSaver","properties","saver","save","_createElement","ns","createElementNS","_getComputedProperty","skipPrefixFallback","cs","getComputedStyle","getPropertyValue","_checkPropPrefix","_initCore","_docElement","documentElement","_tempDiv","cssText","_supports3D","_pluginInitted","_getReparentedCloneBBox","bbox","owner","ownerSVGElement","clone","cloneNode","display","appendChild","getBBox","removeChild","_getAttributeFallbacks","attributesArray","hasAttribute","_getBBox","bounds","cloned","error","width","height","_isSVG","getCTM","parentNode","_removeProperty","first2Chars","removeAttribute","_addNonTweeningPT","beginning","onlySetAtEnd","_convertToUnit","px","isSVG","curValue","curUnit","horizontal","_horizontalExp","isRootSVG","tagName","measureProperty","toPixels","toPercent","_nonConvertibleUnits","body","_nonStandardLayouts","_tweenComplexCSSString","startValues","startValue","endValue","endUnit","startUnit","_convertKeywordsToPercentages","_keywordToPercent","_renderClearProps","clearTransforms","scale","rotate","_parseTransform","_isNullTransform","_getComputedTransformMatrixAsArray","matrixString","_identity2DMatrix","_getMatrix","force2D","nextSibling","addedToDOM","matrix","baseVal","consolidate","offsetParent","nextElementSibling","insertBefore","_applySVGOrigin","origin","originIsAbsolute","smooth","matrixArray","pluginToAddPropTweensTo","determinant","xOriginOld","xOrigin","yOriginOld","yOrigin","xOffsetOld","xOffset","yOffsetOld","yOffset","tx","ty","originSplit","_addPxTranslate","_addRotationalPropTween","direction","cap","_RAD2DEG","finalValue","_assign","source","_addRawTransformPTs","transforms","endCache","startCache","_recentSetterPlugin","Power0","Power1","Power2","Power3","Power4","Quad","Cubic","Quart","Quint","Strong","Elastic","Back","Bounce","Sine","Expo","Circ","_DEG2RAD","_atan2","atan2","_complexExp","autoAlpha","alpha","_prefixes","element","preferPrefix","deg","rad","turn","flex","_firstTwoOnly","_specialProps","top","bottom","right","clearProps","_rotationalProperties","z","rotation","rotationX","rotationY","skewX","skewY","perspective","angle","a12","a22","t1","t2","t3","a13","a23","a33","a42","a43","a32","invertedScaleX","forceCSS","xPercent","offsetWidth","yPercent","offsetHeight","transformPerspective","_renderSVGTransforms","_renderCSSTransforms","_renderNon3DTransforms","_zeroDeg","_zeroPx","_endParenthesis","use3D","a11","a21","tan","side","positionAndScale","all","CSSPlugin","specialProp","relative","isTransformRelated","transformPropTween","inlineProps","styles","visibility","parseTransform","smoothOrigin","autoRound","checkPrefix","getStyleSaver","gsapWithCSS","TweenMaxWithCSS"],"mappings":"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"} \ No newline at end of file diff --git a/dot-line-system/public/images/0_2.png b/dot-line-system/public/images/0_2.png new file mode 100644 index 0000000000000000000000000000000000000000..008cbfa5688a6e53035b02e4874b0d129f3c9f78 GIT binary patch literal 28961 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 ZegU5S3vThB`4|8I002ovPDHLkV1o0`j)?#O literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/0_3.png b/dot-line-system/public/images/0_3.png new file mode 100644 index 0000000000000000000000000000000000000000..6abd17a05f1f5ac03b5b80f0cad222e149920cc3 GIT binary patch literal 42619 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 literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/disco.png b/dot-line-system/public/images/disco.png new file mode 100644 index 0000000000000000000000000000000000000000..8588d2a3aed4ce150cbe07948569a708a8e570fb GIT binary patch literal 25986 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 b`_hEAFLcL-wuDMZg7|T9ba%LK&q@9tTk(Ci literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/entspannung.png b/dot-line-system/public/images/entspannung.png new file mode 100644 index 0000000000000000000000000000000000000000..8673fea5c30a64f3a9e345e70f3ec6764c417492 GIT binary patch literal 284110 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%0D0g~8+Uy0 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

|K`sRUmY61IbPN* zTexFtwzHG`{(>{pePj{%M^#&?cu6XuR$WPkhP&Sx9j`w0r$4p!$rYMud;zYMLIi%W zelVq_U3fIuKXlNa*M;vgSNtF3=rh;_Ex-fuC>;}qlDC*|{x+L9=7$2c-2x5tH> z36LNBq*E5bf9b3x9DLQ_SKv%9%A8#*m6*(Gk;KrZnnC+3L;}gv{{!*|o zTr)60dDqz^-QNn#`}%(V6CU6~6_6jPLIUh32nG1~V6XvJSwO85-H@1&ZJgoDTSZpjTjB$H$QG zi~VCChv8_H2^Y82x+7~#)qw-s>{;~xd^o!cZ+Uh;vXeG%i3=g6l%l-v6|)_$*rxr_ z0r-6)kFQ1t)_^5Nh~zCk`w+@~tQh};`RM5_=OdD12P%{!l8~Fm1?zw%vVbj0Mh;8= z-v4z1@maVG`PX5({Qg6?v?w^U|Hu(CK&fi^>}Qoeaqng3rZWEV<(JPqLudeKuYT{# z5TL_2JKy`>Z@m9gpG2FUh{=`BiWlh(QN6!;;?+OCIx&8C==F*Afg69RxttlUp@CQD zY?_!*D>;ym`MG<$D_ZALaWDWyNZ&7-yg$9XV#SIT6@U8Eg%9mclX(np27M`{L?&=+ z>xy&*{2x{5t7x^S^XDi)3bnWZ{#$)20rC-afE^$QbN8Dt9y|QCdVlj{&wmWa3;hQB z^YV9wSKO>F9GeztT}M~It0TWTa`brXa8x-zID{}7Fn#mlt}K&w@hPQzY76i>FTD_T zigRa$&XpSB^+6Y%eIx@1brKtjQ)U^j1MY6#C3jDrD)iN_#aR!aTLA%O#0l%h|Gs_1 zf%VKw2Zhh`HVWjgn*Lk(zzave>sfL<9?)$)55hmixJV0q$Zt`H>*$s^k1)4Qby=JA z+6g!T;ksM?&lvU%S>i(BtxLM#E8!rh1YulE3U-1eGPES29>yA>nM5s=#w}PkL?h~!xD`!NCMn4%b*l&~N12J?ROgl=?wzBF%QQky+%H)=CT`P< zV;XV&!7tKe`wP6@@8?^OZsT)$p8I(a@6Y%9{#?I5UfbLD{z+#ZL5H7fhr8fI8y5B+ z(XxE;eoJu<9kG0>Zrjv~uBrVF+H3K`Ii+&f^wV)M&9wq*Tm1lwhrD*zr>c7IyPbu9 zM=t&N+YxeqzTK9|)>X?%?lLQam%IDB;XstrmZRY=>UXB{aa07_JkC~^u)i0hVCDl$yA15JP`j!;6vyF@5KZl5C$NH?#4z80cv~E zfyRp)YtaFzg#Q@5MTRt*gJ4_1$$<)F0JMf+v=y_|oCu=;XOiP)rc6>6-{1tnoOU8~ zJFrip%L+B4E?7MT%-}L!Si?&cxWvHid0m-#Ky@HDf%5EB7UP4U9N}tm zl2V~G)+KR*ggu^ZtCf~STn<@A{6QsYs>kfb`aSa!I+}ZlyIw*8OdoqK+_2>WJN*~- zE?=>xZK>^}oIdkqBih4#J z`qcyg_oon7sgq?YZ;SolzXI3iQ-=R&0w6wyAyJn#N{3J1Q6c9O~CK@ME8_!?U)$$ zeY4c9J_MLmSfJoFeC9M}fO-aXmyq#sZnknFM3>-5N>`P=tS|taU%k1s8pl7;uv4_X zNJaseze0cH%V96SZq1NJ$Ymt_zm64Pzt9giox@6-PspRMIPdc#BZ9w{veU5)hA}%vOf==_jc< zD8&IAUis?zZ$6;?+e6=dg92PW@%$T~JkI}tZ*Cu1H`$qwWBfg(Qq#VrVkIJ7L0xg= z2brE4jiYKQlg*c+a&+vzb<@>iksJL08P{ij1@F%twgVIjwgQ0vCvkU{{Y1IU9N{x3 z1+C9@HDiNwEpFEk#q5 zPA9_vgPZI4K!LhaD{zEr^#VlhAu*oAuF&u=1wa!xw+l!dhyz6paDcw9fUr+*`gWN? z>3lWB2@A;r>!SS%_{zhh^VP^TSj*322`GdG8t1`2;9h?L{<~KKUlh+lLgZGkw}fOO zP|pUZCkMec*wqBqEhqmQ`_>hEXn}Bu5g;LPlLKr7;Lmd*qXA36*>Tbs^D5qi(wuz8 ziOW{=-%h3;O0i*3D;;gpaxR+&7q)ESbQ2bF@8HS}*RH!8;?r`(K`rq993qB+KKUPt9q76uARX(WJ&rBw5X!?<5Bpb0BP@;_A0w5K$6uUE2k;w`MPc@~3m{4; zz&=`H0Gn8SNpS$lHjo`qkA)#UFxznfC%@DIz zh=H-=6*pb^z@BnuCfWS_k5{UXd-lJAT}-piktbZeJEDc#yhJDEUsOBNgxp9rap%b5 zeJbhsoE01nf?mwk&wEt&O-t$pm88xj!5)QRl(z`rY34%U{Pxdsm}*FxHO(Dq!p?aQ z=mhTx0S=hY0NCF+KvG59+85Vi5N}wyauED~K0P^j#Nx@R6?AlFrSaimdRA_l+%`SA zd<$Nomc5QzvgGJPPdzoSfja70aRf%jhF%p7ENwaB^p##B>9*UpjQsWH*x2TeCcdKx zY;5b+iIK0Ko!I=w2n~@w2me_?G>)^Gas|eYjH)hH{q&<^rZ^iHE8`l*i+s3Ur1ph8 zNsy1vsKG9EtVY4RoQY)i4xt2|4CMKF($j&ia`8zB5W5<5D$2ygDCQ}WG==+a>kBL6>Br+EFA$I4!Y_5xII!(>cecL4aPv_}&+DT^BGS*s}8+fYb*2q9N%( zq+kLKb@_3F{iJXkjqS?&5$lKOs4WWvKL9r$(gU^xlEjNE9QwrzN_HHUv@y?K^8lJ5 z`fq0_A9_S^#({&tm;l$k@E*>d7hfC!*t8Mx68)YCRK0j|vIgVjYG8sr;0UfsKet`y zB}+BYfOO#3-lsa+mHni$>(x+7kX-WXB)hQqMZ0l|CR-=Jg%f0FmuM=1{u7%$qIcm! zYJJ<5=|7xlW8=mFS;9ngX(BO*e;*iMvSG_`&+z2Bmf=!$s;9GSczC#F`SDZgV2|Os zZSnGj3vWC8sQs24yx`~~>!Q-sWXpzyXD-Gmr07A5g2)3TR3O)E{{4eK8sb5I-qq4h zo4=z9;I|37tqkMun=bX2#efQ>h~Ygu3In1ftlVE7t&H#3F)IZadZfD;6|r~AWt^jG zGU64A8W^R?`po4{0qRI4E*k3mf&WJ^5r&=|vH*Jq{BigXCpG<^aw^5R!fVKZh6HJ5 z;R~N4UdBNjG1G7j^}u2{#q1`S$e^9g#R5K}9pE+dK^^2yJ?&x-`br8iC-SgwDz9eo=~igMLFa>k{NN4Jg&A4a0bDS9#cC-PCToP@|@Kh1}<=A`@L)FT5`#jrGs1U+w_yxdK{)xanEPlmad?0eP?G^M`!1n z1uSIJv1W3b?({vaU5oE)S+d~Z){a(Mu1tTv>5>h-gX>nXJj{l*y~=!8Nm&3g@X*B9 zbbFvj#@M@Kz<;yWZ+iT>S6-QPfNRDrb%1Dbro9{&;gdyzVT-={i`AoqhSB-o18cJ#{xg_YL zDr|I7=uXV#kT0PPgonT~lMIciy*?9rHy)x0Q#zp;z;(*NyTQxS%cV(+EiojG@~mN| zSnpx4%bP0xk8p!GIYY=^A(SMkl1bb~vWVY0agQ|Jz|1+wvQS!!^V2z=5;~1|iU9h; z?9S%zZppso|L*8_K}F_^IS>1^49q9#Y1pOLQuxa-NJsreCioUg4M$5riZspFX(8cjZha$I4t5bISWJ2}Hh`)i6$5_zD)2w{;AZ-M zK0ihsz;6@ZeD}sVZ_&<#WM7T4EDkXQLwDS&5@i^>O+|{MQH+o@KgHry zxHCdFqXMMjn5<*%R#2QBvzQ9ac7e{=4Ly>kpm!a z-Qe~q4Ct7=?!Ai!1cL9ZQg>-l>u%Ml=_-0c6d3#}Aa+(_Ui7|OaO?h}T52?uF3%*XS6p{a$8 zq#P+V6jJna_n&cHSG}3f={fhu6A@=fiu`#IweYkZJd%)6N~Cu#e$2v01LRsiz=9js zTt&wRy!ex}dl{Y_W`!YEWt-A&c#1|T`>m-~JJyhm(38z%3$1u83r(l)+c`~sX6sS= zAAR7l2X-tuWWNOqRxF+T43B-+eoGf3d#QkCC7}k~plj)jH1^#WpT6Pd*?pV8p$XOt zkKOq>$(vmxW>AjCt@f{qG-BWEB#mY-jU! z2J0O=qAcK7z?hI(G9@VaOp)X6a$Hd+5ZQ5z_cayWF7Ai`dl`ue*aPTKG0NwqL0M*= z45wvJ0}4ipQJy5t@%16YGy}ShbYh;Q3)U0iTzeCzT6k2TQV5p-xU)@B0MMVX zCSh24p`1c=i1)&K6!_jJDaXO^m7ptOC4<)=P00Qg2p~U%^fFu~;f-#o` z{BY;fCkFl;5T;f;bK;IWtJ{(x|E!b2j{Ktk7_dBia=^bfsXl~DDfZc?in6o#Q3F3b zY)gP|N^ZCtuI+~Xq=!gaS?K^lDuHpa33c^ttG&wgx{X(zeEq6hum14z_ujkVhHJaF zO>Vm6k{^F8eMXxIDo@sZJ~J&X=;+!u)q^iG8)c$Qsjf#eKq1?+rnBe3rZo%JEIEGP zQ;$8MwYB4Tu1rr3S4(U5I_NwF7%WPqKIn*z?`{32Z|ig4UUO^T&8+^S#b3Uo**Qx& zQw`9~Ac z(QM$zLmcrEB@#LBl#!T#89*2Ojqh-1%h>#fQ#KmrJtgpfuCsR}8JDVvy?=vfS^%pc zUDY#5G);8~;!`fH0p3^&RtKFYU3!7LY=C5>6Fis@QL*c0irAt(%|twM;{V2 zfH|TvTgJ`9z(31@_EHZxgL296=v4akbU)ss?waixyg*s-F9rr@$^!iDvz6>|$JL#B zoV20>RjOnvQj{d+Nb0G?(^@kf!`yR$Oi8VE15|@bBQ9=ghflPTNdflAZ*N>lFI)S&(+#q>2#yJT`fE z7T*Y>V?ULS`g4g5;z6mYp3O8dWU;(NEzD;&CRgAMe)l5?yJ7bv%AP4>d z9d>J!FO3r8it}-iyEm%8IW2R!OC5WH|6DEU4izN?CNjMVXn~5;VQ$#GG zzdolv{>0B};_)G1Ka1lOh|lgo>53ZfqwU%4@%B6i72a(dg6tw_kakf;W=4Ci5m2Wh zYD3T%;UI7#VOXFJR)IW5wYB>s5@rZ)`kn`DJ#9a!$MB?9?IJfUee{@%C|A(B1!hbr z3o&0Pe@G0n!ONVXY*2360v7ra*uO+X2n`4-;EDa1YVj;GZ<6q}@W%A%bf1~xV5q16roN<2 zxXJBcSjZ|Mxrv(nt8ZR)4VCgVIAnFv;Z5tNM&UhEG?OkABA9=vl#SzjmMI~hJ(VV- z2k>pmS9_x9SdOY#jYI)3kEgp@T=;kp1@jX8l&up8niyjfC=^D#mEZjnq za!cNGp(?_#rT$wM@TpRHKZQ{FA8E)I7yI8 zV!*JZ5CRYba*LpI)&|s=+ouwI`iKjA)rAvMgdw14v7@){0F-9Cv6*iGSOUoTB36L; zxez-Ax_l{3=}EO9Xc1<|9NHa2USxf=$YGTnd90sj4hP-lA=OhleinphXf@Y{yp5>H z?Q#-AZg_a@+QGrQarXcC@vuVl3jQBSR0jStsuNd~>qhx4tDluFY`$^ZbQCG5JETs5GZj`_QSvhm`doc;ZapPm zP<8xBh`gN5niU9<$?8=m1Uq;Y>W03cIV8gs;&Ma1f@qE!&yEjaIP7pOJ$_hxq~m46 zV&q8Z6@b1dgBUwNPoOdFn8F^|J?y*3y73>-9!7cXe9`Kl2Wv_Fp{mPwY#}&UECJCb zL#H`jCJo_S*0C)FfRFeAu^(MTH`vP}KmjO`#1ha{Vg(TXdCbYupdKq&abfSA2Ti@$ zPmG8v+YND6A!(IUq30Gu_X%!L^Ot{(*Qt{1WCHAx4B;C)^B@B{z@43S$=6c6&&izc z$1q2zJ=PGuv*?~!j(3g$D1UF64h;NIxPQL7tRpyi=SK$8k-M(^e&oyV`X0aWsaFPnbh~`GKiLLyvyiv1 zLN4H@vArJ%H;|XJeyBV;dSN|fUc&9nXc462(t(;oeVQBUbBw4tg@Ua~iSzURG6;81 zAKasg1E?5AY0{hmdRevYHL)fP)eP5OoFj*n83h$CkGpW7f-vT*-ytvH90OeMB)^(3 zN0nl`tzHA(-^K*;E*nctVecF;Gs+1f5V~s?@;Ow>9C+0oy~3VR2@?XxYrD{GW33%7xaR_^!A#}EMH7Z6J*8Gmc6Lz9axFj zy-6_*(F9dK-lLkAEsOU$NGWzFku%NEuZ6pmvgp~9F6^Hc~ma-9AQJ%R&5XW#Bg*B~T zzg`19{9pa7nh|oH%OU;0pVrL|5+f>&q%p)0_5gX9{ZaY;TyxDhv(5In!h@nv*aPwG zkU9U5UIFH}Q(vGK=qR3ek5ld7Ozmif-j6>gM7dxx3GcTC)CWTV3ARAJY+;>(e$@`w z>m(>iQc0hdU%KGNF9qYa4h(bUKA76`Wz3hKC`=DJ@v2}vVeW$gjOazizjWj<7srSE zAhyZ{{pqYdEFb_}g#o>$rp90nj{Z3m3uOu5jDEh}SauWO3iwT3`KcmU{^i3ti;V{L z`RPAGX@IvRy%!K0!pW&RgzKR$Uf;BUCj2Uh_`fHJw6;^KKfuJN{&B9LOwDH6ga_eA~SP6oU> zKIG)0l)jMY-z@~Q9DcQE%AW5LVp)J5GGT`>^&vy&a;pwGA*ftPo8T zb450`cO4WatAnoj$4C-unms{&Nu{)vg%5ggqRV|&QWT6L zfNZ+@tX`QkVxdcDT4Xa%La)PKlpu5)3iw9@6b2Z08}qvF-EZhS^W2qBeY^S3Pg(c< zsb63GX{O9-(4Z*C2RBdzzlihE>=0OmW=Gjf5o99Vr%MnkqJvNwz7Qf;LE3s|$DV{Z z&?_Xn9+-q@-8_Xyp!nT6WIdOfHAx)Pyk5BwoYw3>D!PkW$?w`PeNqmo7*2#i5buaH zjQj}JzhZML+2b`>%PKPwyw!fEq{$Jgpubbxd_nV`#}fT z`FFtk3OhzQGn{P`*!jtWK_?J+kHcqd#dHXxJqE&c!3;_thstap*j6wRIxI<#rYF!j z182BprzDW^fQ;niU+K;B*-WYr^H2DMP5RuteR#8S?yp*l3x3%(6e3*y@WT(#Jonbi zZ&n9I5-;HS*9T}iP1!oI2=QSGT%iQSgE)n{XF&_M<>0~kYYu`y6Nz3MvOB2i3 z8Pq}O=%2I+a=TJy1>fSt2SF>8WljGR{QnlTz|8~Bzie!5;_I#7ym8LQkF$jL*I&Q! z*2^?FpZRR6gmV!PF)T8yGnOkM0T{Q(5s@qidwnj~zy!fas~(>)AFxX+S;lV%rDOwO z{=SrxAqzq2<`Vo~c;^;*67|g?Oi;@?gEvP6!gz^h0u-RAAs@YR4iYazi`r#N-_PVw zRD{gUB`B#8-kCN)zpL;Y`o-2DNGfTsr?3)f1mb){**up_a>RU8CWa>#U1?PRTqE8J z;ti6j4WLK-J6{LKQUF7qi@KWF0X7Ku1gfKCpht1}>r_OF4VUd5W$+KmpwV9k$?_R* zvD6ir{WPj&@<#X2hr@Pk&{mAw+bBv~p(D*{)l~(a? zF^9OV-8>CS#qj>8%N)}}(%5;U3WaSo!+=8gd8b};sDo;4c>QHtf7|@^)<1rG`myg{ z{bu6ZFP^>YtzW--syIDWf?-Gj+A#}J09=^mC_8DMH-dfs$fy&&DVu09FHkRJM$*5o3kmfu)er1~3Sh1iM!(XfD$M z1wh9+3po(AA!jI5+^HnQwktL0f}ha?$&NjVK|(X)*#KCpUJW0p=}w|Q0z7Z> zo&cFOK|hBAA+C&h`Wk~{u2Ih%iP@#H(UPVqha3O4Y<(V}FF!qaa@M*4E;JE0h<6d=W(!T4^(hn~^D#|ie8?D3C+74}#Z91V^iW)(& zSi80;CS6R#;e#w8;tyN{^vyz*NNLS34BSCw73Y|s2Dm+ z!YJJfu& zqrAMQduXD+qpx$Of78s?SFmayQ)-Uw)KZ9lBP@taXD+cfu>s82*#JsKcSwN4FeWr$ z5`zDf)>1rA@utExc?Au4j&OZ)i4;M9^a#8|uCN%q{Hyw_flx331Eu}<<|x%|k8*~d zpipuTeCW5|aQ_dVaOPgey6m7=?o%ZqVH6M@+Y2)|FGcKGn-X>gya`66s>YNUJ>t*+ zD?b}S3<`+a$4W7G+;R;jSrbNoT>1m%&3NKXQ+*$;4ZLvAG|89;{@j5p*-nVoEsZ$ur?L89y1;|AdAk#7N6udeo||2w;Lep(MQKW=52ze_}lU!2Y|_luD%wPoGZ zIdyl}RV>Yvtx|Jcah?0BR#o!v0sLEvGnC=uZK^NR0z?)#O#a7ddy-;^1I3mOV3ObwZ0I z|18N#I*0~w73BGFrbU^_OC`MYL5@PeD?DIMafkCpZ>ph3`EU%U4?lpGW~wbVk`b zY6BO7ZP);Cd?^#(toY;tLH-5&Uh?LC|LymW(1zdXtB4-z9i^@&Gdser82(*}qfh58 zEVW9qU!fuVVIDNRKjEwp|K&R*MI#&ns*Ur|X2Nrod}{Yms2H9vd>#x(RwS1WKcrgE ze`juhb!_4o@NbonHlYW;Ese0_C?-HS3>?A$cfYlFK#+!$3KHTcjx@!Ha02i7c%OHc zC<@M0>MDzimu9o0Po+oN#_XpzakAY zNz5yR*;QWCIXN=azkbX1EsWx6 znpt`F9lt+e*E%{Nu&bdVE>HX~)PI@HNI-LgI*C8MMvHhuA45~NHpuv;F^-<&>Y;#; zS2=$~@(WVZn_M^t7^e$597}crcrI0Dg#4f&w&*3iK*P^c9sCg?-#rAtwD^Ct2kEsF z)Se{tz|pf9qH}eCJW3k7$5kUQY9_~@P>2y=1#qw`Ks2g^bwXSymc-Zumbhbvy;{)qLfVktxuD`eS8(#fZJ+(Lpz{Kp%hZd36?1WBtGZ*y5 zTE7EE8$0Ey`M+77;;YwOm-?phr9jL+P($|^58`f^Y54(~Bg0sHlZG$m( zJV{8&e2=G(;UW?A4ES3G5Ez*PWu9HbkK!Pz2eNUKu@y_Q#WH38V*bgs_ z>5SsA4Pfu1My0?p2~T;=7!597FR%ho!|q@e6x|M^T&3*i5?NEYz%7BmKb;Zj#(;^9 z-HHO_;&;l+8CFp|1p}&?`A7t4PvdV&jl<8mhmbJ79t9K{f&Csn$%4NJOE~OWP%Cb$ zZl9V2C5WaR=`!47-ly$gy;>Kjtk)}dqw(uhATCANp-qV3tXCfmd|!rG03GT$u^&h$ zQUL2&So75~$oqhjhrH%<1oDEuk35kI_$Q2te6n)`L0-e&-bcG14S&uok>4jqEfTNe zxuyWXdD^;bS?4pgLsMf@AMTpy-juDVB*i#AI+Cpz9ZhTXFRm!7PJ8sHe2E*449Hy*n1)8dR+>joR8Kd(0J$+|!9#CEVHHpS9u+2r{56W{lg z*Ypi`_tSrP{krzP&h=Xc2S54pj5B^acF#w@KP?9)AuV<8x69obZ8Wq+WB|Z+@_LWm zci-OSG>j%=uCFGqh?9k-cJj*$)4nh-#T06Jf0Si|0_0i&h8ts>Tz(g}Djm!lxwV`f zAXmYK(i}XX*ac3U$XoLS+d%K|No0ZU#oO?J_F5_N{i8_EuiJ~L0s6|%M?C8Nb}#%x za*Qr?D^N8`paVs${hZL`OLihQ01#F0%p9CqP{RR?!jA*N5MqK60eUka zKu(5(M#?RJ7VitOFGvuee{vxlM+UOGIbQ_q3&e0BKX@Bch`lwlv{nQ z0sw`nT*6WO+MGfx0XI$LC{0oH_cs(qwCp3LJ3x1QySZ@~jZ}dcw}?-`rmyZ9ymRV-Ef5Xe;5aq5O?q^uXTIR%B7xiQvqdaz z5Yw950(g(So#9O}d}gO7uf)j=e10PS5$1~By`*{-3$e$mhj4&!Cxt*G(q8p~Wt=Sj zdKONLv$bWlu$zj}Y}?qx#N=nw)6-M6W7CuMBckq0D=R>|$F<*;7RQOhf#Ls7g$7_O zvRuezM4s?a7WR}LrPMx?9?hZ@Bq=lAw2WGO8`P`lP5R8^8{Vq*V0krN7TS#5*dGE> zq}GrQngJV0ZzKiJxm;7&tHcy zFpcmT+YRwD$HMqs{PGR`_0JD=HPvjLsh=F5?&&CJ@OxkROilCfW1z8-xR%4VP6HB0 z$KwLWXbuK|@SM`^W_y9O`1dxj*aCVG!REj>4i)}SS8{)@JL+#)hbHh4v!?vDWsgMW8))LFdRv(JxQUlV66omUKbNJ(e~c=;)If7W}=`2vV3#1$tO<>j-qKPz88_R=@YOdLZr_Nxt;9U*;PS?A^*4x@@q}N-ABs&Z1xOLTfL9&+Ei{PncZC7L zHwf;z^oOu;E-?U2K(fEGJ%C0r1MxKe@W6N+_KK>(6_=ri5)l!aGlT5|6}oxsq)?aT z^NMgSNpgoQoOsRvZwU!Nb0kPc@SS(AuS|+i0{C719?-9^D8|0P-_0mzK#s?BA^q`K^UUq{FyGD8e%Q`?_w6s_pKkO+ni%4!p84b{&VlGrvkj`XmDOBJa;#-Br z>cF*tJwjE-SI9cxZSK^GUOrgDay#%=_4c(le!9BEn72f}|F&N&!2nGd(Eo{*!s0DL zqCF!JqD9F}8AJzxJTa^j<{zHL_a&k`YCn44hOxSBOUnC}RgX+`J=y-rx`xtg`Z_=U zwy$PuLoZ%`F>EcTzwc=VlA_#CBle4y0!y$gto^=UV>49f<1fL#+W$cL4NW!|>rK)L7(NPQ`V-_lPN450sKo;55-H8gB2_P26|E}Hc_y)I=XVP|RF zYgk3asts7K1PL|;wg)yG6oA>oMZNnHnxJ7renUZF;5Y&9LcV{@V~p^8XH7U9=I;;( zaJVYcA0H#v;njH81uuvdB$6b%J2^-X__FyMyD$2YjbEZ;)bUG4cy2qGA7?^*edl}M zkBt-y`4+9zN!e~V#_LXQ`f+H_4?q7f-Zn}67DnTAWAS|U z2=I$6a9BC<;B$cG&$(X^B4JsGPF=cql=+d1oB($PI-(U80)+#niXJ_3NYbV-`1BV) zwED$Qk7CN)J&O~>0rlAQa~=y%v>}pmN%BAmpPt!rum>dZDA=y^>g&TP=71H=5-jVe zd%BstOSend13Y4qAJT}*HuaQmEuWd3+T7IM)Y)0nuyrf7EJ_0HCAF=W@KPgd^zR}= zPBUAsK5bwY{8<6GLwzuN0?Gd@!K}4FzVIb69s?Fst4*QsF6yrdTs$*^HD=eX-o%aA zHcI6Sm1_z7b5)eed8zPOIdpg|f*(Q;e4!x-e}N3`HNBMi@N4MUTeODVfv$Biv`iN| z^omFsr8*>P(Das1$psp`q2LtwC#3*>;nH&mE>bHk-oi~&8^kMrg7(~C3`a@?f;m`C zq(P=a9C;omHSq>V4?<*2?-k%Z)6$~nD28-^OwjM3K!5!C+`D7hkY1bwwZh9kig$0x zOpY_cw!R*6lKl`ypV_r*6g|k$+H7KKVk9$)^FK_%3mXXzeGd$q<4>X9g$t5^6lhSA z8|=qrHP`}W_5IaV>GUWHzy`3uiOF>3k~OR#0E$qu8kS9fibTz;-=>!LY9_2)a2SK# zuZjQ_Y>jeVlORAzelKET~flXRasrVyQ6>8`em--t$Br^M&tl}+`uXdpeZ~YmXC#nSQU+` z1#Z;Bn<~u9mjbBKkDWQqY8<2y(Y%jLABcK0k39BTMe5+4jlh{U`5ME8uTz>KyIML@ z7h35Vk?tWyr3(hevY=V#YgOUF@XAZT3-Ydo5 zrDkE4C3t2(feg8HChi10h|ywe1qX|F03;g0XuuzL{(0@sliA{AgoO!_9sjdBAwTPX|rKD?iT+F<+dxPh+AR-}5DSR5i1&Ld8Q?6eQVzWC)W_de7qnG@q#6 z(XnJnyBsgg%si(D#OI$d*aasIO-Kj@5NwsTnSzCj12s}@NGneA;8=z6%U~5eyQa4W zyP0;lrOI4Z5rE^ffDzY_Tbrlw|EKs8!AA=m2+jxTii`ncPtP#Lv55fRf#K(<7;QDfW_g}>6Pv+C`>1(Ch52gNYW@%=d3NtX@)NaErjr2wIk zmODy#q@D%{G&M0K_3}60d+y!tj5t5&47HE*^OqqqTItg`D*XlEM|WjLhCUmgp1=}- zp|kGGHiY;C`haZ%KPT4yM!cgOVK6XSU?W&2rTQKoqkOI0d(CYndKfISfvLNM7Z@>@#4G7dCZhs9oHR^?g32zOh zAYY*`s0{}NeAlpgLx8Xj7K;DS%oap9hwkXSiDWY6JYxg!F&PJ`6q}D5Cj#jdA_aIF zonlu>$>ePY;TRey5*CTmD{!1rg~>Ctw}G?_O>yN>ZXhV;{Tv15RET|v^ph}Z3S)kQ zaERSP1zsPm0HD~cMtt9ukg-z`N6vBe@XSkoZc8WOIYf_ERV|JqjU6i~ z0a2BVE7-VuWFL|tq4)^v{mT7#{g>vfc6(guU^XW z)7w7ZIs5s}!NK?LU((*R?*4LE(AJ)E$8v|iRkvr#5=E(%t<$J9xMTNa2O!x-G~U_H z{0ujMdnZcEDXFATr6qtwx5RaN8Ht`nGF7eo$=ADqeFzyi0uZ(W>s1gd zl6X&n17iX{T_P4DginxWR1~Q}Xv2gry03g)XUCSRMa9N`(%2RVTO-o7*%7GEU8BUh zvUn=#&8})4~{9O{9s_oBbob~GC=bKy{;Lg!dV+OB+h_9(Y(EG? zhL1V)&0!zH`ZvCe|T_Uo(zTEq??{ z8Mpu)(389a))?)W=Mb;`Jlt7B{YnuyV@NO=aqI{j$WM@G9`_`wl*$#3sf*W+(X3nw zugnFxg)kv-i*_lG0{p{x3LGV1WmjXYJQ)n~=1>viJfA%0RR{8es=V})l>qXVpF+(r zu{GU-pc|kE?7SnTkFqHc2Ebc{GTwj!q*NPV^!wmNU+2QVR%wA0z|L(1x?^S^V#HvX zlR$!e&q`qXR|?>X_5MWsaQM8^LxH_Q(gX)1;~_XkPu&{6kZ2$^Cz!|`3<{1WWmqi@*Mf_snMr)JzrPHNE2?2j%F&ve4s}pb&4gW z2@nwQ?XIQi6UMS4z}ec7+S>a1w*Kz^4V4vDn!iDE=%(szTefKc>+ap#ceiZZ*xI_< zllfF&tGT7GV}fcZ&5LW9Tc#O}k1&|=f=B4I+|u&+03+CsVhYzO7aXNBU8bnes`I*= z4n6-r+{chLXhr7Zsb3`LD@!xX$Up~d3)m{Kav(nZlSuhyVl-HNf`rfT1a+3E1)(xc8X|TpN0L5SD#IclT&P$W z8^9DO@1^C4OytmB2>hmG4llRCz%R0ufqrsDn`S`oxp91DrYMDo7~>dH}dDQYB?9AoJ#c#Nonon}A!sh4};iutUzF6@`an6nJ=& zW@d6>;jXfjyyEl4s0GH2um$iP2vJS>)^#15YQ2h^^>`CdEQ&0gsmYPi%+RA#Q=oqr zGBSZJ{M3i94L!dL9oY4ubAWbzh)Z-5cPR7S7$x7oy?@h~v>;oHPGqNW6HQFiwe@e> zuxiIP<_mOnFm8L>wl3xmx3toEXP|XuOH0ef8wbcUzFLS`-8w+0=GPx@ZGF6T`|Qe% zv$G$pxRH^gr(C+?g`J=8Zp8(9!NK=ps$O=`X@_5M_7|_90@t1W%?tYkRuV<{vqW<^ zxsQ$^D2K7BmqcqUc8rEa{1{%fl506Xm{-nDRj}=lP*We;Im|$gj9i9Ya3H$HvFUi3WVXY2fvehn{(6l_m+am9@2z z=Fi;E@2Lan+RXfiE!(=fwso!VS>N;Cdp)~b*0#>fZhU57pk?hqYs+rbpcN%xemccg z1D{@f^NaN5XM2DQ!#mDMXhIOMWp_Brgp(@($fw9`+$?XfSu zK&8L@{EI`tasp*zfBto19dMS^F4DN?d>03Eo*ODwyY^`0!)c+fYyVgg`O;@QC+TdWaSR! zTd*BG@L0}F(JEpafc~o>m#RqhvSP57IBwhY)# z%v+3T_Wmm>i#!7InKnns7;&U%AO(J_Icf zJl{v7y+&f*!(H^k_mKvEu!j3+Ot2LjptGZE%i6M~#kb7)ehAa1N#eWTOSu)V8i{dZ;l#Cbu92KhQoZ?GCMmnGc(w^3?mp}4Qjsz%WC4?h9TD8NrI<>8>z8(jIDGX4wDCHXAbLol@q zfUEvNl!M0<5#;29c;)#r>PW8ahJIR5L;?_PWD!|%SoZ3o$! z8>?r!wn#Q_*|P2Z_ut>##r^e2N6(vYzW3(k_rH06$L`tPGlM;ygUBpqz+h+3;9yV3 z;7m^sjq-P*1shkaxDjq~)aT!PvU3Hacz1sD@#inxM!)gHPJ7{-kMB6|)+>*=gI>%> zAARJ7r=E7xmkoV2%#i-L@AFd+wox%5tu{1ufyG_B}w=ozCBvX z{!jw{Ib*_4_#!cn)VYBTYd6kxmiOTYYR3*{4`4bdfYc(U|AQLz!mB6Nf&P4XAQ2aJ z!CFBazDK~V$%wLam6`+3QF)=3=^M<((Evs1P=QER9nAYBq%;8v+Xp zlnv@c2%#9SkMJ*BK+5G>GuC!#1>dP$y`>q2z`o5(QF8AOnhc@Bim}Nn#h*G{kRK@z zFc!3>2Pg#xX+z9@&XsLqc!%;8pM{m)XCdz`)(9Zt{62#((HGR9_wp|V2nU3_7GB$H z+S{W}g#Dl$!2{X^^tX0)b^?BIc>j`~hu^JtjaE9_-B$Y-UFQN*_gRMVrdhF7anV@n zhGnv{#;Ji?H=|2z(aaNBRG{jJmr5`$mIzwoCETovOG=ao6$_P#v!K-)PrF*ch}?9g zb4X{`*0!UwwGd=ZEW32IIyL^D=lkGaHhnqge?JGdKJWKE@AV@k?W4be@_e3fFA*(~>E7VNdzZ$3+|09WJL!qp|>=Y?}^^j;X^aU5YHuu?pY|L?Ra zAikOCSkO9fADTkQtAzvXhmTMT+11rvr2iYk{T==Nm1oYhXo+X9?@6Fl9UORZ00mkp zx?!gzr6+W<)qc%x#_N2Zlv|T_J}7{xFWnztKng6}V+D}shS3q_VbTDdz*DqJQ88r< z!D|oP6zNNqu5C}PY6ak4 zlj;0y`FzBB4CXKqco^4-M{;LJTQ#`OiBfk$;8z!d5*YQo-TZ;qN;VvBQlqy3{Al)W z3S&DbEyjKeN8!G1R0VLwMe+o#arEkD%xM*osZ?LOqj=axD-rQ%)JD11C_o--TY3S4D)K-m~aTf}g5qVb=RMSCtW z0W8=BaGxRcpqw|-BRW)GH2m8m$G`NAZ+!MQU;2_#-46f$@F$h%vvaiXIIREp#Kq~s z#*U8EaHo*ppBf%6ZJgG{!P5R9S5N}&_cQzh{)VpMhW=C;KSd3lU3mUd;JK)*{m`L3 zsZ?cI-5G4aYV2=mNkb7*nJPNOD1Y=UOHv#?w5Or0tbv}%W2a8s#>y=1c&gglQQe;O z*lF5sY6&jZ>uNfUJ9qu+kA3WIQh*vr(8&`$V-GFGwJ_x%Ol39ydNe}TJm3{dWhIM{aH`%Y?)alfuy_Z~e#LG9ygZr8U-px{vz_;ulLP1l zmh6rBXo89WlnloNqp$3fyY~<7j>{18Tkqj=OOoitN z5@_CNE8!vjFy&@!L_KS0-N<*_{xz(EXeq{Q{ z=SlVa&3AtT`T5e}fq|XJCnn~m8yms4XKh$~M=CYkFUr%=(BA>>(T0wGV2wcXyBZqG zDvOHPSjyVrKB;n`URH*xl~q@RdydgSHbrdwSSpn&8yY&hb?d9{KXJ6YtS(hnjkcUP zF{UQ7+ti%=>{ySbD%}M5k){lFO*<_O_|Z+L<=&b+#T-y`>RrYpQC}?q{C2!N>QR*> z{FtUEcwdV87fYf?iw`B|9}cRKT;lwN+GH&VH0X;g#ff9=dq14`{rLFQ-1O{|&p!L? zQ&0T^K?D5O7BzTYN^o8a-MZj~$P;8Hre6|-B4mJ=)nL0)C4dB4qQ&6b)cdd*zT-dw z696ABQG;BdTEf81$WCwo9SDA)W#CxKlPUWb!qrTB6;;EF=!@ygn$5(hT|@pU$M}7u zZH|62^vrbxaxwK=a@9KwVQ%jF!+rPt;m|II|KrEU zkBlFgIR4#l{|fItUVd^NzWc=7;A~?lScjw(iO=*i2MmMrVJ?Zz!(hoK!m_Qd2K=d1 zKRN=bN%1%%Jjtf1hC_R*tAROZ)pZ)hfj*m}Iw^tHf9)CKYsnaYt)(0z#Bz@&k9d=)Kp6fb)_w2-UX}c^#W*vma%6v#PC{K+-&Eh! z)ZhO0x4-;F@c+A}fajlk{`u#hd+wkAeD?3pKFeHi2j` zEZG8_qXGyUd4Y7SoQM)@8!FQ*BiNtJEYb7qi33b_uVJOj$)?(>;!Ni~1dg9LLw!LT z>=`K5@SE;xR47FS8`9}KD<2oYiw_=vqzy)qbtx?o==s41}Muat0k>U z14^Ke8t|^`fmrOp^epdc#Gi={9RA~P2=jj_YDn?@{iXXm zJ=p7o0;CWfvuzmW>@p|9{uzEL$VLU=`jutnEiGKBtL`a-ZVjb-dYGKfps;5oWz|ds zz5%>hBx}cxlP8as7eV`v4i)!o%W7J`Y8!M1!1F=E*OO#(oNglewXLc4bV0$Yo3aX5 zue<9s90)4_?KoBS^#uM)$xuR7UT~7&RJou@uy^qx-?YT5iD{t2Bm^9e7dO{THp;=E zQtf?;ksl|yd~RRG_U#paed<|*{qO&H>RAyW#{TE8{O1aqpfh%yVj5r>1m2s2Z1jt1 z0l5cZy!YBjiUxDw*cE$(;h$SXaK+;h*oZQdP)| zo9LCsf6lns4EUR=52J{msY9PHp?@PfMYN(9SzcSu+Il6#0;2f~+tGp19^0gw=>$e3 zoSW<*(L~_HY#)RtwTzp1A(Zzr8X&DvdH_9yUt)vU2Cm=*FfD>txlnIAn92<609Uz$ z6};;w#~&Cte)yBW`lRz2WCI)Pj{kwh|Ga zBB=paE6=FJ0IZ*BVOz)H1s%bBbo@D{qw+d1+*E}^@RGEkB{m2=O_|_qJ9T!fteknH zy}Y$G4c}pXic>x4Mo$kspN_C>hhTHHO_NPG7Ou~_an-8A!fhykh+Y+CR+D!ujM<4S zoEY-)G#S7XsO#ifvcH5M`BMCj1og&!Qe}`l&pW<&Tiy!`qE8G-BD4+|9X;3Ax38k& zug{i>_XGR?fPBO{AmNhbvQ+>L5D~&{ifoi3;M%bv?2#TquYj*uKQGw7jR0Uw92m$; zv?vma;_)6Z%R0_P0p#KdvL-Z7d3Z0M)iXH$HTn~0Rx;=sKI7<@s2CS|wfu%@l3qaJ4{7})fL_eTBDAx!zZ1Sw z%-%2(2?AhH5d=O?@CjmqDEifoE)12IGs3pFiw8aO+wXn$7dt18fAWtmbR_U~_*mcg z&Ibmj28Zt(83fp^Pxd;Av#%CJ%nT&?Y>xB4L zUagZ&SxsVMJ*|9|9zs=7*fTAfq=uTdvF3Eo5G07To2q#(4XbI&N;i!)O(N+@7OtDD z(NbFAozz>tY(+sq*6IzK0hEoaV0X=_1^B)wJBVF!`oFDZ#2`;p!rP>NCQJu9NC2;F zI3FdSS^7m5`eSMNnU5gf`2CUbK1h(Jfv29GzpAm{@*lmgK!voAig5WqmxG5Wr1r+Z znX~2)!4X$Y7~b7Nx5T3{(OwI%x1b=-8RFq+#j*wZ%N*vFRcvf?Q3vAwQB+%isbRGd z^T*KL-5lr@F#*}n7}ltvD$P{BudHer`65M#+dIgOtXQwZz%U@jf9}X&&^XI;s(u$}8=%!lQF{=jNptHBZcF5Edqs6b_1*Zy5a zngrsI59@(=anM4VHUCR+=RuOfF>f8!jp+>>_}sUWFanq3P6ta3>&T5rd)3-Bw3Q$! z@Ou+S(19tmprWE;yP1&WIq@LvFbg(7y@g! z-i$8r$$Q{WC90}d#$AodCmE6g>rYrdrSQ&d;3(L+LE>otfq3HHCZhYozfdlFS1Ubo zthFJq^6Bh?lDXiN5VQ5rc1Ewy>P7Rl#1^Svj`@0&zJHHPa2FAfXGmmvs zznr~})q~yEcpa@{4tz)-d%lemwg$2fB zi$pFI`TPs>#MJ!SjV!r~k1r3k_sNV!d31|%DpMaQy$mjwu3N%SHQc0Qow6RIbO5r}> z-)?|>N8LjU<-G#AIA1Tb!ud8Zf<&H`q**SEFYhn>l=9D;ONc=zWc=C@wwT? zhQ`6cMtOhgpg4GIFm7?p)X7^@UIpMa*tdZHR;C@ei^lgl<{wm3Ca<+XOOja~b&Qq- zXDs}M(OMyD6o6Y;=vpAUiidi}Y^rl4706fecDfd`YO*$+Ua@>Kow1@RZ$)12`obG; z+`uwH)MjTC78Vq2$lXw|qMk3$FM%d0xom4v{m2ga|NhNH6916IEGI#ClmK)qL_~T5 zaf=VRPJSpG-+iszn z7MT$WU~#iiy%_a%OT>sw2@U=mBWZ#SVGHlYZXju|zD*9)Ox1nn)Njg0K}~n81706; zK^BI8u*=Brm9Mk$`JxKEJQL97pI7$E1yDu&cvS&J2B(W_5!N0cm*-UqfclVG)DPuNCbbF4y+P%o={X^x7ZDrxhj^# z*UH^$g|PS4PEeWOicFWtHc;CNO*uJoH!WtgX=_0P2x~c@W&R_sV?hNFZrJ4d5?G_oka_@~f(@OK5%AM`c)) z1nH9aNOD8qO^kwT?=G!_L89*{DuYu>E#8|If&90=_1*90$al50*blz1ya+{vXJ@Bp z<~y$15fo`oFnT#g4J1(`BwW98)w6&En;^m?2SOn9+Kxr60^UanYsBO*Cs;&ttO)V~ ziNl-paPEzfF2RK)NUN`?3t{w;keoyck_*NZ48SPpMLqZySIJ{C5IfHIngK6iWX=}F z5npq=M6;fxMM*f-m56hn_^Cv!BiuI$`YGG+sH!JGKiaQ!P!5&;qdcHk18Hdu;-X*y z50o%Ury2+6`uYwYJkU4YSUNvAaqRG6{GL0W%w!&(Zd=@}6+6(VQ_|G)A*PEtV+*d4PP&dAH_8Uv&!HC-qk-h*2X8ARmk$%5|AXxsh7!neLG45d!1Mq%9BYUfPO&{;yxMrlM1t_ z8zxQE4^Ynhq3?YEdq4Tv$bRP9{;M7HrC0l}Ub$*5c>e0u%fh$i-xmPAlY~6#pTD5Z z`G0eRXAoai`+M9Tx2$v+0Fg5j(UC8Bj7}88vE@v!Smzl4&?DkICO9ZV5MO6-SA#A) zhxn|`29*~R1Mn5T5rU+hW&;QiOo-=iS((GX2FcbGcY#a^Fb(7TCZK`Xt!pHM5mTOE zyCSAF*Gh;3YXQBeEqtqVUQ-M&6d+_pBe5z#Sa+p`bwF95K*KlKmFsgz*> zy-zpQDqWqgZ?T=?wTLx=p#&H*VM=jN_hk(X6aa96?l+=7Djd1@xkf1j$%n@A@fTjVc^oqLVV zn(%oYE~Po~IZ50v`awOO$0vm7gbvB%<;cgb42x*jsw!c-3`~ja_g@jS82em zPKDQrq;wEWzkr+v-`Wc1KmF;yetQ0=?El3NY5@3k8Negwb>vH2)07WvRQeU-!4@C0 zC9=g^=Z=@Ll~0tHWsMcU?B|2VzcIhYN4%XcfyJvl3F#L*%^W0JI29B(kbY0I)JpCn zevL|!yGwF}P<}-X)yNz^ydjaCPUgx!KbG*}2gPQrRwFm_2xKbo}sP0$-&ajl&K1bq=zS zCnf{qJ|w6iW%dJu(kxK!{JEYXVri}?L%;O>B3~MOnfz4XBwYpd@>l6FOmF;csH3K= zVzxXkzZIU$j9jJw5h5s+<})cUe=mf|-%3y(>Qhrg&op%4>1Vt=16=~wX5_BVU9lk# zBRH#I-CcLxShym$w^%lIZ<PX0qcf((^fU@7>q`Td>EXEDk2mA`eGy!9&PPs3Bn)>LIH)*@3EgDvU>2*W51u6n;0LMYOK7F zN|nxTpBjDQ7vw$6l+I2(Isq@;)u6CJCldjgKOw*-Eoc?K}R1 zLBg~oy;tXsUFg{JhSj5$Io43L7s}I`UtABRIo+#i%9c!VD{ehF5Wi3+fRzfT8tpi= zEV&lx%)t3mOB)?6S(UMNg=_(QK!xk>TDNZ9jd|Vr9W19#I^rGI_&bq+Ql~h3hg@6e zWtb)^vFHeWan3{%J=O=}^<)MKvdDV^1v31tA1DQdldr$-^{=PC^HcZ!>G8)O>nf@{ zqgV;(HwOq1aQd&SLHq*wyqpoNu?5guSnLY^!$D}h=0Zc4GzKEX8$kp3xkp?kav;aS z5HtX6dj{b3aI}CEKM=+N_Yg0(@y-}R2x}{G%il)*8RuN^ug&CEXm3pe6)NZHtLPLY zX@I7j9I>B#0Y0Ymm>P-D=-=P`C-G|rZ^;M7rQHP*TsE zBi}?_H2&++vXSHCM7j8JV7D3MlOw?j%JS)0yNp z6rre2PD_%9oKDeBs=?ThJUECt?B9ju5}S7t4g}p*0U@F9w8Wz)-b2E>FEu5PiL&n1N)M^!h*sL z8`c*T^m+y=rgCblSr4LJr14P#U`<>Z!Cn*j3y0iHb(jqS#FOq#bj$cynFSmIRVIc_;yURR9kP(<0x z)`|f3<5Zww+n|lNMTNMvY$59PFo5?lK2hguf^tLtgK9VNPOeLFAXLgh4GGZ<{CtE1 z&N*WqK@n7alTpKPuT~UQ*(@6wZ2&|p$HX6TdXkxY3}(rN?y#4BBzB_nPCA_Uz+vzb zYXBRl4sW0iSn?TQjeBc^q-3FHz#8+pt|J2%j~zZfFfiUXTiP+ZeP(|9=(*9pIVOZ7 zbB#r1d+r+?p@z3>xQO(fO48uUin_WOqp)|CHZ9x}e-i2U%;I7MoIq%$Jil(w5zTcZ zgti^!mVoDas+w%B0C#$Qp%e5&KuPTpVnXobCqhTP9}|JNo{PV0wThzpV_u@Dk1Q;T zyL0mj(T5eeD{?`8;f4aD0eR~SHgxkv`HThiX9Gz8CvLB?O%q?gE4`LTq9B$y;OpGS zmIV8WS->t(AakH-{c6WP9BCT3Rqt=rGh{ql~IyBVN8nYddfPc=yBpSWuE&vx1 z);VK05|#iUS<78Ml{Wr zNu*Z7s23sg6LF@n+Oc;GO=_V=DJ6u%=+zEDvQ_se*vd)`E}jti;w1DelmxgVaS({j zoE6ShPPlOoE=2JSdc7?!39u+PRB91|O$c~|l1QW}TGh2>c%retlw5S+-_ft}9~X4r z6ybo0@kdMhtLq;5Ed`D=Z6I7g1fijmbm!`FWjL3^`pXI7s#ia z=5Xv*5v~zNfP2&fHerQ7&o(L>TfTgGP4O)#06_?ACV#$FKyQmP7c3gumFBNiyAVfo zO@#ozoSmN5d5g(&>>=KxevP+Lf@+8ZiJt2w_=hZ63qV*)(ctv#)abzYz=84kD~*Hm z9rIHMNJ%`n^T zrCw4u>g-lZ+|`1Yt+UYGh3OC@O3qsAvE~lX09XN10Qitqp3jOh7DKOcICTaKPt>Bs zO8ug@4b;DvL)9M9q8pD8Ww9(NiWBJRwNK|3u0CDAQ4xonvG+$z3Y?aut$LPrgOG4L z9>h&7fVXeQ$n{zD`v>MgH@U@+pxqJ-(eMzHIM{gB(z5gfkZm0Rz3(N9|c zc#IHs(gaM$xRFo2b8b&8&CR_ujUYK9{;rQsU>ir7%yPA%nl!A`iKUZ z45=leE^-owd3Q_Si+!L8pjkKe#y~=xEmV5E(K$h(I=(hHV{Lstx+@#N_zycBEvUD} zq^KpoC5jy8L43-PZxX{xQXEKsz3_%2M6RD`0L7wFFEkSbdlMEEoIljEPmK>8cM3K<>0cyU-cqa%d9$dzcRR;EuE^}1bM!@C;H{g2}JBv~^($y5#V;mhld zdPyi zkw=Z;@9M`zPUStV7?<{=;3dct3LqG8$6MY)zq?P}d)wBn z_fztJilYB9#Er|C6YR6NWq|-W$qn?B*n1z*XS}O1b--&#Wt?h+)nDjf50R2dLJOP;P2Z=3!>f_C*7W$-x!{YLh z+0wazgHz|G_Dz>w?U)AqQ2*(dktu)I_vyGaaPBRd>~qs75{$vQHL`MX4$^!~8az`eFOa%**}V3;0I5cU#Ze?P(C zhd%VRFMs({TTh(O*grPJ5aVLL`0ppD2eRO=kI4+2ZnLvSDdn@UVg{ci=N7w7JU@&-5m z0`8&lH8ucGWwS$7csn94>nJJ!>cKQ%`15@XAL@}4!jWF{0p8!8BNRvndI^X<(@m=! z4CSW@DPR#7uFI{becI%^z#)iSRDq=qp9JrcMOLrl1m#O(S&=Kn&n-}UZ>>N{#wP{I zI?xg^TDh3Fg>z)#fQjItZ$9)8YH-8XZ({-&>#;XE43%tR`Igy_IJBHLZdYsWuXt8q z_R9K^0tova{3&Swm>p>e*s8dNjI_JVj=R~xzplWS#)h0QC&^J>P zPA#EmAvPc)0ItnrykRSdi-4Q#+x2hwN2!u#@aBj^8w{){)>%yC4Mt+@{H(Djv@>E zGIc%J$d2rGrg9e50vtkvG~gLx1GQKMZLHs4OIR$ca9u&}a&klOe1VbconAgpP#zDA z{lxznB{6@OW`15fheQ{@#dboR1`Kw?T@AQ=1<|<#1$qCiZ~4@{EcS7m41g0`Z`*o8 zrT;dqNRqNGu)v*N!RQob%>-famo=M6W~4p4UDeBy*A~uoLBt)wduW^30RlBxEYaCA z@%S}bHO6e-RRQRLxxF;v&pxO3RbbxT{Vj|FlxO2^C4wsPPC)|w z6(vMQSut#-z4nadQ3W_|L0sc0#H-{>ggItJXpVEY+u&Z&Cpb!?UEYf2vrG7qOMTT1 z4)ILjClUEwio2%IO;t?K?wf0*O0qIFNCxi2MBl-IV-vGrAHqGC0y_8;72 z`I4jneqqDNod7)HPKkpv)&(RJNxUQRU61}IIxG)E_fZR!$oThG z3c?h?as@I3UWo>v0<^w++3j0)sJK&$;8mp+X>nak;305{?BKd|QCG8z(t{en;TesQ z-Me>dW8|}ITYx@$gi%=rDyU~Ku@Wp3u$9ta-p}mfro4>#QBV(=yfN_sUDLO%+08HL zW-?HxI!`w=6KcCuThC-770pJ|zzKsUI%_9PbmYg}g+PCN8f{`H4}9FQJR~;zbgDyy zdBnS=CY-UC=v=@z{=*@{6Z{}W_svaB&CJes45r9&tL&fY8y$fC931F-bb1gW?SUDf zMd|#X%P*!h3o|U;MBRA}Tv{0lv8KMJ#NjwZ>x^LYqFOboSdjaUt8DS&iPOX$zibf$ z+D%{F>YXXVo&iRGzK1n;^kL~Z|5I*Jqii&Ww?2yotkIgE`jQY#mkQhA}*k=!Io6I>VPxPU!b96zcVUufAFa%AMH4xB5dw46eI=%gJrqHgCS^J#Tv1j?JtD*^6Lm zF?2F+OQOri8yldGXMyW>HZ2B8KXrCBCb13Taj<307j zUaCVE7vS$IF)qbMKxF?@{PJKMN~TD#^?_gd;H67zHA}}j0X%gz#Vd!M@GxQ7O5Jtf26DuJkN`mF>5uDgRGfmgrfhOH;* z2yklK$t_!UY|dIS*{kU z6_?s%8`zA+gaQ7;i$h&-{3k@gLdJ|J5f?*(GY1fPr@RRXo&e1L*$u@p zY}l~8my=>3u1vb%kTM_DAAy+yDaY9O`%NhrKfYV z3fJe>)=s)VRw4nvOVi@q%P`#4T6Pi!Xl-@)UPNA;ARJ2#!eSbc{3ju@`OSua`XCK-Eam;}L&lz(9qRqwy1px4dKX12c?QDY-{XiY;b@kqJk^+ zo0j{pmp7h|`0yb*5&@Np0?1IQ3>^+2&r@X;S%R!{)N$yVGhuAhUVU?EtK zYz-<9fIi3~=E$B9tYSGKys8Pom*uRts9fpVjHXw>vZEhUP&Gb%g!n`twSmeKU<-?F z^Wu$o$2+1QXJ$MI1SU)f#(knv(uQa+-^n|YD-O;1r3`mU9~P$oEC9OeTAy4qbKR>Q z5IcK{c_3X|iyEA!1N^$xt8Xk=m4zxy)?)hsUnh40HNl>ZcD4be3!)_!gWvk#;hyW9 znhhzJU$Q{nmpR)rIQ`i@ys@{b$T>3-fq;i4?v*Vsy9B@kl@RKRykF`eNd|oY zZ(CRrE>x9uQT01DI!Eqv#rD$D*}ijq=PLG50yglapZu(|lg@59e(K7HG~|2icWdrY zs6w`-8mybGmDmDt$8PuvT+YrkO>rFNXmth^nDWCG0l7CD`zAsG{!te^MVJbyJUYKb zxr-&6WEm?lf(nY4aB&UGXKJ=k6r4|l2E)p#Rik?*2D5*xXZ=GsB*uCv`b&Dj4DP8F z859hxNGlz5X$XMV5{IamxE27GyjjZtBX34n`IqyE_ox^_ei?lF9?GNt$m4wy6|=DT z(zO$UR6zQ{ft&4yuDKk>{G(fuSMa{>hL_#VB+%4^1*UdLleJm^AR}XPc{+W%>9o3m zWz@Pl2-YXqA&Z&Mjth(Lhct)!3>d`_iA*67bxPtm}sBUI?`V~uUfW!e554CoxG3~+K@Q)gRO8CqQU?LRgiwKc6 z>~tzRlxWvti9u3d6PKQew>b1^aDl%4PHpv+=7?Kgal_wxU?Hm&krFi(N4syo@a4?P-S zt6oEmJ(Ajc^;q{Gb1Y28Frbxh;Uz#?5ippGBrG3p^*}*rZ^?HS=de(MlAel5oO+2@ zIHzlIm^XN16TC#8QmGEJvX^^~!Tqp$x`O;#w6IrPS-x-;QPxhru#qDQdO^(a^pwKx`ojX{7dA6M>MJ$scmSCd?O>rsM)_pn2BqBk#z9HWweeuLcLkKz*~z?`@C3!1Qq{5ri_tpH_} z{f)D8vorIhGqbZ-`Wt6v`$o?V>;(Q5Q==0jhsxF7r>mir>8IEVL{@SjJmHV6QMEdg zk7ffci>BHeS^zubWget;LNxd|Or^;&cAGT4=RjK&f(v{?mI0~}+rRU6bbu=*zL}HF zQ;HL#UtHAJXF`G`Jcoe*9pW(*CKKZqr&_jVUp%NRX~mMZ;$gkE!dL=jQe$Opy0(5r z`u-OT1*^6ry6aFL!$BWepTeURg~ zmyM-9xHuV$4ee&@f<*9V#|hWM!~j@>6)Zbhw?%<)T`J;>2E-20ylfUE4O0Hhw4m&N zuDe-;XigwVCF&Z)U-Muy>|excciV%Po7OTzWFGtOHg(3ix$ zgmQzw)&9;X=H%g#R(mOFSmJ6$^_eZ}2lw;y)7$rLFYUOT>YtyT+BY$N?D*)!947Jp zU1juzD@s+jY*H^pm_JhG?_|K|nhbCeevD{V!(>;CVJ`3kS379ncQMyWYjvvKwlu)K zw#|R637H{bSB){8Afz!L0nt(qlluC6G8))3Ye)qIb3PU7I${k7vgkt1#%y*i62B|E zhU_?Se~%O(yS^8qspUp%>hUeX+?c>l#{SQV0$iJ~z==f21j)P*NDh)9-Bg{oA<8Qn zuo80_Ym!emwj|j@hQEK2R6);#AV6Xavvda6l@aloAKbx8u%Ej3%cR7e9UCGYu1PCj zZr<^uyY9N_rp?=g`KFV%zii9xJMO>x?j1YsMk6+#&du=pNQhMcQp6i(WEw7Z7>E1? zM#S5AC#S>;5Imu5mkIwU0O&>oJPB~bwdeyC5#o)wmMqir0$HJ6KToRS^^~;2B&I-H z8?b+nkN{Vh4&*8c@MnvU$E&)qMQjW3GJrXt(u6~f5uw`~-**DE4q6uDV6JkvarM zLC~ac3c{gUPI1N*0J+?dWNQWpgLW*tV`=2DYB>?v2wcW;KrH6(#!Gf6YkgVy{oy`0 zYl65rG{U~wc$NY{{izR->Nwt+i3u!=0>~pFiV(tTTGt9IKq`}6OgLDsD6Nj`6x@~h z^;(@_CFo~%>Yi*;Rq+3+S_v#YI_IMZs)&e$9`+u&LV}9_VdmsPur@s8Dd++`AuOGix+xzdo{{*fe<{8Op(+W~D17|MD+k^vi6( zP0B{*gcf7NY~V;D`kzCUk`zWYs`Zl@CB(_F6Jcmo*SRFva8Gi6JXR-c$UG)c65~2f z1`9ytx^BcbfaW5yXxj$jf9DrqJqw)>y_0$C&&Hex|JL_M;g_0VIVF_g|DkXG;4|b% z|9}G~f?uET93ZHBMQ1k3j1D4mt%p|e5TkUOghW+FqX5yVnWzBp3syp$0NC3e(3!6L zAly|dgvb5>Y`4SWSXJ_DIZg2IH_4)o7i2!nQKs#VOVp~ih4M)m6pDoSEsbRw$wx9r)Y_lFW=8BpBt+>~wmw$Uag6 z_Oo>+?~_d!kQX{+ivg)pOROX)KzP|- z>a!g$bB&W)sd~k6wvi9<6qZ9ww^sePURc^DSs&b3)my8MO<|Exnxrqf>4WuaU?y#_ zA_-b_`>)j$&|6i@2BAcb2)M3vYZD~$B9tLWz`I`# zE*tttiDX$w^*7wXyMnRV(B~_`F zx}KccH#J4K$(fmX5`eCf`7=uL!_I-J*}=xf4mXmfjV(QRgnp+d@iy8KjyXZVXUN4T z`9g=;%KB&6j|@noH;S$_1R7ch4C-wGbVwj z7ve|VRMbr*-u=>7zxs{ue(Ag4{qEPi@lChiv~Kn0n+S@n+q_K?F(Tb`l$k^_Xv9zC z13@DubqDZ+JL!Pmo)Zjrp?@(8luf`tGXN5;+eOIxI5>rjb{9;#=!|jzG(*SsiQ@Bl zru!1sn5+ShgzUoHq9pZLwo7z0d$G(pHp7Zyt%wr)9J)S6r)<59#07rzVk5Z>)^%d; z<7JA_2#xkvI_96Om^#-tPRmuIU#0W&v#$IaAKlqEJzKiJft)uR+nr%-eQ2!W*GxVI$?WNivKwiYz#;$%4ci7Y3&xDl?6(s=jjQZIwux3eT z%@I-qUO-lFA^8>@5nOflFEoi6ATF$FO|UJ1`mxh%O$mx4lfyn&)P<8wl%#|lL`RL! z8ra6(wJX6`%#2>-BgIo=x^COmN65A1GchGPx;-U6iBIVSNpQ|l1UWCn$6QPv5csvX z@C5JIU-xR{jceivK&5eyHU4vD| z#E4tw%hb>Jd(QK_HtOl~{C@X$@9jfrzvtfbcF#%d^Hedwck-{6X(4_g=&LDlr(z3b zQK1}-I+?3K&}iY@8pzUE6X7Ge@YL~dAP%``F_N~{G)KE-nXliF1*wok8;Dz`h4AQa zYGYK9fPVYg-@pfaX{Fz&Zc9FQyrSxiQUH)|E?&Iv#ry8N`20f;9er+1+skch?i;%F z=+WQ*;D<*j@<<7z>sQ+Fzr$*Hq-H?wN5F(D40_?;yr8fjR%KOWlRJBooEq8^BpZP_ zTYn{JDCZ0ekXg8mf9t-$pXJ`wlNCfbF}4T1x6EaLH1QH}TziKO2{8Gv@QFUoN~KSJnn_pEGCFxcWJt zf9ISAJOM2u-#UBtmN%`~(*D*Kxdzw^pi%RtwJhPeVSLk)#+H$@7d-jv*SzL6RE~PB zHHNPMeX#+&k>U{BLfn;MNr+d8RM^1>#eKxE&da=BmhySw2ZkkL52!>S3qBK=!J)=oxyC9l$YCtJvfw`LMR09t1 z4BpxUHVXQG`3s%Pe#1uam&Gr`0t9-xRPU9G7uUS_!hO#@Po(p6Ll-s;iV;Zme6%0k zx3iTj{u@_w;5~F+TcLK&pO_IBEdBI3_}uwh?31NI*ChR>bQ*6o4B~wO*~PvW&<|l? zqr(+R=yw(+2*KY)7+@V`l0gxc?;vWJco$$bv2`EZhP@xh@Ry&xK~j=rB{6^yZIu~@ zfn{i83H{(zlPnFPk}PGr(4IggSwgODfYnw)#2h~-@l2oos>6JKrl91|ebYgk&(b%%onM3^EHM+CXrMpEAMRlQMe_{-DQh1QmJ;VP zW<)v^pTo@a23{X)=YPp%m&AioI|lUgF1$lxg6znR08(FyKT?4I^0du9ceJ(P<-)s* zyX)f6#fuj%paEZ8v+2J3aCJR;Y0V}E`X70m;*SFzZJlioEN1OT^R))9oFt;1j=3Z9 z$05o1+Vd!azPu#>P$3@dI_9ZjO@YE~?Lf zc*Nqo^#J>bjd~say+*)SO*d9^D*z4#6)!j}VzTuCNkiIKC-P^3D>!a|uI%wz0S37r zZaDcf+hRfzGT=*1utFue^Ue7R%3hD;RSjQ;6Z*lhGn7KP@4S4WBe#sU{1p#jV~JC^42%J;cZJ=#xaQmqYa~)$Bb!goHc7z;@1fe$+JXe+Dr zTtGeF22NZ6j=uJZ=}4R{{g-FK6aID=S9=$QE6EprLI>F9Ri3sz<(^q9K|&*IP00_* z1LGDlxn;^FHUTa6><8SmC%|FyB1f_VU~UZHRk)xK5AlWy^2@j%!|yA2(_}hpx?|re)z?=hCtL6x z-Uo(lDam_;gw{`i4W`VQ0#hI&@t!ljXPJAf_5+gkgvr5vivhh8({y2Lt$bMzN8W`2W~*@(dc@J zYP=GH79;he|B~3*WlRx(dMLbqd609y4?XnI4}bW>Be&lAIJLf@)4Mh^6Vysj`<)%^;Bn&K9*G%91$KT$3)BC8m~Nvt?gesfy*8Fq=yu!K6)f*%Ud(}pJV zyiBFo%mWgxVjljxPmQTE0_i;ryYI@~AXEH=7*tF<#foF9>^U0Kt?Kk_HW57+sYu>s z+-W!h)XpPenE$;O(Y6hg{tUSA(J(@RPR^Zsa_-F&X3m^BVZxXR6UL33JGZ`JTvJoy z0*;EI4Y#*10NuEWT~LIg%DpEbK>u}d1EANkPd!<90Mayi)h!2F?VM~$neSqvq``p! zEbm;yRxVfYLND%$FpqP{3vCW$A$OTo0m7j!amdfp>ju-Af%z46In@$F#LqeYmyaR!x&W;^uujnSFRTX zD3brcRFdPP>@r~1C(9e45>pNg#c-jeU%xLe!7UUlQEWAvY=ubeL~nyX}?ev z4OFwz^X&J3S$is;Fc-{$rcQNp3UH@0ui62IWx9f=@F_<)g@NvtV(8qP^bN8Defrbi zz!`k-AeWm?E{9HDef6g?w$2?hvK@!;iUli3mqmE!Cy{Ib`V*^GsYCBGPf#~G37E4F z0QnP%eB(YE;sQL#kV}11+*82U>S|FM9rBer>%b7m=1<9@!{1WrPx$-Mg~p48!ZBPC z^v(JPyWCI)!6_8W?^MI^IE zWe{55ipI;j&uPXE5;*Ve7#tiJIDSc@4_OX7+6MZM9(^4AAAMw?tFv=YM^CHdC!7iq zZ=`n~v=rdA2ly1G9lYiYA94(tC^MHp*M$4mhE9Q7$I;~s*8ytBXj&`QIFXz}#IFRrbryb`uNU~o{*N&o)g z4DzqAD=@~7$+#w?SWlmR0uNIz-B7{4S`Jc(7>9~I1mq&JmrM`A0SMmGS*PZvkT(I| zl&4?(Y}A|V_I1NIz2O4q3W5Xfz53+62M^x1{2-+x-}?nKNA6T?3ZnY@32X;5n_6bK zuYkxLF&3j}qYR`_J5fRS%B1za7Kxu@WcY~12fVpLUq41_gJVMfzSZRVRy7U`=&q}= z{$*F>`1>oD>-2k;56oLP>k<^wD>wCr_kN+p|BP2D=bStfssd8I_G9&+p@|+iU`C-BoDB>U9E^Th&&yg$H8|GVf~#Q=)w@z|H0G! zlzqJQ)*l`{`uj(ZckSxy+S`ilA5zs+3VX5qN0w=uJmVm*Gf~)d<8EFqY9L#vejkQ^ zY;~6F;RWvj!oHp1PSe3aZUJ261jc#bUJLZB3w%YrD1UD#R&ap+~9zBBx6%$h&xiJt|m^)4h_M{gk`pApOH zMHALFoAGQa9Vrp9ci_vlmrNaoe<0nr+zm1vgi~cDF=lcAhRGkrTQPF);zK{rfw45Y zzU$3X>*vZ@+&pg76#WcS8tNPBo8gbfkq#DxB>nIL{G#9j>bbSL&huh$uL4UiUAaOH z$}1w5Wvz4M;t1J5ZKyWTLo1H**8GYiL$wtEkf3Eq>rQOSI+=(4Cn()*EO!_(f=l3l z5P@p-Jg9T<+20Zx5vRu?p6R!8oO*$0KFv>y&&yi!#*L}__H^#u+Xe1BySuwOy1E7) z=^yAgj?#-U=)z$C5ma9)9X&oUIIw5$uD!c<^(>}ZBq2mDS+tLuD;b!|8p{=3U{cCJ z6S>5C&kzk6FtuLm{{c=R#+UN{w2{pAd+|f2nhz0(1{x6^B3CAq!V4+ZkQrtTV;y4y ztGKrn90E3r`fM0!pyf=l45}Z7bMLq{->C1g`0slAlP6*mg_#yQW{$rCdo2F}Eld22 z+RU|6Oqzeq8K=ih3r)@TudtmAVEC|+W&ds(KfRCfXP(x)kI z$ih!`j?$Q?(@an(Ivg`yrIx0S9=nXnXvj9&&_Lb@v*c8WAEbU9R?)^KBk_(kPs3NR z1sXY0n*y|D1>(_@7MHaAdn4H0y7_@upqF@2c9H_DfBhRo%hf1h0AL;}Stw_jX_r+^ zs;`iqfcF^<`82^kZ~DX>D2iJCCB(+b9C#qB(4Z(dKXOg|O%*^~K*nd*U6Id&X++yV z(T+n!Nb$$ofmC2`0uO9{pyPPo=|iUm`cISCgjN6aV8KMLgMFIxuB5ekmRtVE6t0{!hgTdMjDBoF#70+vq|T%|RK zxsDH#^oDY#407~G(joW?Kbg!G-Y2HNaaI%sD3Q;>3+crTX+^gb-jj;;GEkq^o2r>( zl^S5wr(uUK?mqNx(WglLl}P07jF59!sHPSZstM~4w$Ul(FDV>TMD@xfT0iy8@|mF; zPa8RMRwI^BoMkH(kWNKv0!B~-qM#GKy(fAP_nz3j8&A;{=ugn!Dgykl!WFSY9cr&$ z!37!i;1mQkabK@2b*-(IzR&_}5!K()YZl-Sv$v1#3rOzboL#XD#G}1lR%_9UL6?O++SOu_ zY(NP4yHs$sp_y!2 zV<$9@8H+D~0O~U8Z}4 zS3url%>yHCh%3YQWfA-3)Itfyt}&%;Rgj^&z}9K5rFpZp^7_N!N%61OGrZfM=ss5 zcqNHWTo2oUbdiLPf6?Vv6qgsd_N?#93{=i+0dy#Gkx53fg;Uxrqt1@!z0Qh{5MFyt z;zBuQr1i?YsR7T@6AMEGlQTdx9RC&%=@grxrCt1h(4(V8OCf0%f&bA?vp9YFHQ?EH zPK%_Dn}Ct%(<(G^{xXe(4Xx5 z2fT3SvFj;_z0-k35`O%%i04$CAXZ9H=o9*MOw1mi>J7G}L(emGkv^|YPD{AmS=#;r z-&}p>%liORGy&pbON~>}ldj30F%R)i7}>HX`CG7%(ntS%o zLqvhM$fs!#*=WE5xC)Rm9b)QmIEzRVQQjs^9{uI|%8HX{F8lxv(+dm=xws7?Yhnp9 zjb6nbYM`6ka7Vbwiu+7MW&+vzL+3U6Y4Vmu3gFZ98M62%tv^(vSMDw8a4HuT4|PEJzf$MD+V0JeZ$P{kIjaOg3nBS|{rip12nn^%ph(1^B<24R zp*W)ggKLTMTzq3)&-#{X_?jw$rxjz7iO!^#F7!DNmokDR zNX%PUG~>TFiS04rNe{0mEAFVDba7=ws7#+goltT2hn(m*H?fS(Kpx=b5S|&|r2-iL zSgs)6Uf5F<_+}eF`m(fO$Ax0`lmwX}wjcA(43Frv+3&2IkwbLVd(uIburiJ-hgbSx z4MoBwd`c4FoA|c48q&q;S?v?~aivatv})}<1ja?k2m2zxpMBwXgTa^`(wCNae|moI zFmVf3e9t@I?3EuM09vjv{>f)(ZfO^d95YUlL*r(igl=1YskTGKDuWJfSqN+ zK3`lRulZX2V>b{Na5vaXqO$dwbCULh`}IrVGpv#dRFtU@j2ltx9d{qvy&$91~^Gr%O&L zYpW63V;fKsBD&@OSh8(E)8di4gM+12P(-V~T>~ECx5Fj427qT(H!eQijjvb+Q^w`- z_JL=o+Y5%M85RmA*(@4KNk@PA^FOmmv$kl*otGdw{0y(&EdP~B z64h9EEg+l&aj9W6*dm|QOkzOO1@6!b_!#gHJBa)oVv{E!YF6v z&qN2t1A+X5R75h}v16J@q=>T&p`T2O3s!6y+1S)HX3VU{u@k;_Uub1AJ5g5WWmiQCj}eNR`+kYvH0WaKE-2%%itfkYUU{!90SU4aj*^ zokiPeJqaBa3&0XKgwAmArKh@^X+S(AJ7O6EyS2q4te{z5C&eVAoGvc#i%-Yl+tPzV z#x=+_lxe<86d!+sGEUPehEWs>gPsbQ<%pCDCNALR12YS44W*pGRaY%@ra4=sVG+mZ zIxO_-LT?rJP$EDb*zbNCkVF!CS*AgJ_^R?0yvRN>xI@siqynxh*}UXT7fTc}pCr7i zXo=K2_{^$o2hYX&YOcv|Fz@JS3}#O$#u;IngHwVN7cG*XU_2JlCe(mc1WvTKG_zx@ zTesnn$A5X({r&yFBp%?QBhM2d`~u>C`o3)YOjFRm{>5$}9iULbJ&pfb`xE&kMl>a3 zpVD((cWbt;Tbzr2#z8<*0E~61xc?N=<@ZXG1f&tqVjK-Ph$aUecbz)4VgLU913hh> zyNG}2=-P}+ST$Yb1%C9_{(-^HjZg2~0b%_i#)$^@9&Z3Utp85WO#XmBEu6ae89|>X z?22#HE+KP}PBw%(0MRnO!`a7YScONVu-v9@Q^k9|Xnm9OFJ2_1TL0&2RTesjU}U;5K+mPj*P=NdJRvKS#i@4 zr+&hQ^uT)N;4zk=OzZ&ZDsiYd3nT-1+pvK?O*QZG%5InniaaGH$UPzi8~JqQjC@S7 z2xc>7zyz0?-cW3=^q6!9-m(CN@G}=B!Gnd{bbcUC$#9Ma2U*j|6${QheB<`p?>~L1 z|B*-gFChmVIr1DF@B&1f{EPR!KofnKoq(?w{_rn2;9pLUuDz32p*$a-3{d)FPC_6( z`{_DUAZLPKVt-I2>$veM4KA<#)YvXUz|RU`f^v8C(*lQY;eYBZ$IfD(~xgn&g{$ zU2)3Q#ia{`-%t^S)SYs^C(>(AB3wc`!a~SJH;jHgjh+O>F`6tq6YbMd{f6{LQ#u#ZVLv&v7HIrnOFM8lY|jWhYD8yXhD1TesOM$rsrt$O&}xpRB= z^bM{V8oD^NX$@;Xm*7$m&vr5O zl&(B8RtPBZ1!dN#3iOa0VBVrVz^4JQL&JUA@()yezr2!OH4Ati@YnL+v$u24V;eRc z>O1t@W+f&Vmk(%D4Zd0yMWe@Z6lM8T=h1<5@~u>l80@F*+2qvar4M1N93O!b3h1HH)cpIBFKL4T0F0{Km`%@7&aICe}uk2JO@cifof=7vSu2I`w95J5d=-MSmkKP3+- zvGJGNB>P{vID{Q&jngCyIcLtMix;I1T;U9rh@Lu@`2zJQi)~8i7dvPe{DPdkm@QW~ zWQ?fm5TK99|B&tj-G+U%DsPw$z0fx4<}sT|lTElo>xp{a+i~j9q5b>sy8m?N`o%q6 zU7N30CA*b9eUJ1Hb>kFRtm;N9H!8Wa3tEt7QQ|+kz%oc#S<>QZ(3@wua}hbU&gjs9 z75^XSBJ=pW$Sgo_R^G3iyQDa6@jd2?H)Eq&XlJ=XAf)?pL+YUs{%oHQOVEpO3Z%P|lf<)rmPvrXZXS6rsrn`B zq-tOg#~b8FUe6@MUl-wBft_LleW&FpAD=7|5~8^wpMrd019}*PJixrX=fr#f?V;dS z`$y4&MdXNRARYo$m^o6(dB9ype-o)x7SXF{iJNacaHwy?_U#+`_V*JZL!7|rM;_@v zJuozQ`t<1m%qJZeE_7V(2KhsSxJ@serc`d-?|z}$0EHID)k3GK3jItATM1)RK~I-L z58YQd=HiHFB0ggeFz_KiL2BoXiz$0}0sfGEOsxK$d-@LTzw561@8@->XYtCO&duwQ zb5^e3e7yhhqy5L*oFZr?ncgzkdFRggDnS4b*aHSRnU7C8vg`vK5GBYN=HMTJ-^;bx z17ak3!`&d3F$eqEePyRDREmYu=a;K=W_}s3^=t_w4g1!&wNV)J`ZUllE~3qy4|O<+ zPGl+;^yh#4E?LGLaR(`=C*w|%0r;cf8`no^o)Qu+$Gr->EH12ZYzKy(K!BFTx- z4_HxPp|VXRdtEXPQ{A=5{ z-+l1jdk@~ef5V2`AN=NB`v*?nf7gTe@4x@9Z}QboxbP)W|D_?Mq&3nI>V60Qf49&J zh=K3Ob5qJQiUf69hp2SIofFu$JXe*VmvKSo1jOjUQSHu~L2Ln)Lclw>hJyT{41i1D z7%RU|dvz1@cBt=EU*92ayEa3u1jMaG{NHo>myh&y?`7eKu>O$l#ChdJJCy9uSyuFe z%qQ#)9)L0o+6I4Vyt4Ew)wyGt>;;Ua*RcvbhIiZ&k%SU1fM3a)<>pO+UR;11-Ly?* zMAF8oF}{Obku2zZc5Ss!MnYGxLQS4+SCqo=gdYGRmX@F*2VjSP*E)bi9Pq70$>FuA zxg?*K3Xp%RWPlWa9J+WM+ow?`X zSvSwU`sDI;53jogI)BT7^XHy=>eQ({yAB^dd?$2x*RIYkwt()T?(QKa3m-!M;Vdl- zkUKzRqBN%C5{pWvXUeqp!^p5U*p-w(hEHdGh-iOc{aHylG0B@j}l)NL3WA%So{a}-@pIR@xHFky%_!%Ur$xXQ^Wyv?j_n2wHKOhlJV0g-ZHgg5UE->%MJlVje zN|SEuqF?U9WDw6WQ%oW+mSr4(Pa~2bs$?B~Hu9&LGrd&Shaoy@vQNbMJ1XBEWm*=5 z+^@NeK;C3LR?-F0EsP5H)vd-0ro5rW(u6;0%ITX_cegvT84fW~sM)AeBnm9U- zDYWq(2p(8>^SXzLj5~Mk?3stxtvkDW_wGA)J+-H6S0{jKYrEr>2Z$&z-O({bvGh$B zuUx4U!V}Gzg3pV!nz^2oVcL^HtYJ%+;4PVmEx{rX#DyIp!Y?&korR2c$oj=bl*17i zVe)CU^|-ipbrJM=oOj>-4}Rn`AL+m?uz6PxtAE#_(|ua|b-r!7;m;e~M_WhkEs_PH`( zp@?FGkh6B1ryMcuqXEo=X22m1mG}goCfAz^;L^TE_bjX-yS~oA!wMWrO8n~z!ujPb zQ(Iy`(Lmhw7fY>}#?CWN>>SH@p4p|LwymQXsuT4~1Lz(%$|dUu*=nCAOFl1&@jOk& z%RI$%D!L{bMstRA76WDiTz5-NV47PJO;O>3n53JBVHnV_>=C^9;sFz=?4XNyY95C# z_?~-a&73fH-Pp0KX5Dl4;dM7Ze71Ktg2Udu9o@$V23Sg={%&d|LZp(~grlr37I)#h zk|B+HOo$=MpzP`oQaFK?K4lq@95al5_31_jq7ZjOKPl%I%V>1~fNpmf{J^}`#1E~b zzTJoRZ(X}}?b^P@8&_@y{_8tWo!Yf}b-KHzCj)$7$4(_PC#|jWJb&fcw|w^oyb>lr zo|YfAT++@4A0f^bA6P*+k28@pje5sDhygq`?Di&yXx98*e!&bXQ%=n&9}@G@jxMlG z=!YY~J|7v|M=eYgbE@iTO0!_SS9=5mD=F!GRNRC3cWmuc9GsoKHJyyZrGh zF_d%t_hZg8IJYFh_>zmXW&Sj13jgIWmT&6M3*V9CW*URi{=?)dBTBl@0{aGU322MH zV)LOVM4i$J(nG58b72Fh@QQYdLTU^3to-l-8vqAmX0KSFWAiQ3#*M=p@I*V=QfAJ) zdDg?XtXsbP!1-O9*Kb6q<}`WNp02L0?rtf=L){nJycKXDrb2xYe*|KhAz?c5U+K)` zlNWhX5(XDYp`Y-9lmKBLVoD<3Z@Ync4y{;Upx&-8E*5`6_~Fdgo!S zLw5oHt!qEGUvUpqG-}=Z)Zx|pAW8f?Lo>ZSbZocVX(224d|a5IE~K!rH}mTOJDlPXFk)# zVfY^Qvcq?B!1>sTeLIz5S}b6_7vRs&ve+u5_pDQ9FBJ#a2$YUrGXNxr(-fBJFtP&k z6zozdPHh=^*o&6S^)dvhLwf;@BrW%0(-hV%`e$K%tHc_hXlr1ybiL zI{DSn)o~my=>O#}zuC8O{oY-7?%uci@NNz{_wGLCQX1f2;ODh(Cl{(6Dbp?QpM2%J zq=NjEvr;P#bn<@&e`bC|!yaiT7I)BR9oU5*B?`rA&p0FwlH1$j(ls{l2kyNG2=zw0 z7$OVFY6eOHbHo=_kWdEId4^rWH`qs}?9-|xh-uA-F3HNGZC5d#32=HwsDCmj2m1U{ zBhohCsgu$0#_O-^PllW$g#Sz@bM-a}@212)H_^lD{apD=Uz%{NktgE}1S36DK?9_<*Js4n)|urXMo$G0AfxBEhZDT=S< z{-luox#D$&6nq~dzNYdr{9n~84V_#w&wt=v43Tz!uD*l*m+rVsxc~nBSpTy6KC%+s)Ou1w)?~}3LLTe^TO@BBR2qqpsXLMjlh)zRo8d2BC10TzzS*}utbxG z!g%x2-84hVOfADZ5&~Xe_EioZlK2D3CKOvi(TP<&Y6dw|x6!n1(=IPQ2E7>DsOqUQ z)68NYfE$QKnnJI<&aj}^4jU_{57T(c7$pqz762}Hhx;mwDIF(0Pn!GX-j6ej~qFB#WOk^?`j*pXJ*Zvk~+EK zie{=F({jpj$ZN2@E}$A@R;aD_nbjdF;`=SwTQ_FyhXB6?CXI_>G!F62!3lg!-WZ6J zzwn8uIu=v2{VVoUr}r=v0RGbX$UnMaO9%foH2@cwD6?K!^0x|*M*y77)?sfho*+X|%$VWg{F1+g zcw?HIT9xxl#GuMVIks8jP-I5m25UGd+FoJB3QgkE8TK+S%wqzTzM#c@{?dsK9^yab z#8%Fs4!Pd86621}1FBwy40W!&^6jTwcC2UZJxKi(OF2%rqeKnEDD>T;0|Y;4QWV4R zgD6}9S?7YgF9yDVZX%Om&C%-%G(1ImQE`ir-&j{qG>f5=dlB|cL_>->AJq#OHnml^#@ z3**JRVicz$6Ttv{LsBvK6gfy*a?K|!LP3B9;DHc|peiG%@;*!YX}i$T) zIKXEdSD5()8)(>Bfj>jA+7 zHZ`n2cklZ@4JF!rvMK`oqc~m2HITD3{3uyKAr6H_c;eCKBVF(ZK>s^mP&N$Y;iepu z8-d{*t@=wV_lRVKfcpQZ8dJz7%4J?t%Mz5~dC~+(use#&DA|PCIYh=m;U{`~@d@mP zj_p=0MREnLG_9`FDi0V#k*XnF4RtUU#1KvUydnSy3iNZKEw7iYJh22V3(#w}8puZ0 z11h_}-z8f#TmeNPBzyp^xGx^N>AmlJFZG+>d(-V3Zr^&-!6%du68-lWC1b(AIKXb- z9iI|qNlAi%Soo)l3!M3c3(^@nfF%G&K)AmjSGkL<&vFQ`GdLl1Wc#8k(wtKBB{zU? zsoaoGo{UBYQaMZnef4guyud{H+aeT%_1^=In{(MT5@qvUR+Cfi=amJtDX60&Y7A)K zQKf4R#>~g5P9!eE%_tcuZ; zgX|bfdw;rseHZKt$}_e{LCSiLAzW3`o9UmT!Jv%VFBGeMlJwWwVn0kSs$lO{ZJ$V} z=YHU4y_3>G0OIG(Yi=T65P5@IMmCZ!s0C{OP22pT$u*#+AXF1#LhCa4X+vfShIHlTasT<4Y{;k6E4={4@}FH$$VGB~tv5sN z#dCf33?yCUHt1_NQabRK<+nXUGT>t{@IEEGTkX1$GK8}JBlBw`aB(r$QdQ_bZ>MH= zIMC!u`&}xg@G;2ADu`;g7*fV)?*rK6$w^|j0E;zYIbU8%`6AY54Le~&riL)iA>$e; z#^{eeaLvlDn#pVxyoiNBD+qbNVjKs+&2Yu%8M^WBjMf1)cHlTTmjX@^PpcSnMuNm4 zkbkdg6(@~;@lW?W_M=Qi6=22z24*3Hx)8%ApWXFV;j+CkpsSJZ>{KO`Q&1KUgaXS= zG*v>j6aZ0)?x9(Nctptvl_!{&X*l@p?@jVh@dMDoP{meoO*Vr+#`C7kfV=}wkSu6H z`;uvmTb_J2YHUYsfSnKx>#cPOe_eEz!V66v)2af2YJUsoc{AkA490#+J0?xU#Z?Xh zdw`Z7Oum;82s!OLdU^Boq+f+T1KGK9C0;T6yI&rW-M9iTc zFvY2aWwV_mf0xDkf8?tEu~^#ud;LfJHnm!X+{jq9wuCh(4A{C_(mZyXtMRf>8{m#& z_M)!m%Jrq{!2g-Lr?k^pRC#g}@dob{!@GJehwl9l_WppSSMj3z8o<5{cW%X)y4=d; z-Wx)37F2Rk><6&GJn9HAOEg}>kJ-X+w^V1QeZdoNz&p@{cfh&hI8-lXQpOS>1JxmB zE=+qrt=w_V0wOQ+xw@q8rRiaJjxE3*o;Jgdw9~j_U zmT-{&`6g#WaWzgU&JjaxtEU2|2{x^ANicz7Wu4Xu9pav$n!@xk=pS+UhgjlO_Dz0| zjwDg;;FKSeRZKkdr&2#%%XY2{hzq7!>pI;5aspc^E_(2A28hHENdk3*dj{qSPv&aX zm)lg@Ec~>K%+Hg&w@Jf|`jseCHH&432gr<&-;_qx!Y&QvBJQx&=c72mc)er%_<8T( zqFc+gZ`<-V9K&!&!@PNT%EqA*;2Y4qCWNEev*86Yijr~qb#+ERmy80kH9r}kga;KT zanX#UKPYrv$nAu=Kdsy?P%$JvibO#`pFg4PLOXUM1+|{S@cH2OTh2>KKX>4^wI5%* zcI_A52fh96Z-4i@-~Iac-SpU5%t4ldLKFV7C<;r4MM%4xO1zk&6pR@FCbbGo{onA3 z**lEPJ<(aQd`Ja|Gl6T!K-8OdO;T&Sz&0`^GL6Y5q!-8XXzSSaov`;S!y&tb^#l61 z;Ub92nmT6Y<1d~T$>?AeS5l_>l<;o`a29RbrTvTI{9K%&i-leh2XW&`Z zcU_YYqQBgw`Xw@~&omEc$CdZS zdI6K@N*+pHP^+dxFgv=6rPYztDt?+9KUYo{)LaPJ$T@$Uh4v%5(@x-wg7$jhk3uynEeB;8A75e9}V#HL>X}4kUZ7$9b1Hy1|HZO^I{Iz0{xezK8x=w zTfA(a&WLW=A0~k8NYC;aJc)e?Q{q>H`m2CiQszOgnjA4=v6B@)Nh?R(Qld_@H3GiP01NdBGMv$?dU^OwG zPlX49vYn4KV{%~_t`*s7Tr8mTMkR}>kffLYX57HzVT>u_n{l!balaFDS5?5YP3*&@QV!RE9B9TPN0Yg1+W5_AFoNN$E;dK2?$`6eS|p z6X<|(YPCGQ=eFPd;N1_dz3xLF`p|VBhpxNs<1heZK6u~S?dJ&cLkbFdcArX#!we3D z_J+V0CQ(Epi#YiD_eq%X_yUM9dgh6-gn(~Lj1?? z-oE|7fdd=1-@X-gc<`>fZ{L34TrUie9AFlptUMMV1R(qYg85SK0WJ#zq5@|KL5Mvv z?<0A@AktpQA_!^yH&-~+Q!|BsS#M}MM+Bgi8w(FXGJv42h|fGQoOzYx`l82qgLx!M z;@gCT(uhP6p+DPUZ0o4cQ-?e>8T?yQaUiRFg`zx0VdI-*e%B2wmV^>iu7_N@WUUh>S|RK zTy_IX0$gP>rWJ_QM3kaHiP7!~8=y@qwp$4pk!<2hE0C~}C6USx7$v6B zQVo7>OKC|}{0B@l#@|f*JkL4z9jj08otbyu_g)G-XXczUXU?qUbPYDmD39FeC<|da zF+d@5qtk$LX1=bbn51@N)y6Af$@oMb+pM3eJ9wU)Pgk{y>{rtrl>k^EWK-8>07T%h zz$VAT$BwbH{r2LyzWvYaY}>YK+qNfq_xC=rYSpSIdi#d=_n{Hpym0Ou+`r5t`};1S z3Qg+xk84Q4q5FpbQlPhwg5o4tySxArKula1!O#m=4_+5Q+ohL&0TK}Tu>98M03<;D z4p63$lN4uV#6mmhL8;tm$#?3+db9!T7o0a3M^GjURpqr}!N#%mCn;RgBNYZ6XLnm< zmDA|Y&aDmSJ)AIVaLGK74rP;`2g{OsB9|sNPkj7+)&BLcVw22P>^BINcH=~#)DC~H zI@QJUS4jv|5Jy4gFmEH+ZP;(rDqR;|nxkTG!Z!Hc*5 zw584LXFg&s(xr?O&sWCVKm=IngdC6g3OWgKfbbDiF5aU8rH{>~gTvA3l(lf?(nRA^ zZ~eH*4wC=n*mhUk3?&Fq#g#siNrd#EVIdIFFoQ#OBuJ!-tKH=m2re7c*;Y{$8u{@} zbjsrBPm~WDT$Bl-*bBv{8nG1ei1O$A7ho}rFAk?qi;|_RT)DQsy}q8DiYr@Nn_3!4 zFwLe(wh9b%v=a_AfB+aZ?nX?Uqi_?RA6&Zu|BJ2^q%V8Q03Dtqyu04iH@MD6Z2I~P zurz>K1CRh5yLA2mq`uSNx39Msp6}m3yswu~plwgIp$D}A$82E+NGgU;FJ6D~_Iai> zN6A6|L>R{iKuAEKB9|2-1b_mbiGd;@BzDxaelkWbcTwg!s8lLFSD4oJ3>h@EtJnMn zZ(%kF0Z^$tD7Z;8g+zFL^dbIOOA|7H!7d=9xp#Rp4L9?sOa^^8Ev2X$@_B`=>oz?e z_tP1Hq{!g;%KTP4{$YF~b(WBXm=~1hM<5?)d8QW369TZ2f!}!}$`V%~JwoxJDxyJk zN`eRH)QCVbIF%cEEen8BR8BURJpy#n;f5}kn9*fI;g6$q26cOkq>OQHaPua`f3^cs zrmU@RWIFIE8|}^YjS3DO=;nA~Ndut}6$(+QUAitD2H9strq=5Y!(GfBLT${i=}-C$ zFQES|{Xfa7gO_A9JbV`BUb>}7CA&Z$Jh*bIcjxdh0st8KKV-oCxO8LRxy9?}FM%BQ ztpOEywGSBpauA91QjH`4SPEPdHAl%yo=@il%Dpg@PL(23)ATzs07bl;+&k0|8b(8L zo`_pzt5`778H;<_-+9>vl-2wYl}c^U%;Yws7DMHnd&so)WqtlMdPLi zDB`pHt20+9N7=FK17CX|XU*Ae^@}1ZlXau5U57Gc2z2mX^sl*dJQwIIaxK`PA=Lbz z=DR6cJfz8DGqoT{TvLH87AjMt5yySc7iHnBgo$R`rLTF=(-GmxKn%RBgddY8NJFJ6$I6hHtUSZo2fc5QNDbg;X>|MZ1ZePdULcYdgXBw$-e0Jbrw3hrF>#J2g{nvd_`-$IB$j<`^Q3&)vj($sPzwIfMl z7aqttxB7J-^=UDmI`g@Sc5ys3AB#Zx>C=9t_P!VH(WchT;_0Nzj4v9K+6D1d^v2U> zT_SH}OF!8yrAIKc3w%#}_xgd6GmH1ihwZ@w!1vS13TS##_7K!6+sFct$b-L`n}v8-lv5zjDedsU;5pP zpCh>Gtf+nr!s`W0cN3hD>4Z8cdF3(rl&&lRC`c9qBI#@&ybF#d%y_5A62e%fQkx&E zH7OpMEi?oGKy`o8w|oPFitCTUcJP+Wrxg0llV5rAQCT1?Q{=>;-(FEp(El|te7KeY zC9Q}3Vf=H_QfxY6JY#v{n|;CM2F88fF3kN6 zd@Zf_P&SJD8>46(IX*bJNcM<9Y=Hk(_G?Oy*&fXz6VY$_FF{iM9liy8&8uio{+hC` zU`;5J7iy3`bjhdsP0k;m>L0nhWs&vtMd6!)ZBktc&w@yLL-#V>ABCU3EwFrP{o&lOaOdn zz6BD9M2U*nNwG9)Q;8`Lrh}(CB|BV2DFL9IEM^h#LzI;PQGj(?2K=JGS0tARl*G^_ z8Jw2b?4jymQO*H+Jv|h*)VWte<~EejA$yH@)q4s+0Ic~K>X8!EKDDt2_D@Z9aFzx! zmdCFUa=?B78Ngo20(l-}aM}e372mUiGe)}<{D9IV##3Art*Pu$Rl|x^{&)HR+y_^S zkTu1H&0Z7z-4oN@BYoq&ZO`GrQ_l^@0#L#cpF7CZ~`p)m}UO)fo zvm$p00PNRyO0Kv(c9=pIhHyr2Guv<52P!QL>d8Lcs|8q8?oy(oDI#ei7otBG;Ejn? zkWiyi>7gAzPh#q(dLIjNW-rKp3tmQSz*mpMvjs^_9H>D8XrO}e9;n~CnNP%vy_*#A z!!&aCb`!_0R|-oZcWUlMDT;gUPW_{x91`zwQ|}^7Rl~s&9{m0JYH;_3X>aHv2Jp1U z^Q-dFvaQTO+%j0T7S0!gH}eBgD&_<8o$_~*lZv(4#@4I>Wd*?}u zOQ<&*P|PO8e8XNA6-XBflsEA(Ev1jU7cEzcq)r_OqT4?t=+eK`^L$Cp3!6h*PalmD zUR7OCyA2QmmYTJE=F2>P#z#?lTk)f4IWg2oWudSJ9}+;}Gt6FNgz-%cEC4#6Hbg*t z=OfaS_zpyaFG@2dK5D$>>yuq8H!MBRG1;xAsh(PLVLDy%q42RBI;BWRw@$<-ilrMU z=C$Pt7AEYQ5z3NPe%8G);)h=~zL|FUb-0;HTC556s6hvIyR6?0l7VV5ph7HCbB>Hp zRBaUJY03Ikw>&~yaSZWlpPHgjb8B6-QdOk}0&Y_0$eu*ktH=UXTqfHQpltD83LWYL zG?aPCQ$hJ6e~$n6FI}3PAMHMlegoYZ+n?zjf2Qpugl8`|m*dO_oT(44oY zsSr^)9*;tjE3ZG$sK0tOyJN_oOE0M3v~uOv_6=bQ#Eet->PLY#L#CgD950jxiJ-fXiRXKdKrV zVnDa7mPJCO&yDcdRmI5d+sAtFoyEIE&}p~Z=K$^?tYJofjr(Vr2An+ClqWY5dk$#Q zrjrZ$5BrS@6)y)<=c0l4S3+{Nm2r!^jQIvq;XotL1oTN)s!~pHt(MPYl}pEXRBwBG zOjo%Rg3_u}3ZknT%rkm}=acicvUxO+=^c4I9iZ+3?_$CF(EQeLXoi+k;DNlja`cmr zxDMvQGq|UBPmg85_@E0YnquQl=8TL26-{+rd1>tHbSZLD0^Kbl7hhSM_bg+N8 z4WRnBJp=#8u6A^@UcJb&-SK`5;E}6i%n<;sXs6X70;CV#Ww)n7<8X*x0>>_0g8s+8 z^d(*f+wJqrC72(aclg~sIt>=#q!WqaN&cGClpj0h?KjQ)nPbO#pPoq!asqhE|vw6<1wS zYpAR!hKOXtunrUu%dGSFhKo7 zdr{T&|0QA&0!3*-4W~acP?;|hvHyj>IMHa9`y)03NEp=Y-x(Qb_S$d@a#8n=JhR-O zGAsz8<0oobP*R^}B1tbZH`W6ZbYUWg@>5*o-c_!hw--fzq8Q-+5ST{=8o&e8)^;++ z{$Xrk+@EgC{KY3Uj&-25;3u>$CMy5|*!|VL=Nu8iE`e*%pY6^*K7+CUX&8U@@Y5vO z1+)!QpV#4#4FL&oEXHAg9EkSA_lDC-0f06gbUArdQFnsq`<=;t=R z0JU4h09z)~;8pFW8`){ljM|e2Z{@TkPBhYRN}F{SG~UkseevSgZ}@2M$sy=JG(X+i z-QC)veP4Te+rXYZJ(dLdf;`$|2LPl&Nm671f9T5wR`AaKH(w&f(KP8UCcRUJi2$3J zxA^?@)8YRy-DB4Q)Yrw+Qie?bd;fG_bkS*zNr@yJs0~<&aYJQE4kf9XyrOj) z_5F6Fx|-pGp!9ma2~HI#N2fZvVabUa5`hu~VJ%h@wL@Oy8p)ww0&w(xL&Ff5o11BE z8E9%5ixb2d<*og@7$26Z!{V=u;hoF@qCePHa{_JKjSBSneS2>nB!me2V3FuhfCwm3 zTFU{4zjTZh0S)OI^riR%ao<*fG7&K`AOmoouHb)}DJWHcg`b!gM02NGjB__C0k9iK zqdq!}iR~dh1O5F3+~$V;{NA4+`@v$RCzAerFm30Y0J!3-A$ucJ9&;Z>4DRVajr)dA8GE>g=Q5s(^v(yvE8i=DIfZ- zR6{`V|1aJC!c~o^xsDr%eF(+Fq&H)~T%dG_`5BrWG85laxF7rB{R{Z_*wEPgCgzMv zqXRxpTl)p{Fco5;yrOwZWKB{3K@AAQl9N;2bS z0Lg%`n?+$ZPqHY$R!U?5O$fJ=e4%I0!uRxois4rHv4)|3LQ{Z@1ZYnug-nCQUx>X- z8OX^X&-c$c4VIx1S~lk?L(kM?+}5?>qAEb zlnUa!H6F{0LhIPSkb_IZ!yjEM<|Z}E@@mOC2vjd|WU4Gt_4zL6cNO9SH|UY97-4FN z=DH_-* z4gVkdz?ZjuXxp}$7G&)H!tS5`^ryRzFclyH*Myh~I7<>6K<_V6z;}Cq3=0Enw-h+) zk~eBkbwCpjOFW3%h$%sl+ZG0(lHj#RB@w{v6=JbNRYB6&x{gnMM+mATYjjUGfuWv= z5t1y#M{6N=@hxg)W>QT8sFv_Qqr5e{>bT+AM@SW@`QGe(4+m*699E}HM=j1*J!Zd2 zQ_`7G7U*G!W*tvdcTF3AO6Pl1soX7Hc9r!K!3JGI%O6k|EBl9pDkl(L!0Ve@6D^c zJ@Efpb&N9^-;y%}O(!|_@$l4<;wGRvOlOc6u`H@|%l7$v{UYbT>5cL;bl^l+)5@&_ zO-)loW%NAN(@1EN(s1DWZ`(7DEwJb0Nd&-ng1YVGhfw@a)|P-*1^4-(L=%!8^!eRL z!Jocxlz8OzbZFMdI&!|8`0>OWA zo>vW!8m^k*t}O?2`4WcKoI>1(*`j~(4w>lodiq+A^FmjOZ}MTSD!nH`0jmH{C-KxO z8TXA80R0PtH4WRFJtpUNU68sBZxg&A9q_7r5lG*WwTb$AIBbeb28Tdox*%+`|XQ0d%9msuRJ2o#3qn_Wp z{1*9JNgR*|Hcom9&}8u1IvwEv$Z;<*5GtpoKpHf6Y1Xw2{95zC5A;G9xIiM1o*eM~ z$#RHj8opno_zwR+M;nIbW)H9(^8m$xnFA+Uo7khUb&9{}p3wcJ{>Th4pgRJv>ZE-^ zy?c6xl^=zhSOH7|s{*@a7qEGF=jV?c`Rb7)N1omL+|A3sW3w3B=DjL#OfpcZ_Sz~5 zBm*FbEkCIpLXbU_ALA>vikZMkO}XvJOa*2p82l+k{)kynV6=}2BGhxSlYI#Ao?F&J~xM2xqVl2aMX+rC%%zfNd+3l zs4uT;dm8J_sPd$mZrPtH-ok3iWOukneLoN#iF_alSoTAvJ*+t-*$YH-sGvn(Y5w1V{ir}#XLB<%+hPAq7t~)twhC?0c?uo>;~yX2 z)7JJAT1kFQ0cv_Z0LU5oOvFO~D&B!@f;>W>|NQ5_`qd++PCt0?Jsd*|*F6thQX)J| z0p9lr1yb}~X!@Z6vF9q3AF;gV8ugWtnR``P^GWNNHwHQcmPwgc= zVLTB6kZ{9K03MPZOHs*zE`j?a@HzsHR7N(H7+f^6@B$iPj-p;}fA@!1uVpQ$tc)6x zh8&qwK{4$_KzAn0evO_Uc?%SN07ThxUXkh&pKs)KG940v%-9yR?7VDQ!!nrwox64&co;tr`<1(x z1fD|uvDSkjFeZ$23=DLPj9pdOUVnf8$e6aqnD5>cVCtg-@usjM^aRlnN+WUg_#(JH z`JxpexrB}(0aAh3d5i_Xn-{QgE5$!klg5CbZ3AII6wVNX@99GiLjAZ~=|oS)ij)Xj zmVMPDamWsPf70Cp_mk%!?d5o|T1ZygoKF!z0(1}7TiwZmp@o^1(Iu20%^nE; z3AD4ppU<6K7=XZZ)#s3&@H8b@7R|pdR{jNeqs& zH`9Ek%>}N|oC(Olmg!9I$rwNY$jT6QvbQQqaOlKqsd&*607o-BGT904Q!ziB{HX+4 zMlyJlD~A(-EIFPva~FlEuoS0pU+*tk>t{?!+OJu2^k~Dnhu=(|v)KbD9Nr z1~E0ctz|nTbkBB0-ZkT_8Wp7scf4asMb-)6l^G)S5NS0bt!aj1gnZT@dot`o13l{Esx%6%&QDMIeGg`lo?#r^~M&x>JS$w)Q)f3X_& z`KRUIcJ8rt%a_kRL?*Pyn;Ro(ZEeA~M0=WO=DOKWk-EFzz-}Flr;WAX2fT(sT64@? z3bKPFX&&^LsLTLRGk$$pjrjbOC+MdHQA#FhBYLY+JhU#xd0ue;UEgK&=8{qR8XlVf3C3_P%_)XY6Xv$DgrxRI3Bt z7aUe`P@ak=Ws*O8y-&FB)P-{gNg|{ZaK6VW$v|o0Kk{ksnUN4|M(?oz7|_~2&PowF zv(yU|CV>+(>(HVF1>6*>2{Tc5#F7V)Ws*d$;;acMWQ2b^gXk7h>C zS@qFlsZIi!yV88*PDt`6{BO+M`q0}sC~gyLn~&pLtfFU%5u1i*z{F4LX4Iy|B4r@` zowk+rblMAn+^Zhf|3iKK7^-9BPjD4vcST(tc$d1DwW<7t)4A`Wh=|N&X)^{_^@5{3 z9sWk2nNI`I_8UkHHc{pm@#v-;|5*yIgWTq^2mb)trJG0u3JhhK37&$f_@U%!ZXhO< zCcGvPUQ{$gKQ%lDc%)j+ux~%m=&sSu8S)ZwnDJCko>%)6jnw~ph8+_G<4qw_no>ra;~F)f|EhcpTB+kw)PEw8hCSz7In`Ao>Y)DKmY;| z*Mi_9n?n6bnCp>|2W0x4+}gt(?mZbRyAA@a0ft-6H=B_HrJ<9OUvwqk&cy9N1N~;N zYr%cqJ~e|r--G`KN`H!gv>z19hUI9JovS$`l%d<(0Hs9Oy<vWoanUlx~82%N|L*quhOK?) zI56K&hk&tcJJ~M(V0hTxT=JZKRTPw#0SkciJSGA?Scgh4{e>bppXLDc249p4udcXT zpr`zR0r5Rdr3|=PF(2ylLjULgX=rHq(DLO>0YgK}s5-mbdvE}a4?})rU}Hx|54Am_ z0NvlL=O=($G~0QZjcvIuU@-vXk^{pdG6fVuefPG*MTwGD+$ZmQk_5n{pjzRfj*8yQ zRG_sj<>3J|L~aX$>;)>po!s?$mN)epbcL&@xHHj-fH%%$mTFz5jo^1m102_8WbqvS zXJn7=5K&EE0vgg zo#>{OU~LM^p`SA0z9I$gOI3)Hmb#u9@4G6Da2DF>D-RYXolEy~92))+MT$QCk*9GE z9iw?RHwT{)KA}OJ8q~IZiP%cSK&1&F&UJ$FTxqyBGU!LhU!Jv_gMhozP&sKsa!Fz zD? z;vZ4Y-XWiD*auAf7^U9oopu!V?!P-1DBR}lc#6%Wh~QLpe0KoBI#P&gH3 z?_gw-sin_Md5TN6urL3D1e*5|me&@i2#{yR^u!363{wJ7uf%4@N2Y-)^UU_+76l!-8W8SM>gI_UlSSvvNI_Tx*6_+XgHj;Frbf4u+P z)eBe8AqngryFPz=5;4#o;K#W)YtV5l0JI$Pn9yjMhZ+S z6iRWw^F)_dzv?$_+EkzV3P0)Xsbcw4F?U;tc8S_d!k54WBmA1Cp+IHlDbE!%Aa1x$ z-W#d$CyG6sqj8ZDtLN6^@l%3&Uudx^kxC2i_&WY-d{EEC3U9)^_gmN`zo;+MCq(5s z{YA`db)O_p_@}@9{qu~9ANlsTIil;qE0;n^R5^~XPccJB)>+&p)DaAETHr9;ol z1!2=SCxj@MihgZL6z zgOmX=KJ8s#If#ZjeX(?c06T$`Nsv$FxY9u`fa|x3k~`1a5z|f-q+n5NKjvVSK`YqL z&>ZBgbp5cO)AvJfS_%}x4`X+G1W2oTGVeHmPHlOJFok1A@E<_M$WJ3!@3H_1xMcLD^ssasmzDpAUhX9K z4d381fZ)w*`=TkV_c>apjr<@+xzq4BbQw_V$>H&w%Tw)F8h`UbuXXEB9bW_!GSR z8SM|KQXuM>kDxlQXiE37I^?$aexH}1okTNa`ktL3k67!<7H#}qA*_{#;1QoHqx7Yl zr#7B`{NjZd#9%8kssH=6B)@H(vcKV}lsi+uGvg`zlQMgqmqO)8ILzVEC#I-5b&7`e`4s z7uiI;I0ih{t2m>yK?s46T6%_l;t)+$h$#j-0!L)+`HB>1r1jCI(OUPb|#O zVsXtL*wsZe+pezBx%(GQev1LPD1_tF%U|>4uR@p<5vh41M5LRnowbT_Czy9WTzz~?@}NzBvB;O90u{o;50|) z2dw`XsrQFtMzssZXoKvttOxL}PNOIXXhv5&=SMV)`hk* zr;`dfH=B#vyo8$Hy&zTpxZiBl`x!n7%2t+OTwG@ew7SdSAE2>R12Qcsgjgr2NrzyO zYqaA(xT#m3^Jv8-zi5dKku|r2*AgLdnxl^VW}2W&ANOT0j0M!rKZARmjszZm?3Ut z=_ykpf3K7vBaGlecGcSI+?t7pJZB;^b84ph!_V0nbc^$A~6>Ar3oKGw+3?-Qh z4sy>9?$1Fx+93#K4Y%^mm{|!wYymRN^|BP}W`2QH8mBGM7O*l8ly5Tf4>5W5m(m?| zS*-L0+{snnBzdJosSOZnxAU%$f0hLXASHFbf9V{jlJ*Z?d1?QtR}LPWeEt#}%;zU> z&(F`_y0p;Ux36ztc3yEh-K}Vz(~F}mtxfGK9$vS8%NrhfWXJZ^tGi~H1=96%<8S=v zb+n-&B7Pp8>6GKAsZ~L5R(S>nR<3N9D98mwqV|C%@}Uf{24KG^q&29L{Dyd*j!%H! zr_%L7EDT!~QY$)Xsfe3uj*oS84-!>+d~i`%xUG$o9JU=Ipu%cT?p;ZuM6}v9)Ib$6TDNaEK&&&E{E75(F zBS!z(dCvT2`kf1xvV6*2G6G0^!_eQ{zIE+d?0_ocf{9%m`k~?!ewD2K4zn*o(GDLr zbWx6JPr-`mSa66_2_vj+8Kl6Cq;_Yj<7AF=LPVe1+;lh@rF0Q;iDgYp*$nG2dgq zQ$my{Vs5YqWM>lJLJ%&Y@jk5ybWbdx23_;E;agf93+`L>vB&;XhBbfl)$u=|ngv{! zc7YGB%jVK+pK_|_Sws$V!XKA;AaT(tFq8zB5_ISEjb}DnD!-0z)T#4skU4U?3yW#x zimugL(OOzscXf`=^EV-!ymPc^Wi!eIv0YTTKvmLhW~w$LwD01N3{~3X5BOdN{%1Ik z+3mKKn1WEe~yZXvf1Rc1_P-J6g@uZZ+VofdAOa2WB6ed-$=( z-ucdVuAdvieAqt2Mwl+qe}Zf%tcbMyUEWk_*YdMA;aZq1xN*DQlCd@ zv3K5@l)Ss(JjPV8fF&>~Xd3WDU>N}9LIJX4#d@#$3f5O*pO{Ha;fiY& zsq*;XdoJU-f5Uq{;<%!J|5IVqDL45QDkWSz!83}!28aK0k6%FPDVP#I;d5?)l1EPB zNFL(fl&Bw0yj|J4Yle>bQ52!h)|O0y3>v|P6+- zZK+@qh-;(ppN=G_DK4%0@y`IW(EQ?FMQSn5&xX$*NMoZT>6|K6Xfo!l5HgT`@q`-g zXS9E9o+6wkab*vrz-;0dvP7sm?cg*G)In9(*?Iit%~SjLKKJai&mBB{=k9|$mk(ZE z#N~PD;>AO^hURCwMjL*yrn94a*Sp_-;>30qU6=!wuYYL!mbZWSk%u06`3{jDfy5T;+Up>bh0^KJ@M=^E>JCxfHU<`~6_o(4TVMr@SPjUEI3aswr z@XwW@;h%wBmwtn4vCMOa+VUc`8Q8&Xz34!L(-VVWVtQc#^=^{Y0A>OoCt!smlU{}9 zQ;{7L*dh_!{daJh7FC{KsveBna$sZV_pqMl3?8CYrVS{sy~v8P_Lj2*0-`@la{#vp zJz~j?`ALQ`0&orgr*Pl8&;jOT?B9+lQb4z_#L2e0X{8)M1VJb!rb@X}s10(orm_RH zppu24boDOx19Ec~WT3z4n5<0>n{B3>M&ydfE_^ddFYKZr-2NnD3B=~Ho)?^>Ejm$Cl0>6_Ycp#@WM+kys(#5fXn?8 zLqq&a&oSmO>Ky&sKh_<1^Op60-?8NlJG}0=ef#zuZ+PVGZ+Pg#pZVw`?_9TTJrP-m zK&<^PA6nJ``IVf3K?>sIxfqlJ{@}{s;FT+VIKKJ`(|=sn%4Y0o%=Z19vFezxl3@q1tOM2)^@^_)G3LcF8ySnEcp<#w$!=XYmE> zJfPs}62B}d#3ml0E(jHoRN?hjh$SLe7L8okpNvdREb>vEaekQq!> zrUliheKD+O>%A#|S{2Jx-ncWdSmgz&ASVCUwP@}SMFR{ zymRF^K0^j{hV*6laNfD@AHVq9FaGiOhqr9mvE#AF4$O9~*uMVn>vn8;WXp~>Zvl@j zU-#IF6`ivUH!fncH7s9_)XWg<+&yrdHK>Wj>1iCOlk?LP#AYs{Zy^ZBM>G%g^+*Fs zzYmBpyyQSnf*a8+?OLPUC`-YfL)_^74_W8`TJ0FeaS655o+h3=vN)NOZ!vAeBn#1e z`{B`yAJ$pKA*>XI3{Tr+JsgD7ikV2_;fG1$hpDx;M=5Hav1!F$u-ftmc)j1(=f18T z-aF^Mulu_1bGC=~=ej=MuFrKZP0uK_NfE*wy1jmiIe=Pn>p+T-qEGCr3J?H|#Riff za;B{3=(C!`$@^WZ11dW}z+E;HwHNI$ z1AMym+r}0m;4H$IuRHs?mw!`>brfs-Cq6oEKM?x=kUX9P=N!7&A>l9qN@j5m>Sj5J zp*9-@B|j+4(vA#fUVnY*r(Zw(>U9EG^{!hU zVDb7beP_Ku7`KKa^Q_+8{r&w~f8XBTUhk;~9=PDV;o4rUJ`DePUBu+jcivqu-}3k= z(iHz>{3k;Ny=UtbT0fg!FcJ%tbUuM?{n7WdKDItS-aPKu7X|?AOz97f@P-KRWB9t* zy9IaZl}R6{awr6?sn?z|d^^SE9^pOr+KH(dgc1p8Q&JS&LKeZ-PlX~RR{jxMkld7x zby1zrjvr|}ob9_2-9hZ&hd{RnE>H zv=9eb>Cbx;WaeS%|7%;n;dkyKmZ<8+ihf*} zK&-!q+V6`0eoIdSvVggzls9#IjdfA|TW9X?@9ouU`+K#1 zhW6q9zTy7bnWXg4yU(q9y{|@PpZaiDy^qVz2n4*MG+|0Zs|> zSr4B0m|O%(oPR$0kxG5yjTx0Xw89)f6D8=xNtpq2whL(7lcQY8?|C5QKURYkr6}Ds z-xJiZ2)W-0ag6!L6vNOjJMk*(1inEg9+6ITjEd`2UMIjUuz@(U6jWsSyz3+;TU6-} z0J9ip4ah2uFJcSpOVgP?Bi-c?rm-RE;H__f{W&q>8Ld-hwdISAHiazOjTe~+w%N*j z@#x?=a-8AzJax-;M^C-ZK59w_UBk6-bbK7oGP$2u9_cyfx}#?wasr$Odb7!b5R%~G zG^5zBInTf+HH@6raA2`~<=@o74|5RwSl;u|UlB01i;52X8#qSOlr`*!$_BX0MR3V6 zhwDuCDpK6>iz=lHmZv*BS)vuZDS}9sudly8%+TK7SF17ZOD;I~57+xoe}P${*4KxW?9*|s>0EQJv7*^%?&A~hy?OlDvF6VDvefEj)Q@#>&}l+!U#x}n9C`oY z`dOLBS|?m_JtAhNI^edIe2?-^{PRo_06$)=2$!OGO9>Hx)?429&TBSZw572w2^e1) zM+goby6T+qmlw6kKj{zT|KLCSP3q7&hqR`tDg~Po8aTzFjbtM*As?=3%VnMxbhso@_ zB}T@fkU#8eeIootr~Ur&sk2Uh;jI3?)A#p#d-q$d)!qHwUEclhe!n(cuhG9p7XrY2 zj(Z&UxyeiD-K%2@93H-giJ)FTe9J9eU5}so4QJ8DIPo*GaHa>FEcJENckVuMnwD{& z={5L8B02o(gDz>=yDN6ejg*mdJttlslAK=s~32eS<}e zGwK-D0NCFAd#bsCzM-n>l$J*E&<)E;RpH`;BfXh6rc-^Qm$QFf%>Q`+8z?zO5< z4o;PpjjBKF~JkpA+EtiMEkO)}jTe z<hUoa1=V59L#8=L1KdD{*uKBlxm&&ZMeF z|LJGJdNCaVSY4&PCq-x%wIA|xy}IAmzqcwwfIIu@_5%_Utl>+p>96%4?z-TbIwElR zafQ36esvrt|0LmFv>cQhH_&!sSeNZ zBW@fLP=Jg5h`Qq1zi+2{x7g z2#L_`WF+XGKI1iHe^8o{3e?>D;|aDdQx=?-=-bs3l}4Qy^v8=&H%8`{*zl=sG?vo_ z$e%9b0K%x+=oxYi1aEuAD1XJ-7`;a>9&oYmGvIwjo&n%^A*U0DX%$m5qPMvoVmzGO zEfHlc%T{8f&#cQy4@ppxIM9*Zl@b(xuwrGY&J)P`nN=?Tnv}9yt(z=PoC5u#9H%I0 zNg;f8KbhSARP-mmf3z(JVJ)9l@9jn1uYHTs1G{^7;s5&+vn`3k9*Y0&YOA#`@YKE` zb*LBS_w`u@q#&JHs}bm`AMQGQSTfM{^5EbhGT#Q9(>?M8!FucfR3Wq=+9>*y904s6 zU%8*)6~%&C0c_JY5M000hs=AxV_8NtVMG)1+e`?9K*be#1MweN04#2jy@wFW(c{w? zD2wEYuE96v0CJ_QL8%o! z4v>O7*w0Lp45H<-blR3+tbrBYc&agi3uyi&auIh12b(=jT%0qDN)y}|B=e25hs6!j zpGkf@qKw+^%a8-bzc?rl^z5^HFa*qcK_rS|=oPnxTz=#~S^i$f3lty7&zXubJ7k!o zz^pgbW2i5KyamBXCTxg@P^eE&M*g$nPogEnU|-cYtaN@0A?pYtOb~x>QgKZDXwgil zbg&SC=m5zJq+uVxeCs}~)zwGem|)1)Xiwkj?CL72K0q?)RkwwskIz^ws`Ueg|E}S# zYwE-2-E~X7ul~RThl%*&vl#CgJqb|wfzzD<)gKsyAzyb=hjIp-i$G-hL+k*BTmwA*%L| zf%O{LRUasF*TSH+es6A5dv4^Vk@hyJnv1wDrNOVPY;LnIe_;%N4^I*9Wjm&^kr8(F z!_fHSk2`++apAisM#h+QQ1RO`#M|@pOB>BI&d_!#>}dY1s$4y*J#Ha7Kji%kO&=Lt zJc=Z6p_#xH#Zj0M1wnQ}u3qk1SE7Gp_Y_}{Pl?*Tw(`TF@V&F}VczO4TM0!AF)@pt zT@XnLC8Cv_MruStbMW8}Pm5ACvxF@!u`Km^d^q$S#Kad(0yB+VF6H|1gTsFmBYN_^7q` zSB@RP5?!}2M$JIBfSsc>IiBWv*;CsH;NxLDf}MyY48GhflMesLToNjl8lm= zrQc^cz#`!fJ2~xaJl5!(q=_=*xd1d&N`hEaqupr3%$MY>sq_^Wmri?+c@L%^-+2eN z@9eCQ}hK8L3<3H1|7;fl>ywVN`t1uMH*d*3oGMA@M)V2RW-)Yy^`oXg=V&R zXn-=RB%urpq}`t7Q<%p8!-clL@=*=f%q+D8E1f290uye-#4#YN^IUrv%i73n8G9@) z$~lGG!51L`}CK^)_^HEVe2w=M+JS6j31(-Ek$I@StpT(;ky@e=*z=y`&3=rkP7eqDA(g3Wzv_kjprB@HU)!&QuwNnk{*H$Ghw zLgN&gNz6L_jEj1PU9NvnRB_bj9op4*n}I!y(>bVhzj?0VNr`a7LEV{V6RVW zwl%@vKig`d^UT@})S}IM;-;J4Xjv>+>ZLO+O$xS*dnG))M1J1apau2U5Q4t?Eo4SK zut6Hc`1J7;4F2r&m0`x0@+bK!ZW6kE_}+W&?TGW1=$D&!`sJ>Udpuns`GG3i7qAr5 zGf#j`mjOL0Mqx`FolJYRqON_cEel4E^fYH2C}_%+J2XAjBxjZ~b0c_)+G4BSmWmfh z`vGn4H5@w71VM-k17ZyI2XCktkH%!mBQMwx-l_u!5Qgsvb^&O#^&PSUsMzur#>2AZ zZw-_Dx5b_Tc&>`c=U#~wz{`7~8z~V^36VpQVVw8&g%I7AhNv!>O1*Wi@2W<7`^!0Z zLX0d-4wNkDxQJ(Rh8c&<{eqIcQ2eK1GY2B4?cDgn^i1sBEq1%>-1KM(pvAA$N%G;p zBd8wwesp9LnODYX@3zcoA17vK5rE_dnth*6Wa5pRZh9ZVnC0EF6#!cA^7@2N*?_2F zEMrF36gH3?^qt<{H8?PE^5gLM5Q8-hbtr&(s6g~(SvU~Lh5m`&erJ^^<4&tUI#@z@ zTPyH>lpQ#lFK$lAAL*XjJjE}X z%maqSAm9f5kiHX1L4fIbd3@QnG3ajn!whs!Sxm{BV|ZmszEAKr%Yn(C%}&}Kejwk< zjz}8Z4@Xj!Ol0=vNe7(ka&Niv;Te`XS3F5Vez^rBVsw#2q5F~boVc!Is_0Q#RKW{fesvjO9`w>A(3FK1vTAaIS1yiBr! zi)cwf!QHd8y|Ibj-$pRn*4y7At~-VRG_Vi=oAfDJX|60Se`>jDH#FWi<|$@MXaX68_tNiK>Bt{>J8Z`QM&mg*jq^JACq4kpCb1y>rOs) zBJ>}rLfJ~OQ37(NYBdAd#?QK1-VLmB73FpDB}fLc!jr|$CJ8MGKnwLpHLYfo5j-)` zq5;__CMM8xTJLkVF64vw53K*N0)`{ky=TcMI!sc~XO~<JUi6J3iKCupja6K0y z#?-@{;Bt&Kv3w~S(WHi4lfo%0%~625Gp(p~w+7*!@s$lC+3J~YHZgA3+NeIL?c^?O z$#Ju#RhDgla|eHscV7|^5_F)E`R%`F{@!GWUl8Zz;~$|u9l*NYVxj3&lCNWw%Nua0 zTx+GfhpgU<;6upjp&^(YyZ!5+t8VQnB&|rPaNTSz#HIFELA$&4*4rl`zVCN?l2GY1 z{6R((w5n_XkT|VX1Lr3r)NtVt$6mulRGGIWs%qqOaru<2po}O@n1YL6uyB?8p!1!-m>pf1BE-vkc z6^bWF2Re?*VHHWNWXN_3k6)qB``9tk`v7RLKp=Nm{c)Ofsix&60g&D6j3s_Zn%+{e z?o`Bqci6xuXBLTk0nE!4)E$=pH(A7nj-v>{_@YB(7^BJ_-QK_m#c2elj`7e8CFdG) z7s&z!&lq14BzQ{!G;|j z44tLPDB{Jy-aXKB^e9VyL$iVV0H2Sc4HS9U;F8T2QS$&|!1vH+g(>QNn)*>Hx70_; zDV5G5^A>W1*1)-H!Wg59q=G_fqg-}0LdgiId9%3Kp$sKQsZHaE9CI$lwV>|_Wvh};9rNNCZdisl&VLL>J;-TKqYE7W|aK#Pm z09n8oz6uzB`-!8)$iVL2nYcsGk~@gZxjnLi>bDGi1Dzqhc5J!!+6P&ees6;OIuBoa z?Q^e^fx}pb7R~8|t3ik18vGW*nFG`=^>KM8&LaZ86N}@4I37VYO)ioKj9kLeydUCk zg7b5)PUZ?+sQ;2au2IbW7_3fqYrG@sw!om`c(*-#}wX&tX%{Z?-a9a*dM*U5sh&VLs z(y(*+wSxmvaE4Bj>Upvo;#=pz(iJ?&a{xNp1q!iw!*cH-D#=L$em70#0fU|9(oNp7 zfb}^eb~VlE2BFx5JVXFBQ7?w}D-%gWQaBjJ-&qoEbP~#%fFcJTwmTi9Mzlrq(0(|% ztK5rghd8*e#3%8e;wCwFI@u7kOUkK`rLZzUIAol*iUK4&3fDFA`)76n;_q|1X~Hfb z3jk9C*zbK|HFTeEtp)WuAm-380Wp@zU!Gi;kC6ZT7*(3ty9?fX=r?z1Jt!(m7`}N` zI|GN^Tbwr?Hf_!b6@2>@ehMP$D~S7s#GteT-g@JqxF^X_f|6np7$ow*Qmc{7(dor4 z5-(3l6!3<=erge&XU*9-C~VxRL43v1bztqS7$%31fg?v^tfyZJ zKL>A@6^1ScA0au^_EvHh1+F|D2s}TjJBWwiDZz0mNND`sl=KbB}T3V`2n=Me$_`WG;3BY$p^{ zi(;aDI^e|H$S%)=Wft9?ZdX#EwinFENU?@QR5((TBn6k_{=xIOg04eYY|A*s7>z)v zhS&HEX7Y>@r@=g(+uJBcczk!(8_x*Zh6WeV*x1s}-V$NZhVIKhaY+My<@5=_ayoM zJKI~^GsoWh==<+KdUI+@yWYL_>T9pPI+v^iAr-c@d*84G_!eO?-rURJ*?WWZ89ZG) ztwjc4{(+gmvUSfS-p_m3lZ@va8YUhr0z_c9}r$ z45L4!NBdFTj3W{S$g6_Rm&f)-hQ{=OtXfez^!qa0>p9MVAP1nSqctfD4Uh>v$&>P` zdP2x$XQKw(|04iZK`EM@cMyk)*TK08)kR23i3_9$Pdmnwo=ugJYiPymQQjC}y_(`f! zFxD%!6a@}lC<3`2;g!y8M{3;4*`6FcNAU8MURe+l?XwD`wjqVzIY1|gd=UvS`Ry)+ zJ99(@Mthp0_+bbJH1U;E%?^;P^ZM6MJ%4qoq(y=z1b&LWW>hn-+EDT@^ zBmfaXUm9ZN)&SLr@E2{br}WZBgJ9R@-zz(q8+ewO1XSSg$oL|;{#xLOA0zJwDhUu_ zVZ8Wl#JL+%%ib0DW%C*|n9g#nFJsaZ0sp>Rr6p;%MShFJ2VOjEgs_DWAPl80Rd5G% z)UV1-dgC3chB=p!v-Ha<$w2hwy#8#)`Xku}HfYCw#IaD=&&8*!qzctY#Dv#dh2PSM zAEhFI&$OxwsX!XvSMC24si8ZjfFc09Et$js`Uf5jxtLWQXlX`rhkmkqmhk&6M<{m7 zN|SHqUZWy8c<()t{>?Yl{OAN~5T(kQ{AnpZU|Zl}e5J7xvMlW4a4+Xl=8d0(q6MX@ zaJ)+LTfsqjMSLdDn;#0G^3BAU3-_>734f`b|tN`}xF zkff6cfi%)i2NywuW8)a(W`e6B4ibqnT3|c z?!LEf-P;`>->EvUIz+fN`w1^9Jun%JM7YaZi&Y*Qwh5wtQhFe?M|&Mi?85dMVgj*q)g4t$!pa;dB|uFs z52qSN!=GQ#x(xN@PzF|YjL{Wz6S5G*!ys5bg7%`e-aqN{>CBYFg{3o3pHBSp>%ykP7>L(3g%s^S-PB zHov~$n)9Ez`r5<86Vvu$Q-zk(#`dYb?X6vuolORQI1kU!ilDxUKj#ttCIsO2@Nw2E zJQ$@YmQZ;^2MWeL;DA7Y0D&`~&GR~f?;y)Y$bn99-YH6)3*Z#46tiDf_dbHQo#?8t znC#%+1hS~L-v8*2ud@=E&rTjaS?^&DpRYew&5Y@a-zpb2&G0Yrggxr8PLJ z*dF<#EOzmf1WnQJpS}t`eNu61#3*SvdFSYr*iIcwINHhInc2<_PKoJB>laokOPF5e zhmt|9t%5ko<4N0;7w)b$k{AP{cZ@QU!l4N429xXSyMnegkAKlNO#eW!Ki?4jvWyI* zb#e&TJuUPhPDU|M)bE&ewT-1h-j|GwQxwmWnqV2HZ+wJj?l#qH{#YZtK=40BcjfvA zP(S&DxJDw91gPe-?>h$~fF&&>m^ zMYmof$IlTkmC0TYX1NW=bNK54pRx#mcr>a%b5h)Zz*m936Z!v=+*`$O7a4IQAvN25^N0cPC)DiJlr0c9$9+Ur{ z9!Vu_8a`GQL$yKh{rV)>de&dQ0Ruy822?#7%5b#vXN32F{hL;j85-CJJett?-*hhs zZK)ddbqwU6s3&B)>B1@HL4G7Hi5V=t?t?jB0tsIBEUxQ6z1t2G#xB4t60($gC%h{A zS)%ubgpam_ogU%<0a8DohzS4zaAJhzz{(G8MC5f+qQU#`O9F1D65<~hdgI^g?x9e$ z7C>t0`8wEOLl$0}dyM@q#xRv=jv&+*^;)kVTnGlt`w|YTU~vg=e^Gf{@T6q@8`g2M z7pR_U6jwE*+_(%zRM^H@4W$l|CgF6U4XlE7qQTnNRgv1}a1$`&x(F@qN@?&KfiIo7 zR0igy=~h(coSYmcRk|_BErq0R7(zUnOZb`qS$6OI{`)V#Xo)5%bz}qWudOYe^{ld{ z*+BvT&sQqyTO+a@g2)R}D|XP;*v~8G4MVI5nDJLexwt%icdHmP!qAkDDf)E0s zPImI0A4-DYtA}*1OM-<#`o|=UvlMVJ_q>|ZdJy9{>{qyo%g8;GA*r+-{P;uAIz0eT z=tsO%9j+lT+-SN?FzW?ysX~4Tt`aS0IZsP-G=pPwZ2lJ3ukRgeJZ<;3&0x(cCiVB2Sh+n>PK)6zRY7yW-mWR?E(bqC% z_*6$;lmg0gkuaJaMrG|fec{iyM1=E*xbRQ~&tq6EqY7i2`vEo4lkP5~e^$b{0O!4| zSTFPk;yG%L`A?sXVy+edYh=7O*bC%8TVM+?O7>B&M!VGhN~Hqjm2D`8W*4k&^`D!c zazRcTyi-#}??D=Z_C5*L&^0$W$@9MZPXo5Kz##i| z4%XX6f+T$K5VC-xL(d@MN}MW%mNHLvH&LaCbETl zbCmxNr1GmgcB3)-lgpBV-#=NOUOT!=qR{^Cu3`cN#@F@Zc5rfHY_lrQFGjc^>{}7P zz&BYc3@n?Ly|@5cIUmpE1p|5skp%=sQ2GVJM{dC+K3D(%BOoxa%rJDE7*F*ndcN9z z$hU*n-E&&7Kh!33Xl*Y|UnLh|WOl>AF%#^|5_ZaaRE6#bKrOR0nTx;c`> z$bZs8VCbI>oc9EIa7ul~y)WIL7a=i|-v)pzT{}Vl2Gx2nm46fa*`|;(xyt&VWdI|o zLDjEL_Az=FrT)X|i0g(5&uLJdPZ9xH!`m6|Af>z+==ZgBy&?q*5qVjM>%EfPIm+%H*V=zg;)M}+yTJmVc-pQTe6$Nh zV6@q^W;Wa$)%ZV>SIk^*__0tE!h~e#52II{AFCh*IoMV&#S;`35|u+0Du^rJEAaF6 zrwWRqR8WNpYL(?^*f+&^kG~Vi^c##@b5qabt_SMI<7(@>oVd0nZ;7oylNsQN1v_~h z0KQ8RVBRSIraul+1cO*HPw3JrkhTEO&Zidw_PU;JY@Bkz9gp=X0M_Zw)2M3o%-l3% z3MX1WOajgklQ}UTKhOi2<2m4a5YBZAMJMDz@G0v-#qsYpC+R#vc1eYsNq=_6*Z|=< z_$0pTX4sE@xJpoqkI1QpqfA@?8d5<_wra;`g}5{YJPQ4XC$M{w2_jz@&oZUGR+=z? zeM{0-Je2!OQlhNd_(fa&h8x#w-ti}^_SRP>MkmC3BP%--QjU0{>R@Pu5Y`kt>fe|c zqt@emxJ<*o6pC!{#z*imXrDEl;FFuaa?gVgawsTIn0nFjRhe4qi&sSO2zWVks}3IZ zKbw>FTn`=CV`XqEQ&!RU%h0bz;6cp*uXwjRsX+_m{=fV(Q7|Py194x85(~={cHioCe2hZkktzu>M|C)a`aCDsG4Mg4mtG65Yn<>1po-f!hgDvjEu4XAeaB) zXkhwssA9gga98*>g%8yC!Na$Iz(FwOeJHp;8I%Szx7#yw{i>{LhY#I(1!kO`@U*aE z?|qt(Z3qAkouSELO0_{_5!kkH0Z*WVAThe5;h905#*)?!V@(72uJ6#xhg*V9^Z_gG z=ur6i?BobYSoA6=_sRpV8;Wc|_)#4s2_imBKXm9uFqWueLbEjyaX${KUy%6FDM$=5?{9Q7t_DQl zysQ5@u5G?vcbdG2WR#sBM!?oqj zZF&9W_te`UA+$|e&=En&pqJ4@Z)8bFeq2PUDi?oQ2@smo1GMPPss7J0dvGv<`7l0~ z*mIJN>s7z*1?c>FEu`f^TD@UufcurpB)c>1*D+8-G$Y*mGzUD4lxVyIn0o69wPu#Q%nN$_(NyBaa~#}X7d=N3W*xWV@+gb0qEqEXZRmcFQ>{m zwKF3Iej|2Vedu8XBDjyY2lkgKpi=2U@~NmjkZohwjgI17tszlD{rz&UBL&2>oKx0p zOf+ZN1LT(3M{xu5rWy*nel(DZ0SC=6sT3&x^m=08GcLM7K^5lpE*SvIk0Ause)vEinY^^q96-zMP>N_t5)M7c z>(HS$4v8zsMfTRDEH}>CinT_QG#{QWVp944gV=^o(Qm{f_0A$^X~knP6v8^M97-U5 zHtRS(fkXQ6elVS*cVN;w?=Lf7sSW5h8t)gv9r}urgJ`>tEC^+O^0Dtz8$h!EJG%tj z`UP%sj9;<|FnWaua7F1_E@$RCoi1t+3j@;9fH?$#{*VCH42LF9AC1`i)M=X~(I;oY z1u6;1&K?-J~@5 zl6MsQb3zo1be-^$eqquw0SFTe`HhRcKNJt^YxIY(IIIS_vjYg|$4AU?BQ@w1rPDOv zf+@m9Mj7=3=DQ%)8}jjR0O9Znu}~B9(Z&Fx0^evD>Wx7(;KqR!Fg^}wLWQQ8dl^L$ z3hoMZL%DL7Dil@^N?6ya$v`o?=_;O1z42K$uom_5ZD>(~g8+84%#Uk*(Fl z{yYf;FS#tp!*`Ja^%Q0w6bFykMKFp%;bXyQ?$)VZ>9G3Bs3?DjAtX@CEHd*;J-hiGuOec4QF`9yzpadE5P#u%9EA~CFqA!Non`R!>OgQt!i(bC0S zvPwL6BYQh-Dx)(|Re~c=ASwZggpt|qi&<`)~M;9-fRz2c%Or!!`b=1TT@?NLGOD zTfqe!qH2zAaZajG#eQ26m7g?YJnp$4=8wc(xj_S5@CE}OnLO0jiEGN``?U;de=$!8 zLC_F|j6sbkjsr+t6}9uclYg(U-w)ViFIF#-kh3!Qln6yOg5f;x;|}`*Cjl|?tBV_f z*Nyj+`p*tXYc(du?O zGc$9o&7E(*+TC7mb)kQ|jX}`v^tVnOnLD}OUp#hljbA+Zu2mbj@2rGarzO7Uf?9#p zm6Us)PP^^JdXQt{Hu?RiJvkSikKj9H05`iKzTm-MUQt?-g(@$UCp49Wl>qU@dN<7f z7`oGpA*hLa7 z=cGooyt;h|T0jkTm9kVFTXs!^YssZuD;Ye*XCyTO#9Pb ztF^ei*>7=TW6IW4r@Qyn*?lYsef`1Y;UUZMKpf2XmGBRJfppzKHk^6}GVIOl%6UW9Lt;0i z=Fdj9784_TV1g$1lnfJ_{rV7f zLM$OsT?C%j%Wej@5rf(m6aLiGlhgB+DXb^LtTk%>(C90iE=@DPM($!u6t>1WAq1(I zwsnN}Kj3d2c#NL&Fl_*@=BW%G;I)~0d!KTqz%R)24E zf9K8R6N{~vR^R+`r`P4xYoF_^wz}O;ulEh&aIUx4pK70Z@1+x4RL9xh{c8J6yZhYc z{{HUr&bRwp^NZ^wgPmpRz{ckK>?do_Z>UriFSriBk5~5JqF~DltZq;=mIs>8mpwMt=HkE7qMYB2kqxNMN8m>raew&8Hgq>_k0Mu)e(j&S` z4jd%rUiRPXR3cRpqT4JtCOV9ERX3C#At_FrL6#G~Ko^P49DU3Dw;Al5AkO@WaZMn| z1O42##apj)p4hG|Xx9Sb5I6EBDgykCtT~#r{g4moxyb$uQP6dcMg%$9DkB`VPwkhD;NLbWmbIr{^G`y2%E|HAXxqJh-iC8~Pt zE^qhIV>+E)tB1zZ>$bbA-|hU+YIjz9-+!+upof0*67#_6oztf~Gar6QJY@2Y-ckB^%0W8y@z$78ZrwFxdq1a+>0}f~1-!btn>bN({k_ zuXOkb4C{xKsxuMVO!l`%F`x7XFk4W$zg*-nxjBX8MY4+@f9seG*T9f@&p9p)o(i4l z?#k0c3XRj9g*ZljWt3LS!VwRd`-2-2MPEYHyR9~I@iA1M%=V0$i}PSGASXu~aRc~R zn<);80H-(>SQvEpA0PmK`DHK9-_V{+ABh3uzTQ`p@sIvPgp-k#4U9u3Og4(#U#KuNPa)>K94y4OJOuJf89*&2d^A!9pQdD3pUakFOAnq$MSy<&90vH$f@JHW;P;HKvhG(gijkBBfIY^h_XNH9nh+P?PGQ%}kMpgNS8 zppI_OrmhO8oJm0rtCx(fUgfpDV~dawQn!+~z0un7097x>4BH|+=8H~iTfK2O9Dn(q zA-Xx#wnsS%Nqk$a2<{;T4fv(lK<=4ZB8oyRI)FAX6p_+LtrigK9UVYy2T+%s zDIK5@M>&|a$HGgX^Phe!Vgr}9ZG^R;L`*MH@gn5QmNh467KB?@fX`9<8*yilj_tzo z_SgX&%q!5FOc$aPUMb*@G?m|5R~rDL>bz>%T%=kZ4@Iwyhswq1_I75K<$o#?EM!9X zySZ{Xn5{)|j6>Tv1l4e~mLf`nr3&hh3nBK;?XR^Kg}O^U=eSz{$DHozlzM`1kW5O@avB# zf_lK{X##(iW3}|pmk`~^Cr-389LD%rT@4f1kLJpmYITZzAQH*tz`rNV(qD{*r?7}3 z#d3Lj3jcStdaW9os`8?*Sj>2q7Oz1PAX5qdizDQPL1KWE~ZsM-`-6QACxTc94#C$f<3~6?y6rIv zqQR`*&`Wrag0Dh2L*yPbfVJJmflMIOrTODD2w+vYuhWXTeWTz>k*QIO-dcHX4*mCCiYD$mULlD3|#oi!;xj)N{6v@QrJMuoO}^^37XT% zV_5e{1#(Z98mQIY$PN|Y$}7F7&ev6K4pp!cR*x^VT8$Kfxd^w2C&x944V1|gbJ1`# z6!Ii-ZljrKG!)5f0_|(7+ZiyAR%B|P>|yIlc4Z;EwH;lJWU|GKdLVL}?{9 z8(W~4)^``Wz8`3wJ{1^z>H6nhehv5=k(`&-06u1=mlYn+Th^zDI}|-|n?|+UNY(|G z2f@7y|6z)MmH#VlEoilEY9+X%qxNbkw!8!k~JV(uMS|gemNzdwNiVlfAvYH}&?C#L}58K8QHJy4W2E>s>-Hk66H~2kZM< zo~EqmiY6xpi^Dn=d)@(`jl>^ycRMEAy-V0#=eRx)B46T@KbAV5LawWDX*A&X&>lHJFOQvxQ4Tf&`Db7%fP961;y#4* zBdd`}6!^2SuTE9BHrI0iJfb2(tmZXOC`t3TrSbyieg@wlclGL4p}hO)XWxCa@zF=0 zwpR8JTfro&e-10jvx?w6cxu7JV7awVDP_7;@*i&O@9-B%rRqb+1A&*We@ffv7;shV!>u5nqt@tymVY##CI*w92i@=(jB0K zS@`$p1E^x1baSG@5xlYfQ|1EtLq4F5XY0|?{c3ep#XGmRw{oDL3~>3%w^tTClcDi2 zK~I(Xn#vT)xlHl50{T*3p-!MyC}zUGxx=MexloIQyyLhBp=2iH!>WpS3L7gJ|0}c+ z8eQAL@S0nzH!7+6a((d5llMLKy12n2KwL#Th5yEl&+{^3EM`AWt&R4}kgHP^W9Fy; zlVGVV_=s!dh$6_WD}YH%t^@u78YZ{(^qjbToHCtg2!tpH$#hg^R|d8FtAF z8ml~;+IE0S(Dg_rSCLZ+1= z^&~SkyQ^N&z!vpj&ai82xTn44PuoROUXEh47s8v7-A~;0{^&@s?LKzq4QzlEkqMEL zFW&Unsk7mh!$AJ4BLWCzqODm-o zydOt3tg-E-&uucfPzYXG+{kQK!G2ZQJ<$-rudcEuL^4ICAmLZ1Kz9k3)(OB9JQ#nUN{W3zhWN+wM?$ zv9%>^Eg#vWy_5iOv3v}OfvEEqQ^I(bLQ0-Z-ymezWkXArOrzcq-_R#ZV^DCv$Ho3} ztw0W^NA-lVA_b57hhHmvs|F8(Z|0cg0X2}CFy7R7l$7s-sx-#d>GGs&!w^fdgjvv5 zTlJzI%9FX-mD`SwJPR+bQ`+gEouZ#RvCf=#Jm0T+u`%H?vQP}o>VhH!ez;{t=Pfo>$ zqBZ4G)Uv7%2v$#Q&0otN?(a}i!UvA&&|^0Z)lc2}=mdTGn~ky4x8HpmDxlq7{t+=5 zFav%S1a%n^2Ge-RTty9B!Si)yZEApl)W@_J0t0V~KpZrJ}C_7bmA&aYuKTe}CO>$}zn z9r+PabZE1xTmRS;P@yCkL1v`}QP7BGyEtjcWln!*Q)~t35U%aU3$x5+6?F|2; z6i|z5nffnj*A;nJDe{Z_Cz7em1AX|JdAd{0#js$o;^$SX24AuiFD*g6oF7f%%+}`D zmPg}h&W0XJl#Y3UKax!9IFAlAYB_`On`v*dR=%>kOYDC2{mZ}owi(&_t+2j+`SKMhJbVsXy8`UX2z$I$ZK$?~@8%O6=~mRNJ$X z7osTD0BJgsrz)6XqyUCaHJX$ej1ApHw^*JVd#O}lr8ZHxbddh5Bi(q8C~P z^suwfp&d#zN2qZu;9~PjW=e!POO?YxCKSpC;U*+!v4X6fFJVw@AmhjaJ+;2Oz*dAI zp|?rQ-)4?&cN6E&yjivr?PKs~IXVD%Q2<%&fwCQm@uZ<|v;b#7L@EM^aFPq=9qS)} zd7hxUAwbEj{@U1)C<)J0!?-4q4ct4+zfuj*$VCfxUf1Hr^cjiBv&cVlx{eX_Kj@d> zjxJbG#^Ck$tL&vZVmCj!#!*)>P>-oIk3X3Q=2;)M#atrqjpzMqinv)~J5WuUy{dZ;qUyCKUjh4)e&s2-u#m|W3q?;jn%lbk>-Oe$1foN(uvLskVLfEF)_*&j zi%y***Xt$0OX!CPQ33jc{toVPc$|u`a9D+TU`Z1|3KQ`>+{h+9wPa$+%Z4+3ri6Yj zlWVItQ)IvzUCm14RDA>^Ncra%cIqkBedYGYZZX!4E%GwXz&{cJGT_o)A7-n79cDDr z-Z;&Uz^9exUDWg7(9ocu$V*OuzF|7{W;x7mAAYR|NGow3)2*9QOOBVW< zD2sF)QvtrbYrB88#p%tXAiATD%!>1+qhvTWCyz%BA&`IarF&GFwhB(OUZ+c=^Bh+N zfhIq3#EyA2_)Vt< z#H(xzRKTkq;w!u543Njm<&ry#2EcSaS6vNh`e&8N%a=2#!B%Me=~p2`e>_#Zv{+`7 ziDD^jm#X!2^GE-~&z>H7+X25Va+E+QW8`JG63iv_D}As&uC!Es%9)|hzN-a> zrC?x=!6UjNC0!y69Ww}_9grip81GjXu1L>HM67;=q%Qv_q9V@o^u`{*+(zd9Q7cAYUvs3_*KU#P9y)T`-UL6Fes?a(0 z>J?Rn=*7Ij=!s8HXBRkk1ZsXbx#aVemhg9XmP;J`iMhW9{F|wr+4>D#*WJ)HhA$BF z?{E-G>=?_wH;on`UWE9Nxp}?Gg^dE?&h6Fhtw^|f?b`O`%~b$UK(D_@&j5Mra*q4> zwa+)piv<)R8V%zBOhqHnOm1^C^urtI#%2c2L$TnQdKH#*A+xGIXA%d5Kr^}E;e`f~ zr-Os@{!s>%RHF&ks1aBWG}eYjQq5HV4P&Q90)cx^)f+dUunkTo8yFlJJFZdfxB=W` zQ5U|a^?k)JNP}@J676TCQ@rBfYKPb9vwWAMCN^EGk<8c?y$vAj1RCRrkZ<@wDNqCj zXuT=08E{j+E+1{@Sl^au%Um-#1o0oVhpeMSz*alImiUISq#IGIxrVse1~f52+yd^; zh_kE3M15rO=h2alqa5|y{21I@aSzCoSIt0lMTdsObVA&BL@jJ0K-8qIG|?{lM{FM_ z?DM2{VeYt|zc$gGTkFqO=V0q_fOC@~V1kNkfCCcXaV&>Eqi1pO` zbS7R4G*XxsEU^K00gi94Ptf9crvc=?zwUg|y zFAP_DLmj_WIiG^`uwS{MxkH!mKp-&IQ1;LA@JREzufDn=5Ey6jWA*?K^tlq zjaUi;B8)a80&CrofqWPfvTh^FZZaJ`!7`ZFLatZz!mi_D^Jq;Fi(iB-!H8M-L5zq) zd0>dKC(+0K&Rw1B zK0hY;D_>Pcy_8^yyK3YAyat*jWNE_w$GIw>&Ae*-17`;i{3F(;VPKpCm9RCYm1EkQ zT|p+ebK~OZhzxXx6B%%qr61@c8vV+&NAAA+Zmw!hw;FVrJZ@c!{;|SK?Gv4+LQ3mq zGZsdKvzqhK=FfjooZB*9&)oR;_Oyqr& zlfKc39e*Mj;_Q{uBk`T{_4!(I?$%Ln9QMvcGr&QIi8vffe`&4WY;ep^W5Qo=1_I5- z$llyqV`qNUzjF-qdo}o9)$r%xj1O5VlrLS~+T48q*Wa&F*a()X?xox@Lo~k%;*}*t z&HFhvfRBH8L#ab#D-x-mJ2(FE4`GjDN|PT$2|_r)KGMVGa%a);%G!`A-c z1zN;j`02u)KQXu0Jb#|QEa#+5tYPw;KGhwV`Kr=LLB$UAop`o!kQzAAJcmwva$1Msw>Rlsp!Ez7}( z*fPLZ#DOM=w||0c4tlXyFg0)&byA9LTPt$ya<5$vkwg&CkhmiP%p4 z!hQoHXMaCukeqH}>I7!#X+M#mc{45o!&8cFT*v6k%2G<$RvqXS03=@FLE(Wmih3*ltH!& za_iIzyl;{CzHJKa^y#$4TT0p*7c$gW=D1if7&TwSF&3AXiy>9pc5YG{Aegp>jV$GE zvj*4Pu`Ewv+8KL@C;LTN`mLFNgcgCgzp<8#3X$RhINvl-wUD;}=h1fi7cn-Je919| ztbNp07iOD>BLC7Tk!|YY^RK7phkAy4-cGAyJoAc(6ZYm4ernGtLyu4TeDjG|GC3EU z1My2W`im@c()fIG0rNjmPjwMU8EewBaNxwqNY{0L{`uF5Kl=yA>J9${E-yzG=`*Sm zI12Y;9nX-=$!!~sMsVH=*~!AtY0eyk(wl7h1t%gZ`SJP+HU$h zh;4V@Z8!5@0o#!Of6q@(reB%%o=Lx{y9bhmO2?;xI90fD!uZ(!UJi0j8j^B~TitYP3XJO*_@tgX(2IyQh&`&e}zy745Z6naEL+AHX!ah2;vxog3V{s17 ztsEZiqX0$)^ge(g#4z{e;`-L+*5$L`Dz2wTFZh`A9zs4U_Z2CgEk{FyL$6)C<|!0H z*UtSADrVGcKa5}FN@_R=6JoaQ?pT-sT`!n zt|yA)vfiwzZ-og_fV;4K+QdTJ$-WL7;XzMWZ=s`Gma|-I?!WH!BXE}z7M_m_!$FX2 ztS9rYvE}TDuKVq%nRh^MO&t^7R0Utc@X>=u^cZCKob1&?B23JFE&N7A zzs5ZIFq;K4!n+Enh9E&2Sj$U}C`(72RZH>g@D^;%m&8g%oaWL;_zn+1JG$qN$6vVq zwg+Ck>G&|*1(Hwni>25jeTGJvoG6j^=e^!hIAJ@>7m4a;6Mha{nQin1*4ARg@nZ

l-`WNDZcDR3T2^?SU?M*$>?t=weG~c6ayBbag%3{p{@{fo4Cf z?SsIhPq~DBHwvW3&S`R;iCsQ|Yu%_&Ko2lA3>meXJ=UlZeA|(9_~G-oGejSZs6WWC zc8Ca>j<&)VauaA2zpI%4KU@bpV&4#9(k%Vb8Ckb@w!%YCJJ(-%5js3-EF3-n86=C9 zZw(>w1D*taQVywl&kFRV{gnfMZbk=$*|_FcMsk$~&&J5<|JZV1o#)L8HEEC;b2pPetIk7?gmeJ0U9?-C^{TkB9Bemh$%;?=TlN227k z$c7cJ7Ot+Vpip7)y)Q3O+qm)Nm%BNv9Z(N14(0JBi^Y&42T}j^(PtNa`svf%{nlZb zoNxwmL_}4NWyk{H0)MxE;cIo^=!Fa4T{xiZGS&ar_XC_b(AVr9Inh7!OMj{YOSNm@ zMDqp?;p+>eW^Q=+*?YUY$42UbgC-2_#_(MR+755WF8X}hInDN_0e;b3FM@NNV-dl< zW6j7~E;Ka}G36bqj*g7#whc^K8Z_@QFOZkkSSm{(BBwMeQUqdYn>LlDL97AGSj6Jq z;dkm109n)Z+a?3XULlUI{7P`IZ;%@ln9VkTVCkq;9dv7gvks6lVrb~ZC{8;&-(kRY zSTp7xa40IXfroqdS-9 z9M^@<^E{u+`~CS`aKQ_3-|qBWb(2p9wh;74N;WKjXOcA=ll<0fK*|Q)?(Bg-&r(4MnV!P*M5sNnvvxJvc~dxx(k5yLutd+`T5D7UG)5it@Ukm z6@wi*S2w7A^nPJKH=D}sZq1KQ2&4a%(TUksST8-Zv)DFJftd-jldH3%v;+1dHxi|;S?&h#FaIT{@0&SJ6B zW-CYKS7zSs9b);X9+9KIxw4_YqPMR8gAeMPoy9_N@t73aQC74ZH3K$^DtbR?UBXBy z#%bukIW4$D;}O0@J(~d)V8p6EhM4S0G|}*J{Ti z*K6~DnZf}2&$yTcysOUy9`VD=f?Fj%ChkEmzG*=C4KcBBXg~Rm>a5)w3s+l{@i+z# zQBEsOq>_0l+v1gFtbMmfk;n`A3$hd$Tm{1!{V_us&7|Kg;|OC3k?DPIbu1sHF3zF^PD|cSf)a(c~h5UsiLH@ph6W0I9)#s?a^J7$^mc#U1U%*N)FY;xg z^QRmzd;n%Z+sORRZfaqDcmMP>MbLjTH%!=nmmOjKG&ee$Seu>Q8J&7%>U3dkVj{P{ z*DW|NzkvIn-5>slcu;Hi)5D2-KbgoaEVOn%mqO!#LO=gB8^XyZ40`sA!_m#now*ZQ zBA%R#f4_9N(fJ}IOkzPre^q~ZZP8ZlDC;e2zNE7E?Kf9Q0oT^D^4Bw|8!CFs>#w?^ zzTR1jqujaJ*_qE`RjUfFs2>I2He3R+89Ht>*D^#O)tjwr1!P`A!)%d$oniKyqGbXQY2#3Bo3Zm8cFx%2d4Xy?j+8x z+n>)QgYjT!C>hPnoy@Jau2RZD!s(m?37xSjUD#Jj*y`)AYVw_@dcRBnK+_$g3q8B* zdwc7riA0a^TAvq?2EsvGU(S;iSeP1r<;khho!r-t-Zk;V8y|hT*G)12)(iCB?j8|2 zP2^7Z=l8n>|95*Uw|2U$YUBn|OE32zupz_a|3foonxC6CA#g*gtJHHBOrVmG5v(1C@ zZy^>d?d|>9o=?dGEX)erTlf0u=x8nl{4t`JZCLvXHv;3J>o{T(U>M{V374KJ zd`vO8FIvb_Z{8vjw_4!S1WI2trMdQE9G070_L_cceDDE9mXm4V$E(wG z1CbcL72~pGFZiHkg|i3cNv1=*I$6vpv4H@pjOUy8SrtI~X?Iou+?HKd2JjbR@xJ(S z+M1Ci)P0bN5Rc{|bdl?l@C>F`r$*I0`aQL!m*^3D5D2If{b;&^N6f zajU~L%{D}=`$lgH4FKU1F97^hHw&S(B`^d!&jGQPMg-8`PRn<1XOS{u` zUvD9Uo6JYpTYO=S1?39(Yg`xjouQj3uL?=)smEeLVmcTynZi(J002*@?!XuF`+-2x zDKJU59sf<%BK=&Rj?eDOu{#-)6)(fg5W zLkvU5MEsjb9}cYjD)y6nf`Eo{XTGZY1n4yrMz@V{Xthazp7X1VR_~{D=A-sx)I0{E z`C2>1QtFHkn}`MjQmK;Ed9Z8YeS|+`JJ3t`kPA?G>uN1qt&D>}c&cxp*|-E85s~-0 zrwU>)qG!H1nVM}zL+|5P$Q49_N~j>Nh=B_2S3l@4_t%;B`&p*0s^xgVylG^!bRR)0 zCPdDGZ^d(wadqvoJMXEMjwo~*bnwxEv>fxcgn44Wrm;~8Cap`h4^*poI+F(yg!L~e1DQb94${U~`PW~6RaVwDIKToSZPO=H_^yfE z@a$&CI+#u%4PZLw$M3~0pxWmfZ@lsQ8+&gM4FVjx`!r!uF&jSJ+r`n{-TUZ`Jr;k! z(d&XA-#*Nu-#R-#F){wiI02spXaPH$-H$!@JcY3URFG0$U1cwLcVlkz@cYH@f7}o| zK|VhOC>lD>V_4lJt`)OD#}{3xeyy4{xbkAx1^0wp;Md}= zLQ}Eaoec(EqO6W=jY{xV3i*M7!OT%q#;VE2o@!G#W!oNzrGv;pIYz(`R=l!0MtQWP z(iXmE`Kg#9<2VRRT%h2_sIj}V5v(MS+he6eslUIa2!!e0@Ha5p0L;JS4L}N~-QI)@n2~gMMvsj%13xxjfEG2TgJM*c{#MIpEFg`7T z&PN{|&!s?bg7O@l|MJV+0=mC-`s@Cu-@YdjTRq#`yL*I*L}}1%d}p4W0uB|-;O^Dg zwW+V~y_b0Cy@{Q~($3GLJA{EcM(20Na1fqbr8WA}(&6Uj$jQe0$1gtg;)};eeNhNI z$)nmoD;$mnXG1wtz*$!;?_GS6Xz5}(p(0!TP;aku$W{c+S3K^dz4V>;-+#-0n%|-J z>mITI_#2TQ(-qdsy{QML0J*F)XlJ?->2PSITJI378}*1w zs|~7Mb2gxDyvC)9g|j0(6?KAmza-*KCW}!G)Q~g$7ajFnBesDW^<&yW*BeMTZgi8! zba+)`o*#2i2SuO3=9pdK4bqAlC=!mz#aCbL;a~X5>ntu$%@x;+|7VKJaudX*WFEMI zJIgu^DOdIc{QjyckDNg-@k*l^E2D&ABH6wu77VRb!j-)~AIk491SFts>FMd8GifEQ z3YLzPjwxAd(&m?CD(L$ZIcqt0)3x_pa!)l2NKL;xPhW`$=x0kvbZz(hl5TeZJ*R<} zJJ2601Pb}1pYt#P`jkQZ0a>zo?4i24@{ms^d3L}AXp3|R2X;S~AV`lHw9}fJc;&9q z`P}f{9z=lWpWDh!eZ9H*)YH(ycDMF-KSkdA=%YPUew)+w8+#w^=W^?_OqMThMZO>- zkji2CEZ|bi?u@_G+4<1>JG9D}J=}p7%tTn&oEsU`0726~fAzB3c?K4kAC5vrIf-CW z0lnpABmtb~ioG*09xooh`0~p$&dN*bo6j{BXDU{P{6ohF-yeJKSJ!`?2!mGNA~^41 z(;YDFdrThv#^gepcsRj$DfO(#nhafy1g7!UjZ6Ro#C?PqAxupz|5coyhuXLv$RT=T zMrs(;U0bYvuBcaIL3rZ~l~JDt1^|u&E&vyZ0faD=*N__4Wt|sn8vN#-z!O7+!i&{h zyAs=kPB!OS&2n9@H9z7XSE_Hs{|3EF)Gb$Ea3%eqF0Q=3I>z?vMPe;-f3dux{9rND zmkoC{#vqo*>4i+QaKiG1kfZLP5DmEP$sjW$A_qK71ZcO*bP{(F=dUKliT)2rN8fME zN$EIGW0B=BG;n*tk9PQFX)x`PrJ(gHM}w{YlKZOM?tZHz1!a3Yp23{*({)hB8PFsf?*zujG3wB`O^-6@Q+&D@|21N%)`*Q-+!J9gFsx1lt%KbeZtu0G*K343g=!+Gs6M1I!+6#|^J$ZktA6AUJ$uE;bnGl0 z{*#m?wMS1sqiO)>Z=q*ab>E% z5OX^aI{|}2%T>^4N&TPTgJI7bM!X-W)2Wp}htR&=*K5K-Vj zQZA(%db8_$sZ+~X1OPUK=y#lR+~r?#{r3^`!+kK2MGMf;mahU6DIqnTgS$x zS4S98A$4(gAvrKRLdA%=^V(N;7S_YX(fTYI?c z8E)Of8%V8$%+I?jDbb`#+6OMRx7D(_JF4!Bkuae0}r%|`W0hiskNP-VF6UsI*M?00{+@(klM1^zi%avsf4@#mYAvM`7*SKzKc2?H7la--@O9sb3Fc!}o1qpppEh?VR z+tak48zhVmS2h_+X0tTal|^p2D;vxT;3jn+B5`|AcEfQ2NW_;b)9nO;$aeK1t5_GH zCR!tMU;dzgo}?q#Kdmt3nHG{gEBE*JyMxL8{;I%vVE=Qu`NLc;G5X};_>=G6yR@0= z+1~o?6gb<@gA_uf3eYY)f~X4d^~9H_`(MtVf)?5)PmJ#i`V+{1`(y{*kH(jF7Iqdk z7Z%oX*i5?siS|+*iSc*8`f(0wFufw%1=qf{l|vdp6AT|#g0aoq$IpN;F%N-4s&|`{@vY|!rLB(qfo!6qE)CV#v zo|G>1NX{84AsdMNBiQ}MxGtqTb6DuT3S~Bl?Pe7${aXJZy8WZYb+m6(M@-zm`dF8L zkLmt%iTkJpb9lormef~n``4eBYM$^>d%o-M>PtJOmG}ZJ_2pqpyjsA-fki#oNC?@h zuIaCe&i-CVw=v8Rx82o}b=$*@P^jF=EDhU4ul~O}XOmCl(8Clc%*v9Wvd1$0U>~fO{(hfCYRVcUSCtLs zzsGOR7eH@P9~s(98+ADOXCDaGcp(D<0s>1c%;u3^z5y;UiVn0nfY+Rb+`r4h*&tZgt<(bji7TpT(86-gCvZ7oV3(;j5AX zM*TMv_b%nuCjtEA_WxKL-N>gWAw>>v@8RjL=hh}(na4KTPfU%@@AuFUfKFi4L`3H& zQwxy54mYU_n#zstEac{=mS6yHCML!=e*8HI1%cj>bNQe18yi0^{d}~sarAxX_eVcx zq}ZK`B=XeI8)Qp=h!R+Yb~cXRo_YHnG5`&AWW^dTsjqZ8&pCT>mCK4{ct4fp70e1U zWQ+8xbUmmmIfw9r-urc+!x@b~OKst|B*uvGy5Rz7cE5*My-;K=gsJg9-c-#~;v!h2SCR4|#0nZZF0T(si8|w0st# z0s9#sLIcl4bQ%;f0?}1RG@okpK0pWZM!_jz`twN{2!0nV z#B3qZ@+vPjdc&3wiW^V_QvV{ausdmC{=h}R_Lt)XU1X4!itKh-V{let3KYa9&(-}l zUv*`g%usU8qpL^4&_glMF#wnaxp2Fo?8)t+_uOGw1;S=EA7Koa(Hh^(HgG z5Jq(|hE(K^$yCvvim0bi$sdwCYqJ0aaYMAH#9}x2yry~@#yetC8cTk$@f=YTX4<_u zhBh;7#COc_zR77UMUC`&&KHZ?KFmJKqg+sZ`3q$D(0CzeWTeY`!wp{Va_qtI10whT zm_WaK(O!PM%gZ--yW)LVG>SXm57@KzY|M+&DrBj`^yg=aJZKT7w1*Y=26&2CMxy8@ zUm><})^Me9IUDN=XHhc+MRxlF)pk4DdFM5}oE6P)MYI29MIt^#c!1!XoL(J#H_LGQBgp95B( z4={yj)XP5@2Y9-xi=AR_uJ7n*ZUkMgx$@deD#N+gc&2c5|`PWR?L!!wpJ$opuTFk^cPTMO^Doi|R76jy zh10uf3-;XV0Nqdf2HQk^xgVo_nUj-_v7h0=Y{1|dY>&&yJoGfXJ=yPX0t_NLZ|9GQ z*y7^Oti1fb+_Cn-0N+rA7eoJ_M0VJL-dH1b^1pCN4mE z|7{UnuZKy`dIzPTsm_-%>><&>TH95s#cXaNSc9&4FS_+s%$x@z@<955=USri>ff`Vfd}v^tM=> zfB#+Ad+<%j$M;-2oR0Oe1JAU1@W`~lEJSvIE!@zX6g zRW)64)g?7{i#t@`@3#7z&g%dIx;wnPklGd{uts-!x_5ur-yhCR%un1qm3ZarSDsBQ zq=t9b?wy}(-CfU3-~gQ?{rvLO z^G{I)eQ<1$0=ccLb3ZO^9G$GLVh8x0%|bjMX11G&O~WW@cXO zTgn$eHR8d=M1B z)SFJzSiQcj+1&!srb8*(gK$2xj2jjYLqK@A#Sze76;BMqNY{TMyekYJRqxH9j=Az{ zb3q>}l|RAg!Qf%fp{El80Aev=5>T&Rbn6YvT_R*Z*iQA~$J(BMy6rJsRLuR0Zdewt zJm7Nsr8Z(I%jmOQ_`a`?ddb-75zH5>yT&S~F<4?@NLp+dUIogEr;ipB3bDt*bXyl2 ziEMFMYO>W9t1wLBpm^{BvXNykZiUOb>?I^5Tn(%hC40?-;p&*T5d#eTj@=gmfnd47 zjxXQ~6>hqwCFT)`(Q7XERJ$&?(pTd_Rax$X1*^5+=enla>TB}316HfTe)ZT5-dK?7 z(AaeYJCIrlE+Uf3NxsvA4RI6=;v5I#!b5RcU3PcC{efmg_N~j zwgdTsdUoDsZfACPcy|AzZ3324hpD}-_3n>aM@CFTY zC-}nOtncq6I=aEi?QVax2N+_TYH#biAHkA;{yD0mv_5sw<8Qt7*e~7BQ4JLR?`{Vi zu{t)ld2+Zp$Lddk_Me5lKg4=}kOZCQW_>TC5+a6L2%z3owu;^tJ71=CF;F1V!M4BZ zY9S3A43+mbTvg|=3F4R&etfyD%wZ7_PK2XL?8WxiH-Qr9*|hyNo}#YrX2)xW{vRY& zFFl+^=m(WIP!O68&+=|qL`I0lT$~Yg+#KP?)Mdo@TQ0>E&^%r34?zU+F~ULyeNsE# z#vnp?^>*Ridc9<+gt30nt6dk}`YI+prSUJKCfOi?iS|BX?~ipUKE&NWx8biTX-E36iFjN_^+9OV_q zaDi899amL4LgjT=)cZq26}=U8TZmJ3e_+d;;7>P z)q6VTssCA|_Woh({D0cqt2n-3A?bQ;X$9_W1A$6YDPRg~JEYE(%<7PD&c>oFHg_3r zpqTo@9q!`f%Y@;28A}$nW!`SbS}uY?#h+xWk3eu2h?8;3H$}V&ZiH3 zL_O=5r_b}yyK&F?e9o8md7m26H!Rb+Kdi>-&plEzO}yDnU5XHuyfa&GJH^&X@`{rQ zcr-gZt#KCmK@#-?gGN_t4DA>s_dd*C(GDs&K7zYGFda>qM-mCr`xp{3kcyMSoKAJb zr=#HVgY?v<@b?3KraVuA2)mhKT1|1Xm(7rUC&O*eL4yA_$uKrFV6S*VDT`wF*PZg9lsdQ%|36yua~c zeR2K828>)(3O8v|2K~dTPpuBxIvXVbXdyaX*;z)Tv1)@%(FFZ5bc1ENy>y9X(F)8y zk?$zVp<5t_MtJ~o+1Zs+3)Zrk`%NH|>qj#4jI>ofV-0eoi+S&ATK&}lXH?23t;w09 zm&~Qsxu~?WdyBMc?$Rn!`#t&n99Bv)ZqQqz=$Y@(M81miX4GF9o48bV%|fb?L=9->rG&vhoJxgbHeYy-WKvs8qHQE)H`Rlp zt}m_slHf`6U7dZ9_=M=&jhQT#KnSfhgWhS8z#~ypGaVhFD1kdw(>*y&h%FmHDjFlF zVq9yD;#+jY2BWFL80y~{!O8b`EL)?wp`ZM{1P#_r&etS-7P)U(S!x1a|w{@`1pP>RuWI2QhGe!AzSp^NQ zp3cZx&LvW$oObqkV1mfwQXr&i?DG~2^Nn$}NkAvtj^B(!F`T>4WtOPeq zc<~bi{uHpPe~7=6d+Z2BSmNGOqVmHh-a9pXV)(?VQ?R!3M-VD!NJh|l?vKWkiXJeU z8gC7z2!;h5bB!cXe53>C8B_s&LlQrP7DQ5Xz&!hci8Ia9WFFh5S-=OQZHY-%V*=2m z49}cwKNU~)wfd4INaHv`(@E5x|$>br6v-01^ZC5AsMQQsb{_vm{qbOprqC{s8kd1D2FI9wZOS&mT|? zinnNF1kg!G{QXY}YnfpL(Es_fXODjV_m3ZbLL(zxUqAWe`K@m~gXQhktB;?5{DHK| zJbx%u5w|{kbc!PA8Z+>a^%*>NZGYezXf{ji$Q2{1kG-l;^OJ}mzdU6N^ z+%qIeLwmU$v{{bJGS4KuQj)hAE$UHFr! z2JG|O(`WQ!eILm5FKQvQq>WU^jW*$3ww&}gsgAI+!dejEbH|Pm%(mrz^@pFyZB7hG zW)mhgnbC9i1ao@6``#(Kl7!C;`F?ZziP_00-6sXD{v@#@>CnMsb&4*n-spHiYy@t7 z01r%4k=PTY00zX+5_0Hx5&3<=IEruY@JDH_9U{vTO-9*8QiH^%#hyeP8iYR`oSq&Y zZYD*OjGE0&xznDxnz&#qD8Eroqt(TaqdF32xH-y9VU5ZgS5iJdvt0tFNQ4ns0b5^j zFle`#Qj#mh>lzsxwEIkb_IUsFV5HBQK&`NzKl`u_w>+b@6jw~s&k*@utuXg;8E{?q$J&Yw^OzP0#@6c9bc><>Tr z4<4{C;Iywky>Q!x`~_v}6!RB0ABDKzFnQjk|Vl za7Fr7!LOC1HD1x(a)BSvgm^-}_iKkS0a@)eVrUM_3aM_@w#3`e_uOXuo*GGY4y2-q zWH-0lu zrbJ8*fhO)v5DrA?G;tUm`Bnq72va_r9k?0`e|(T^N(`4{&|XsKwUO*WsqlmN7J>|^ zbFo|pM}u~cZzPzK9B8*h1!0dl?(;=V5x>Q6r9G6QTiA>S7n>~c zxG9zdFEqdxo9x8mQEI-apxJY@sXL6W6Ys~T34@6%h{=g{JU+V>>hr!3h9j{Gd3CZs zxJ-1*P(#2sV*Ys`-{v!8T^1SE6SoJ6Vtf|h87@ zzpm36`XBB1e;Cn4_AmYBJjK;I?pEV=dL)W`tG08=oJ>@b+#Ne?nfIoq@?S z5L2G`G04O--zNn$JDk9!3C8dT$mfvqmylqn1CO}FVh0d80y92^VfGMM76ecbc}&B5TSM_Jr9rC@=6GZRmeINu-Y{;Q7OngD18Ce#1p-~VLkXLs(vk#h5v@S${FeR%FutOoVJ{djbA zeq$zw;Bz^5Fgy8e&%~;@ov>Vi^bZEeF#v77b-%=SOo$R?B`OkS?{85X) z29eWH%z3)JqfK~`yS4Zi;Y;QF2~n_s{u;BH)xNtXw;Ij-?vd(TAPoe)M^3dfrLZ~8 zVLZ#Mk~1ex+{I^!(lSmeuy^uIJ4LV2<=*9vWbzD#Kcfhb=DAb+x( zv?}Ny_-SmNMw0>fXG?!>KZ`$|^6{`GJ~u}U@3FS}Bc;Xprusm{7oTeZ%+;2#z9z$J z4|+;WUPlRyq849me=W!k`TyBM2tlciJrEP9>2EPxEk0<$F;P4$N%EWH_VSorXr-I@ zt6_26@N`D;FM^0R)TZsALZSVnCh0Xlppu)P3Ob?Y}-e_xR2f&JT%97(gFA z`qQ7j^|KE_G~?&|AAE~B=hY^+FYWBMJi_uz8@De%HTJ7|I(Xmr!xobbpcdzWo>WI!1LBR zj{mbkb_dW;q}AzsN|%{yT@G=w}uNlryC^j`b%kB6}tkm=HnE|B85cork{ z%=WdW#4Ezfz(HcYB^@(p4;Ac~TB4R|jA?`*;!Je{5N11AB402eLyKTJOfqZt423da z$&8i0Ff!zy53RHg+T$(LXfEm)2mXd7sN7W|88+LQ?bz57Z<(tFq2Ti*h2#O1#zk*w znLAooR$&h^{v6Yx9j7sTwl>WJ1$Znfoxv0kfcur?QP*cnFynUl=@8>QRwPqof_O>T z1c+z5Ooo_lq8MauX`E|pY^!bTpTn1mx6Q?U$pgZ5yLI(ly5FDU+J8z2aP?>3`TEzN z{p92OOG~p@#O;T0yip8(@Z#M?X1o86yI$S@_;0_2|AF6f*UQzZXAd8*Z;<1>K}$Qq zDg+Ox^N%J_?Tw8m*;wggS>W)oX z?Sb*{^|N3LYQ5Ji!GQ2V>#fm585}o?|6e=TUo%%I!;*7zzuR365c7vent|h>#Nd6V z#Ro$9&I~iMK`cN<(~|xXF8z_=yLT_(!k_7Q(-A!-GWg_mM--NP6t=r&K&5UdkhWTq zlOxN1C=(n8IQhwBVj9W8$hdtlmFyr^HL$b`u$ZD58XhIr*-pF}2nakHLQl$UjBtU) z^VHqZYY&F#^6K!7M+Y%4T(xfg+BNZ1A=*!sLNIx4HEnZqa{`TM0l_iX)<};{3*e>cs(^u{r$C4I~2=07WSnMY~HwegF*n5ZMR=N zy7lbUt3Un;{&Weg>)S7D<1CVp_1!)7**m32oK%7;%x3OhTU>b96NtC|{&!03PaVLMi zFD8Kh!^^2N@5$u0W-2g41MT0ZX=DhnJxjHOu(Xs=)@7dm^2$83$*)}-8SI646S66i zhOJcvopylBc1x%;zck>ZXe9*%01{SvG8|129)wv>@qDH+yOHMh3sE!6Z1c3I(&S4_ zk6cKRmv6^_#z{Gj2U44CFV!UCMN)Qn=MERv%72 zJ!-_X1|RK_!FZT%aYIABaMZViQ#e%icx~e`eE!C{aQrAe#B?nZ$VWKvBK+Y8pd2b#1_y+b0hwfv9!*$aVeBtS*8`iv?q3; zn*<+_{mD;R`Q^EvY=Lva;DB(0G84@g(mEg!PX8}OgC=sIpVLVAl{b^QMRR#n5Rl1? zBAH{-LVMnlyb%V!!&5cFR{*Cu8g4vlFnA~j0b3>sj&DQWXMhD6wLjI2SKoZ<$JoH# z;mOfx@1(5%BMiq9H|Ra_|A%Bu`H#<>80{5uSttr%S0HS}Pq6u8;}JhfAKG!cyauz) zQD6b}LyJ3r1bm~2qthOx%Y{NLemxFIJQ=Z1SK}c8XSak2kp=XSf`$Q_|Bl`v5RLYx zLM(5Vf#tzqi^pd+6ylT+jfN|Wi%XzqG87lrzjOF(Z7p7k z2SP>M9#-l(K+v2p`^qF0;H#``02p3TQe0YEY_kLzD$|r8!-N5(EOz#gL>m&fIT~pF z36`1T(_tcR8auz*Uu(7mgV+SFl*f~(!7n?&;FkN(UtOh#Wol|sniqccHzia~Fgt-57bsDDoBwE!L(*CAehW;fVZ96)C^2Y(MQf$WX#)w~}S9qa*} z^-gsiF(jfvOA#U4l_~kSuR?g&T$5}%0<078!>Iv=gHE<%2E@38wN$%iCr4-T!P_Aa zAGol&JUDqxr~{)k2{N6Nuz;e>@R=W<86`V9{(5;Z()#+fur1(G2eMg(T<)sYNPa_Q zyosih@wGmC>$tyQx4;oJlSfR(!z|D?OSsThVkXBvi0kAt%z?3-ZZq5a{Aje@01_TbJRDKzg0-G z*Aot!N&D8~|J2O&V=|4kakIV5qX=3{421^`Bs*Xi@PYHBKu30{1w=d{c4JdN*dtJQ zzRH7V3mfs8!fpNil-`bgwLebk%4!ccg;`JrnGP^r6Sp3q0qoY))SX-R?{7VQd28|U zhkyF|4}SLiC*<=VF5P(dr}ut(@8;Cad-ou)+FE}(wZ6IZ>0f{Gi;tIH1(MJLq64v@o&tu3Z3Bd0N(N

|h2+Xo-q_ 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

dtn+2ivz;qCo0;Wwg3H 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�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 Z@n7_I(iy(@2(kbG002ovPDHLkV1i5nqxS#+ literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/feier.png b/dot-line-system/public/images/feier.png new file mode 100644 index 0000000000000000000000000000000000000000..d849e607fa50889060c3ca012fda53269ed19c5f GIT binary patch literal 183150 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

{TZWV{TY?!t3 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 literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/gpt.png b/dot-line-system/public/images/gpt.png new file mode 100644 index 0000000000000000000000000000000000000000..ea252e573c9e638a8ccc8b1636a4b97b9a400013 GIT binary patch literal 53546 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āUak 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�_TE+fX7t!`as72vw)){rZ@tDk0 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 LNkvXXu0mjf&H@~} literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/grosseltern.png b/dot-line-system/public/images/grosseltern.png new file mode 100644 index 0000000000000000000000000000000000000000..6572a1c7013dd82feb23558d0d27dabd20daf7ca GIT binary patch literal 24485 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 Z{{h_=o#m^Lg}49!002ovPDHLkV1lKKYRCWp literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/hochzeit.png b/dot-line-system/public/images/hochzeit.png new file mode 100644 index 0000000000000000000000000000000000000000..ce617606b7a94ae2b91140e585de5e36fb3b8e1e GIT binary patch literal 34850 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 SaBofk0000m7zSg>nw=SYvL&QK7$iw! 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 V!yF&(OQtpex7F^emMP;x{|9UtF8crg literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/klasse.png b/dot-line-system/public/images/klasse.png new file mode 100644 index 0000000000000000000000000000000000000000..91124b8e2978fa5c88097dc4d7606500e9b41a93 GIT binary patch literal 352014 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 EwtsWTivI@BXcoib6Nvxk=(JwO=CTsZNwTR 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�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%0DjK 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 literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/oma.png b/dot-line-system/public/images/oma.png new file mode 100644 index 0000000000000000000000000000000000000000..941dbda73348335736c843b26ee148b58437f109 GIT binary patch literal 30789 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

({8x{NJiTFsx)y`0Isf) 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

==wakdz*%R0QjGxP5%oB=v8n#Jb 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 ToJ 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 X-gRPfa+h~+00000NkvXXu0mjf^)aZN literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/see.png b/dot-line-system/public/images/see.png new file mode 100644 index 0000000000000000000000000000000000000000..59d79f9e9a8b89a58dd1eab52562d84ec0ae065b GIT binary patch literal 131002 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

3(NpCf$4 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 literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/sonntag.png b/dot-line-system/public/images/sonntag.png new file mode 100644 index 0000000000000000000000000000000000000000..3d5d6770c3498b669367e746d784ac9e9f9dd275 GIT binary patch literal 645343 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 z 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

m-vS* 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 `N&3=_KSI`X`_@nzF8|i8^nJX! z5`!PBs}R!46}U~FuaQ0q;(x>$y2gWUy>sbDl=+2qPFlV95)fGP9ZUiN2G3#?m}CI& zd~>TA0G(SLL3jUt$h>!GHWLY;z!6c<6{`b~JpR|=XF(s2!z0#zLg+v`VeNp1d%hIh zdxIX_JdPf?8`48=1c6~{Y~U=UU`*LhD1-GShEfc`&U>-)IDh{H8?<56mD5v!2WJ3H zq`u){lwctOfvPLzL)qUN`{fs{=Pr zBS6>1u2&adz34K~KmNx*p0>XN^8es`{`m8MU=Yk&Jg%?b^}WswRrvDk9K=7x*%AdX zQXnU1WBtAJzx??0*)5FqVWby;#`hBie6?b`()%sLxg~qx|65wJi2#mVzHvy6gg2-a zG7pr1fd{S?>ahWg3CrU7S*#D;*L0JXD-@0{;D_kQT0Q}13fe|x}kO3^llipiSPyZK~#&zo! z_)b&))3Zpvu`O5xNe8ak)IBNkYb=N0#k@mm%HuS0XwL!siXN_UGa4S$!72o7{h8_k zsu_Lh%3F>m=*>HD_2?E{zwyydH?Xw=L;Rrt66+_?Pe3VGC8KPFcc8^g8|G8;3BS&) zdM+gUbr_r(M@Tv0N{E`*l>Z6B^M00W%rjbd+#&&B&t5-M1kG|&pETj72px3#19cD4 zL@=6)P!Yh4jCp^PZQbSb<;pYQOk~2vtW6#f(%GT6W43P?o$moU>5$P6>pm>jETsSS zyW>pV(*S{C+J|2L{rOxN~Pt>ASn`eRS&d_2IJzDz>jx4$puv zSpkyyuU5R5fB1NFiS-O%09uZGaJuP89_qi##Q&-l5-iC1gMO~?O(R3I__qBx57NN# zJ$v%jL{MkwgF=x_gZnEBzifbKxNW4sq63g#Q(hbnx}CGz`wMj808|H`m|=hN5+ z)ImfN67_q*w^N~CB6OeTSf9~ze!=*!m(-l6IrwV)VbTCc5+Kb2T$Ta2bSsS<2nQL4 z!}kJT&V4OIp@^_d)cRd`)c=bSFe=bVwv}xM(y;Mp^;M&5 zY{OdvS%v9#4F@hU;1#)<)c^pHe@Un}KVxyI9Vaz_0My%#sKE*VoJM^xpPt6DfE8Wv z`62tJ)(o=*!oMpXFR;sLxC|9<@zj#e(Bj1A8&=hGtLA>q&%RVWZ&vU!E&v@3E&urb zXP5*o`^KLcIAhZpmv7KawRVOM-&JG3g1BdbB!6q$DV&P|80+Ijcs*lm<@@z)zIZz% zB>BWpu@YQK;-a~K%K(@M0PfXgQY{hS%pm&H-X;{iTEKm7c4gLfjab@0E-A27!m0z3 z;a@*jk#EC4u8-?CAwX%S$Ntus~;=lj3zhq-POlPEfWaPw&LIN=X^$MsK zz~$P(zk++y^a$t1@ag=Q@7JA37-cGgSxTZIOA!%mi*Ze)4c zhMdxk+4pXK@aeg;@c-M1{=qQQJvlN1`C0krh7nMgRAB?C0^a!a^uZ%H82tnOx9@we zLPP=B=TjF-0oG<|LTG-)mLgUSpaN0~0Ek;1$V@Hzhw&Xzk2{8iVkbQ_r3Vh%m z1Hhg9VR1A9KEWaxguNxWw94{|sp@7VFpS0Y7#@n3<&6NO7)eX_C+VsFxX)JjN6hqC z){lw6BqRf)U@J4RvMAobN_vS1f_9Z1glLaXHWOfB0AGsOiQJ_Sz>|y7|Ae!C)d1k;9moGI)uQMQS~UcW5+VwC zBLlMJzrisAg)#sEMgG!ZdBPU|hwPX!z!DH)lLAP6VbL4W7X$Pa0^qh00%;F@1a7Z& zmdtscWdIleihdXXMrF;c%#QJJv4OK14#^(~@MdZwW4L;a?-0bFttchrkEAjnMPq^3 zpGP|0uTg;NApra$^i0(|BLxBRzl5C}S^#`-O5&R7B2t-OASr>Y^7rV$ z3*vvY{8_Sz6ad*04E{O901OVr4CJo`-lPCcWhMR+WWrTq{?9a_ z{e%D8hyTyd2k-`dI~E5(98wU>`fAp`1#`>WJWW-zGXp~Q1sm!ptk zzxO?* z0ptE(ab8@+*>^sD-MIkx&!0#0fA3!X_uBFJv=}P--*ui_%qVo={K5X5V+ybUPe1>B z3P)bDbLWOVB~@5~b@dz9?P@tsK!5l!_5XbE50zg*7Lb1+pOU`p!S1s~eQ<}zU=L2- zJbn70Hvi%N85c^pz_B6#aexQ_-NF^oh*|GTFJNni)|dbr+^P#uab78uU#+A+wd8JM z=J`7+nM?AfBc5L3X9+wp1xZ#MyMU6!vT>FALh%V>?dk3(&ZJB`?<0Iyut|2w>1zi5 zn)`Fm}h85hHGs<8wgPb4mgzty>H7x*8H!zHOQsSZK{LV|OngOe$kMixuKMqNf zG@l^Nh$!Nbv{+|aW-ybYPM~rDtl%=#o0nK$^4uXV+xU_l;F~M}BA%;O27uOkh#*kY z{xAW6-(r8R2>CkF2#Rs$1JwXIfRX@*R;W|u-zyEKc-D|?KuDo}ayiL2>(5H@AC^eq z|CRk)^%h;fH1urk)sK+0i-4ElAJU&3T>$GF#EZWKH27G&H#pW@zn3qm_*p3sIX=ke z6Fn9)W5xS11ee|dt@@akHlVs1^jq`(OU#HAd*B%h&+9{EiL)MvJ;H_Xf9ov(_%rx_a}3~)a_2Kg+N7s8AIKd@0hl9*5TJh7y82yHQy(2Wb((VidxZ43e>glQ zhC={kY3T=3#4At(=F;y#>5l+_nW8r?pV_yag8=~V6b%sonF@fPv)2Ax&(ZpTiY843 z;2c{+{BK%rSR!lxL)3+IjH)23VTHMbwdzP>XQg*=V{L%;R-0*MGz1{U>3~?VyBY!u z7^TnRL%fn~d4F%S-YmUEJBBTlvpcZ_M3a=KBplI!ZBMVb%}$&Y(_6Pyafd&Xl^6Fj$T_igz+OK? z_(&OW&P)@eSHoDzgbBUvk4a@n8f2114#ry8hd&tWn>>6@Gd`L1Hw@XYY2u_S{Joz& zqa0B-qY#Du-$_iwz6#EP>)05I8Y&hJpF4cFX;?M@d&YkoGNJj*P>p-8C+IC~_!$uZ z_{R5})pb0)xP3lx-9X$zfj1$5!}|kjoW9fE3q~JcBRH%2VLgZs83eh1J@yF|K+6E; z@bvBN3oXC==w8bdoqVJLE!p2|KidAd-hBUG{&MdQNdPGT#6V}~Yjgr5Xaru6KaeLt z9&XDmLj{msziDUZx?MYWF-Pb!B>*rD>0hx$X+VB{7C}5b;Cor$%{nldmz$S&j7CAU zf=C5fA`bm8l@O1B{+Gw+k}raPo%b*U-{pBtM~*-PVmQ_e;xuE{HNu&92p+j?>lRxP z4CRM$s=6YagL`h~Vd8w$lhtdpp^5qB2VR;_u)*>6IlS7Zp{r1}_T!|~oBYVcN*!9u z5DTCam1&X3AgRD#2>MtT6(^!K>FItI%QknqYy zhPk-`ZxAkTbaV+|r@%jzGUYZR7%>Sre?CHZSja-CfSORLFHM3Y5+IREXP`gj$K?HK zD3T5@>_K^}$=oS2fKhYQRQ`2v`VBuM|EE9SWB~Xczzm`-6?jWDfD>ujF?V9e*d<_|YOGX@rK(fY1U!2zo>V^tTa>-fJb zYpqlu(syBnvb#;)5gTZKC5eA90ORoZqz#aMHWv_Jk|hk5m5s&zD)oRC+7A+^y;MB_ zhoP=w{)+0x;~P!6_53+qI~{Fk&(M80MJ1o;2z*u%j))W3E780!6^C1sQh5jmn_dyPA~-&l8lIqdfN z!YPf?QI;`!p9k=A=oyl!v<7iV7NnLznr^59K=?~o)KJGJ4?fhvBkhj5c(|&uAfN14 zAyArNH3nH%z<-=oS?ae|lK}4L0A4_$zhPK_t@{Two(fYU0Wb>YfM6dD1iQVc8u{g2 zW&lzr*l7=2Nyjw**~D!!3{bcjbhZFs86(nhbA#4*$Hs?e#z+oik+tUI!d>92w0>N- zzztwdNgXH^5bUGXNDOLHAbWO53KSojLQ@4Dzj8D{I%&XD@t*j+sCj(HIIjb-Fo|#g zA}8~A75EYsu~~l!@}R}}eo26==*U^=Ywn+cZrE{MGrTIW0fuatVl4ubmxq5$2B!Vg z3GVW`=d?3}FZO@fdkt^nXaiWdq5go``2>>oXw8s!a~F#`3J2OW_xGVg0ltayMgMcG zfltFdL<4|s)Ae&JfRvn%jGhBS_u0=ZBhVv^B5*Vv7AQQR-RJ6OJo?cS7y$BlMfU#? z*S~R-#!5JR%`0BHMnpWD&9XTyf`%ndz-R`7$S)4R1+oef(Ox*v)XX4L6B&Hfb|H7^_K+j7A^;20G}cOF$f%`^r(TrwmJi)`LQarV*vQzs1HyT0wbE< zdV4)H0U1BjXi@+Szz`*ej3N+*fFb+C01&V!1#qA6c$VX}(63DXkP(f;3HoaJn=Qh= zsGg7e#RTB*k@(?N$7U=R>w^N1{{4!{uuY1&ZV z1;%7j-`6m|m(S!gX30tv_mgHcN5adAocVDK&7>6p*r)^V&wGkTV9q2SpdWBPCiX(p zN@_QU!aXea=x!5>r}%y=20-{X5L=M{rgMKlt^45Lih54)FZ{pKA+KjDh;)KV6r8`8 zkngks+e5j-48Rk!7pVPTcf5iEq~M1`KdSoQ0vetA#~oOP*RK!;J!V2MvA&9fb57mh z1)V~?0{<4116wEo?#W&U_&e9t@2sCXfAZ7^$IsHyf8eVs|5dOZkQ%^uv(9Cixc95u zC!3iOMiqb|;cN>%!(-VnRAKu7p$T~skXODSE?|`eQgqQ=83wk zM%69Fw*9RvA5d&(mE-2T4TB6Ihzb71#iFVrpxS5sq6q~9U>Z=@GGoRUQhiJ12tM^a zA!@61aXC%;tMrn_MQIq7;wM45Ei6Kaf``OID}^=#pk`$^nATvksOU`$01ayg#oa01(HwX-+!opG?Kz*QaBs{13@%YrIH ze1K{I^iL<<>DRWPYHC zdBBPYYyt=9B-u}-lC}IP=W8SA{Rb1&1#}+)p$x!0xgU(qlV<@ zp-T_=V*sqXk3c{zJx1vPX3T)yjh_eSN&xKWkq%8L6T*$W;WhsRD#1AthdH3n^R0(qVJdr1RnF1&y&WCvup{RW z9XWXX@cXO-CH~+3-L(gB|78Aj^pFc+5D)_FVCD$}K&q;4eER+wwg|IOjA%joLBYSQ zgvy2-@&+y~C{XHfw^x!OEWnz`yb4|#2BwA&LakOC2?TXS{T+l#1ytaQlrrf_`bsr^ zLNb;3`Ho4TGenP#09O5V$;9EI7&14(Xs5ndjC!y2ZqV4J`~uTYqDj0E!3~tH%kdin z$eV)KBv$45wWVCGg#f^Tp)hnP!cbbYps5lQwes~My0|5tGWjdV!i5ZeTaJ;`okuu? z!g9*XybO)gNH1X_TsEDOhw)t)QV-H*7v$P~jKnpj6OkIY<8eTT9`uik(_*mRH~nUIhQgUSgSF#vUP{89m!4Dixc_&j7N))Zh9!sKbbq<(<^ z69eN^R3pR@DObP%)D4JP)adU<*&BK+Ncm*|RN&TtEhr8`0!YDWa-kI&0Gw=`Q>(E% z$_D6f(5e6w5Z3cgZGaJR7O_H6f`>UnytVVe`KUlut-G8CH`M&}1r2n{GP;G}ZRVPlks&rXgrUa+|cb;*wY zu|yn!R!!TZnyXvu3fq204bTFA(JR6~;Xcy6N22(IIv`Gp@67cvM!@JlVc&XqHOfm9 zfUinBBnDC1Ukf4ZVU87XjL@wa2s|L&+5aOWu|p$lJ6>W}UhOVHK+^lAIWi;5VY zjBtm#0^kBwM_`MMQT6WI2M2h7p+2zxr2c|3SwICgfLy+M z&q-VHbK`ICpE1b*{eo-&m+v>Xzghs4|92{ZvL5-veHZ{00@kPzF0(jj0|8rWT{Lvl zPm^KXSGX~q@oiRzkLyHvoW(8$1j+!kpf;XT=cg-PT1uCYH0UJg_!{*9gc!`NF`vu^ zNcyLfF+WpiBD@>@t+a;tb2e^r#V3kc(&&ohRj+Huz55d=k@=Rx_=5>G{bEg&vX)|D z;0Id$1m~keY5)NF#F4s!CP1uGM16!-V^$Afa&<-ltUnV=DU;y}bzI4SM?$^y@?r!` zU?tLC=KDocG&HIUkrN_{zn>nuz5MG?$`wzAqak>L+nMQfI1-0Kqz({DP5Y7o9*P}M z>D{Fl)`fC~R4NV5m?LycEFh3|ru1F{|D4qvK-mC-U>N|l1=9KVzf1r*fDHuvg#N*| zr~(W$v^DUGRt{`X2p}m&z+1?Ypz;%yA?vZo&3F&{2%mNM{a%4q33Auu| z@%Cs6X%o+9G55cA`OIULakS1TV1jqm0#MRo0r0AF%wV>k`rCYbLJ(CiCrO_sHDj_B z?ez{H!~jr&vIC&OmZT5=1rhsWTAGk0O8y zBx>i*-W4^WJmsZ~fN23BA^UcaZLnka?BuyE+j-n1gFwtgOkx-&p%lgs?ihc@EfH#? z<-x{*nzpT5>mE=9M1+Ow2f3EHOV-B_GlrJpo540T2)1^XUs- z6%g9t^P_$+^X>>d(3-425PHGvZ zj*dRaJOv4K@7ZBLTubEO`UdEJIGh74Sa*NkqsS^%aEq z@5j<16^JSt=O~m&ErsrfIU2=*!&V26cmOd1VgP_Xq8`3GhmHVr0TKX^2>e$7VDz8@ z06hvev2I`hl#$+r8)G=AH9@SuNL>_9-+D>cDHj1~bV0+pfjhPa6uwLuuj<@@WcCyH!vQMW6#Wj1E79*QV~Fc7B1pJ+muoPuuS#{bI^7(j`IKN>yN?~wwK^&f4R>hknxL6J^$9+T$;doLGbu{d~ zAEq!zu;9`Ru{2iDnu!Z3{#!xNKz~VQV5@cY(P?P2evt%-NR>MS5NXddtJqX`~&BB6zo45KrI#%T}0 z&(jGOK(GU21F|kgltds0;ynId*9)lt$^2hmY-a=zZ2~k2OhFTp2pq)>j5s~SUA%^6 z6LbX-GY|w+R_@(&2w~48yTdEs^$-Dx2$aG{X@DgI+pmw8RntpM74YLTpMJXg3>CxO zS&sIf$4NRw>2JFpyw*txvbZFAxZYfni+BhFu;`EZ$Q&xWRsPM1JwK>830YZX6l@^z)O`&#hh4Q+Uj{%lJqYJ! zh6Q9Ax3W*q+IU<^WXwDlC6C%{*`8S&fa?pzqv3d9G~)wo3_e8$5c0-pbJQ4gG9%*f zP~&K-#w#`u5TV22pBPfFFpIe~tXn=?CICC|Q>*|7AFg&`wHi|iP;bCPY5sH+_8a_7 zhOTQ84O!kG&+jOU^y>%1Zg(Z|7ck~h(-$@TKdyM6+zgV9&PgeNDb^@~qH9M2N{wTS z*`R(NtGWov#~9iD1m#0am}ZRJPG=Hg;^FS#@oX!XiG4vGR=J-tKL!9{je0r-`+n=| z5(7{K%z1(W0FWszPSrSz%DLy6=pg$AY(t+e%a*}j_oEA8j8oQev}2CiSKK+_244<9IKO_Xfs%LkDLg?pohHy#I9kcfwhhDt`7#gk-~(%Z8aXZyD-=t=6bN!6>$69kCZ7S zH~J_WY_5E_yfRu|xi{qXdBQID=ukLW?k+D3K4O`wH{)|{u8gv~hSd@==qSpIFptH& zak;?1LpzemN(TgmgdRCM)R1B*iS4{rVSulM>4l*0<8u1~1@Je0K7h1vTw*;c_FVDm z5@d##Va?NZv=cAOkkCAfPlPil9I(tLyzEPcleK}Q%Nvc3CJUl*J_5_$3|T+sW;y+1 zh8-v!iU+(=%>%ZMndO#aqqrRmmiEd#GWwMiX4H3*&v@Tkc!Q0|sEMm7iaNZNLkkAVG-M1EI9$JhO0N z7y+J&@TB8{y}Iz?`fY{*s;872W_Z%dA*PN<+XaRJq#s46ms2=~{!$vQrcy=?_kFh=q6W(+M zNgo7ZC+Xi)&e5Y6pYW1a%QFo3U<00Ze$FA3$2N>W?f-=qLBYLaWA+_Z{X)KdbiUb5s00FiX?nIheLglCXqbgc zx$t>Mc;Kh|c5gv83}1i&5L3t#l=J^v3cwYfNlH*Lo|oI-e1roD=nJ`Bl|3f{-mnWN zF82?496eDU9J*Q)F7)!o_Ii3k!NMRdc-}(KXkoZK*c~h{uPHD0gmHq2NWo^`=5n01 zyEX!B7#|i5SGKz$lQHH0-Q}bbAI9c$6f_N$e$=o_dN0<5uBRRAL z6c=k=5H&~qn+$;H{?ffJZ(Vv?VXX23wJlo0D`mbf2@;nzw_oFMNKN=a36JjX{-`fK*Avi+r`BEBClhTVGZb=N97GZQ3f%22iV(+h!u4O(JLTJ zBEh~EbXX9s>yrN{3((dcdIK;3PNx+g;$| zc7YH8+Hmg49I-++^5Hul9cu2aX4*FozI^8J0T{it8UaM|u1Ns-4xI7u`Lsn37cmjx z#>dA$-Hic&F2qR4$#Y@fEgG;-M-$*Q1y)?>V=KwcBS(tXl)FQ$O>}!gUT+Wk&Qm8& zxHGBl?hxB28`DGSfkH16={>=4u%Yn7#$aJhxQ+OqJBjDLVS<2iIB|E+iAu5tk_YN+ zo@gQz@PZ*y2#>40yr41_JmK;LWjM-%!Eld;MaJRP5r#DnU!eCC;?LhYSQ>!{EJ)D7 zTHs1%7MV{P_YnXjhpq-RTt;tKYZZblM8Atak3+=Mqa+euL(7BOo)7*jF<*j+$Krn| zGY2}Tds$at`+v{~kn*^nkRy0hL{EA=nWZ7nz8nt(N_|W=3@@igeFfM?1|B8du4Fop z32440Q9>xGVd$x(XB0ZIPD^>v>2M8%1KBseTdBJm1cYGm0b~MJ3X~p@k-uaCc2t?R z;NSU4twNLu*-HUoz(H6{R3Rf{W53f-FdSkV;O}+^4E=6v225xwn-3a*btyzR7O{^- z-t}vs!gDJQGT=*%8Y>jaWAl;>8ud#bmY}WtVVW?;16`AHC#Mx7#ZY*n3bYwwkVK#l zAs`_HlSDZeCOPJ%N&a!i5(V>}pdG-{Wen;$bZTmioBnBO3@`8qJ(m1lj-9fd7(BKA zuP?uZlIfFbTxBn59ASr&XX^=p2@T!0lrKAP-RnOkSseFg88W zTCyqB*4P8Lzm*X_6aiuVl;h*Mjo>HlA7RJ$D=}~*4?g^?`+=A~GXTzGY?^Nmll<__ zKoex*)Vm)MKs+$~EAPk4JIy=melYSG#G-NqdvZAffX-|({0?+K5(acg1Jr@)cj3;| z)SdJ9PHM3)@W08&o}D}E>lfNFG8grjF7Q89wT+d3JDQpf_Vb-W-N(nz?8E;9c9?ym z_SfpdW_uw8gf=By+H&R$qr!3j2nM9}mz(E9X5tMWK=6;v(3igDpKB`WJyLYAsAx@q zvFYaX;m+Lh{2q8c4WA+dC@f5a_`=4*U>(Wr&_El;V6=g-(aS*UR88AxTd<~_YC!bS z3F-&#pl35%pgdt^m~do-;WYr#<%TCr28MVKhnGD}6@!4+2++g|S`r9fs!%ZoOQPpx zt5iX))=t#Arg_u464NerOL+&DY#oqv?qd8ia{2c&TveJ7Pli*1KbIW*blS+kH*8>fwK*W++nW

*?b;ydlnJTZn26expYj+lAt z!%?vw?445zAm;yqfDc@`-FAKRf-`8vowiI#b~SxO4}rcjD`3q#md4AFPy@gQ96pBu z;NmPX0QknVaPjgkK<7sODMJ3^ch3Bl_)yy->+6ATLxj)rex>fvbp-F-2KYyIY=@s>{_^rCJr@oCDE7L=2wF*iE8K4+ z->ZLouJ*o;xd)E0Kt=sWbL~gFo)W?zCBRUGfr;4FLG7=jvmW-p9)rPU{T;djwYD#} ziYxVxkpv(RKpjMDz*P1&I{WhuvgJq$KP%={j=+-akBfmXhk^@S4yGcX>I9eW9nPI~ooK zv;d%xX{y0CuXmukoOj;9he1!>Xkmkw;%Z|qnpX^^p$iP|W>5F8-Wr_^QdTe4)0$OiTTjDluT z0{P8!O0H2|k8Fw~!RroPyS0=^1qxuvqR{(kIKD7(zNmoIs1(BKrP94oq6!wuRuT`y zGv4rYJlq%xF)}QggfxW;P=J1$2m>jwSbisXw`8D!$O$=rL&E^*cTB+5tF#9y&;J(# zV3&q`NmPvifNC}aU{4{N;6+#k1pdp3yi#r=8~xXi03cb`L@>=|MLOxS!5~~?H;N=s zA((#nLEj>?nKhTIw6)*1ACcnIU9ELUDygVr$~J$jDBWL?rL|t;R3=)jM=d)b!ahD%#2mt*)ni%Ay2*N!t2mWaa;9h&+1x(2-y3*?ETK+Vit87-| zf49@t4|IY1hz4)y(6`G0qH{o6WFUDzpInIkpTjns7%8s=|Fr6MVK=&=1>4vh1}C70 za9+Gd=F_rs!@F~jU>Z>a(+NNoAJ(rkJA3M>qKmn9g8zk`=UY0b7!L-q_m`=u?DHSB z)bBihk|};H?B*)N{C0J8$oV^CgSNFyEo7g}J9UWep6a~plZWUA-d(Xht72aT6adA4 z3**J&p$!K3Dg{dDlYf9VA6xv9Z$p6W!lHgm0HlG;0!={1At-*JooY8BMgG3`*91HP zB6D)4@<2BW)OrGig(>_yX+8^;D&2)`ZH;v`-m8V(!PICWc>q~}rry*wjPlO(^2Brl zLBgY+8V?Gar*n;IdoSbo%*lDthD%yXlhKeZ+%n2wlj33P>W+KCCEE*?Sc#iaw`5MDCWd6{Si`NQPk{9xO`9g;EAP_?~jMJnwFMoggm%rWjlx>46!268$ z_#hqNckl&bD+m*55`b2NuBaD~{+t3_q`zVcFg##=(ighSy!vxIt0)V~H={F zM81}TOha%F#=vTrVSz;dq$xtsZ>yo_v&=k2ACiI>IYik&F|Vp#;lGLOhvHx9fyeYo zi$XF1SHN>6EZ(gGC#)dHtIi~)54f)9CEW52Iat3Q7^TxPq2 zP>|yP+UT{P|85-8FK2i;iXg!IIDfDL)H+t)&CqY=pnK+k-QM5-e(VX;H);2h0GgV1 zS^S%(`iB`bO|?)ux%T$$^WEW29>ovTJ+d=4cz;=d@XFpD)YZf3{4_rR(X+$Md zDL_KOY6#8)4`|L0U8qq7fKmlIA{rQC4)T92kZ(T8KtS2yY{!YIHfDWWSK6W+`^RlO{G0$W$kw2?pB1L;D6 zsak>4(-Im7w#Im&(36)p40zpgGX84M5`+(-jv|zoUc&&i4_UrOr~v$0Z^>wjJo7-i zD=PZYA!BHriD-}{rm$&N<#)$eU?Om)?+X10_Seo}G9$Yp`g0`!3VG=A>I{nIzw~pl zF;6_(h%gm%HVfp5ibj*jAsH0hJ4|on^Zs1n8NGf8PGdH2@NE1D#;h>Jep)ht4xaDXm;MSftP;9b_B?;;jXDK7>_^8> z*KdFT{wOOHGk_F?OeOJt`z`o@eOIqS1t1?&sd*&<{y2x)-&- z53g~c_fAjmBLCAgTtb1bjPb!B2;e^GA2$F2k^VlI;YqzmW{HpjfUTbAT*EIr$4DF{ zWvzU}eN+%Gpo9n)h(P9u2MGKI(1U^p3_xH;g=Sd52Zg?gejeg` z&%+kJ&_PAecmmun?+vso0R-abylJ|DID`JY7deGMK1!f_v7Wt>r4HZ@jkp45xU;V< zF;#&6ClH*5AP~RmO&6XTY#Kogsn&kaAEb*;ynhG>bpoPnW!w^STl!+DMkyty6 z;K4>O31EYkIR||TjOLha^GrztH=Z2wvG{bQh+%UZ0 zw6wDFa`Wdbe1(01tPBsMWFy02e2}b5daQFGoauBS6Y>M`QPu|l5x@WS!@XziiC{jk zM+Xos|^0-BjNe;@}s*1osyP-{+w5 zf1Y>Afxq+ldhJuy8#db^@U`CNdP7VkN}uGi4c5-OBF!C zdoBS|L--2|n21NuO`IbWq%D>HK})kxR+~0}Isx{j7}{J-cgOfe>x2n{UXMb?PfErO z86-rQX{NO12j!AYALe|heXv)-ZXwOSx_xSWa;!6)<2QsXYd9#0(gc`b7Emwt zfRXX%K0Yz*XD5uyC)JO2F@73!+Di*kK`RpPZzM-1`!Df*T{!<^?W5fy4HZo}q5Gvb zCZ?|rdfqRYO<=`49RQt#(IPR%Ai59!@S}|f_3lV6ny2HGnkrPF>(_sB_1Evw`m=tk zpgdw9f0vs4&9!lk-E&xm(tD3y9=~%=g}qtmeIp|7HL}IbG_|5h1b2RJK6Bw7%9_tx z*gOPyR|5h2b}h1jt&;G_?hpqh*?JzGGW^PW9hA4!iGQ?TIkG6p|e)JA;$^ z`>67DKHoip95iYqL;1kZ=tM-Qwsj4w&ytefuFFlV`K};>B7n5&g2+H;Iq4bykrNuB z{K5pN0{X`%>}=!Eg_GuJH-o=pK_G(}kRKHNx+w_i+wPGUNem^Q7hYTZ}V z-#uhLzIx@5+yPg}=#=nbIVV%T@3w#Wt37>*?{ktH#W_ZU(lOUI9BDu#KmL-UIj zISE!Y@h24k(hoPP0^|X51LS^s0Ej+IN*O#&Z9Ooxzfa?j88{10VkRJv9nY{7DIS2zNZbq4L#K-Y8mu@KkO6J_i$Qn*KAQU`KCyd#A^- z10dV5{Et-N(eM~@ovHnSmHD}UtgH+SG76*!m>fQ$;us%45;>U3;lWJz+88hZUcXJY zl9|iRw(iOPITZeG4xI<(FZTV!Z6k!As*|LIp-mT_!Jv7Iv)BN$GI*LeZ)8|Vad6E4 zhYoz{r1^@Wfw2f+u@KRK`T1Ft!#Yl6=&!MYTvoV3z2geP#j%`7&F|&p{%=*Xex8@n zBV07xsH=?>bv3@v5b~U~)czoEk&ui8uo&c=hVw6;qAV58u}2Vq)_$;_r)I)6Rky>4aNp7|5`kUzaayOP_I1XzBmGK3VamB1S? zXp86leys)V1)&H-XUxL}AZd7mnJ80?=3F7qAmA0-!GM5=TkYt91hVa{18O25+kP&U zM>I<$1a$zK28s<}yut`_lK_gBys3{#fyG?_u?0b{PZ@JyS=L@<5@0+5O%^_V*kYE61p`~z9 zkBsjV0|``Ob(!g>2ND;wl(xVP%s6r68$;4nWr&CRVK+xKE*|3%8t=@?{#r*n9h6mc}#(gJCC zhCLxhi@UqS<-47ewdLJ2y}h=sQuYDoVv7iUp!JI`(4`C74ZH+x^6aIm*+o|a2_QBw z&=6Z(oaLdSbm%cph)*K_8W|yz48Q@o8)JZ>ZN6n-al0XgH{g0bZz6XPst7QLv>MD< zE*YXc)G{zcI6hU>$QHnI0-AUEYY2RJ`r6s63$d`XEa`U6XndQEi&HqvFK}ULQUSeM zk2(l0l4v@*bZKBT0A`Gpi=lHZQbvmBtrJDx$`8;IsChk+7uX}_L^Eoi+HsMa9ZSAQ z^L;D`u)EHRZ-A_8tN1@G*r#}gQ|4>IWHY?V!~@ZNJNwIsJ0-V z_PlzPrNYQC=9l#WtUpSbV4BjOssS2cBFa>e)70riM-&h<9!TVxElytOk?{h-o+bJz zvCo)TC)kaLR~wN^2d>MAktqI)biglB+rtGmFo1wRbMourcJZP=00I0iPBn%;Wa8fx z08{#*39K%ng8ztX%>?Atf00|`%$G`d`RtlJ$=gAf6ygmv%Y_5is zu3u!$W%>rN8rgO&%QlcwfHDdCm6Vpu;f-H1+dI+-`;BaPO~8x3FCLG2bOk!ef+JtZ zAHTi&Rb6JK$ov{f8_;|dsAK>I^ds)!hW7kb>+?+BDFga9i`qU$HQF=*+%VrkvB3S> z{L#R#IQRw`AlaxM(d&q;p>jaJW(c2EbUACrC!#mb5&pz2=)CrD{;f!V{v5Sl;(um* zoKsui+lNNSe?|r#8VyLo`|{UF1aDyDE9@oQ17~1d0v}Wj-OTN0gzmKr_l`4EB;Gmv z56XwjG4~lAr-m5kiPR7?Kn*j9e|?R;0knYEXyxa*R{Hn%!T$!)+j) zwura^q++udFarb_D0%_vz&DxT;~ERM@ojwHD?&0V)vgb2h1%R@ZJ$Ru_6dKL^@?<#!y)5#m#bj`8>cY8;jn#` z2c!+;bJ)=ZrAi~y10VNOxfLbcHj8rZ#R`$jSibU*0hZ07L1H>Kj+Ui}GQb7^!yY3& zdDk0O%46`Wy6vD3Q%L6I&>RzzTwD=w-&3EyS(E_Cn!Ibtal{vLeAkF$D-i2x*D8 zdAHil<_=IGI{-rcXuI2{)OjaJ0DU_BkBLGw0WblM69AZ>s6TxA|Mg>0gfIss0TlT| zeI@~HDg3=&9sk5X0~6fIbZs=FY zO4q3(CeB@}>9w&rQ{iV!h{VI*AwG%7EkU>PgdUN1%`f#E)CSxJ2v9|!xKwgBoBAT^ zJa!qnpdyiWjvzF49*>K20HZUo4L%@_?mbNhd6uvRQ{U4(Xf&vI9^N(+0&xS1$ex}p zn=f{Pk^0huw<+|^#;|6ON(e;#CeJ?BnK8>ku$Pyy)CoGrH64*9-}pH5IaYLW?D*He z-oE4_w%#(W$DJFjp3dNa6P*k&idzr6ywLZ6`9c4M`{CWI1R(o974wYgf&Pw;x`FOn zD#p#}5A0vNHjJlF_slhzKuQ_Jy`HZ#{mq}@4P7f=W2a|M%X)-AKmhN9t+lnRD-M-NP^Sy;W5Fv+PTO1(?JC^uvy-OC22@+YJM;SVMn< zEA=8po~VK|sC_{b_9Ff129O{Uf?TJImO^BRv)ic=hr_pOcMLjC_@v|-<_$H+qB7JH zZ>;A<>IA%fL$OCHSSI!>ln!t&B6va8qwzuxaX82-dS7rYxa{@edkYH= znLn^PjV++i{mP3GM300Ei=ZV zOx3~*;Vx8KhIc4fa)J@=Qmm4w7%OlaBELfWk$+KM^FVguJ;|JwgVQ-cfZ$#{0OI#` zluwp3hjCB3kgksVbJ?6f{uE9SDI>2X3F{U6Kk>u%t@OESCqDOHs6gx*Zasy3sKtY+ zL?OE+jzn7^3;q8~Soz8qM4p~(@`)Er^vmRz;OF|EFo@*;qzq01c!w`_6`=tUfv^?S z`7doDV>Y5%g7^pYFM@y&9-ECwIG5IwDd}afE)gIp9!kJZ41BS0VdDH2z0-vL2MD3i zb^+soADy{=P85(zr)oUrBYTI^I>!}-kt@G z28vpmcLlaU9ZmYpN{?0rp#N%&7tL`_=0kLk@hz#?Wuaro*TLOTlPT(83 zS>y~<0fv(S82s^EGeA^|;4z*J5m>My*1#SQ4D#x8)nNHWNS|w*0^n~t|JUT#F*82O z;Vge10}I-0Ei;-Az}{05kWUAlgEn_)2Y3$kzWN5ie6^ix<-<=VYs+loR0aCx;WbY5 zwc+lPaW;zyf8~Uh;cmM5nHh!wloI8G`#bkv4A20)*#G_h2Tvv^$wJR(T6}glj-H*J z<>lyqwoH$XPS5;%rkiqv6hIx&P58F}9lY3a-5yw(L-7y8pT^mt*(zp&H#-3QFM)ed z$-Ba-6i0cCPJs%5R-aLzsDFGEahUGkGyoj@m+%E-X!!A}+xD>$)JHj%?I+yzIK27n zKH^l-DvvlF|E39$P>(E~WfzW`hcjptL2#Mjj4L>0&cgTO|fwQZ_J%5#6FYi2{fw0H#k- zdipD$exu81!>^)hN+w!1s-~P)tPMz`Btr>6a-aYJ?@zGTkl6EsSmdS%*SyDy2h+i4 z0cS90F%0v3@B4)Q7A1Z#Xy$$F0{T4+)dYfAh<+rP6_5>bW;-fsmg(AG#l0|%qjKq< zx_ULx2X(qVD`6m`2-c&`W78VwOSJ$n(~Kkl!ve*+*$F}`xY+ueZ6I-hI zkV}FHUp54@`fJ43DPu0URHV@8|e^;q^hla)Q^E9F5L&GfzAieo{*U zXoV8Y?f?~ouzUg+o3HYw76t`JyPbn0FYeRzzvxfxcanV$pb))PiH|XdO#Rn}TWj5& zrJ7C%p#WbzBJY4SPLGz9G$9Dk4|0}57vz3wXJEc#Xm)n!!r7{a9hcDcqMq(20T9fH zCeOc~>yRF>A^S-Q7zuC*h;tpw0~;nViJZo521u2db%s3hP*=&^-9XxboPgk=O zhjK5if7;sP(2Eu_LDWy$;>0{I=WrLm1l1Nqj0Vp z-0GWRSE{YY-~nf-+uO}LZ<@Krf3F+HjmO3_zF&#R@=~Kq#ORfY}kWzyU0Y0*Z%ArX&yW$!NDy3;Y&A z6bTy3+g$4fZD+1uhcLww06Vl2Ve;GfU(GMsY2o{-WFL<|AmOrFfI9$(`fa1w55l8O z9&kQ&3Q`9Wy^r`8+h6DLrrwQSFv&oXZ;tk28ff5`ryh?6foB~DuYy=tF#!lTP}d+$ zP;rj+lZN!zC9MlV11x?6Siq*)9W)C>N+Y*<1aVTq?boZyI;rx5O5I zCW$yR3Lpc3zMHgA+6_?;JWkv|$@59$lh_xWPyBax4-YFevHCzpx3ruCf0t-|{uc1B z`}y-pSUu&v*EB^mGJvuLZ7DA;Ei37rX=*C7JLu0b<7NCW6@YGfe$@;*c2czL2!6og<O6y8X z-L<846=fa|acv{+y9o0d1%A#N?YexqlFNK>I%`M)9tFSO=E1})5-BUKt1ET8OY7^f zH!ZF09BwV;Kfk`dwY9FE1h5b$#ket#2QEZoGU+ZQIh46wkEISvrGP#}m{2z2FHlH; z4(+S@dI_vY2>?PH?qOOjHKr29@Yf|4+XI<|V)O%1P%b6F|11f+%il_$tF)?vHK-DK(`hPRn&xPx0>&|ZvZ#gw12XwT19&R~rNN38RYZZzdkXpN zRvr{E(H70cNhw-4lwM!q%&j)>^)@pDAmV$BikFsvG{4jFf9w5kwttuN-xL4@I3xj8 z`|n8qQ|y1(K#~LJCgBeWh!Nna;|?Ip8zO`P0E#OPX8*@rW*;yO4~XVous(@EiiqjB zM4pV<;$4E?82oWC%CAp49MbFm{vkH!T(Bi4CempssnG|(y0Y196nOknYsdnYwl+xs ztB)`T&@Q2vd>pxQ>?6Hsy@yyXy8Ii&9(cwK{S?~NaD*-w`haTwb@Bb84I?%$fDr7^ zko|{@&Ywbh+jIhE{4ZqDhztNCNbkEgbYM&LPtzx%uda^K4}BDuBXn%NEnMmXpmUgIfEEoHWvlI~i!&mMM-j8Pd6 zc8;ypPCE9-_PJ6!BPmYxv5^sEc?iW$2RNMFfpO4ttqDUXIsj?3l~j}~D-@T9QcHEM z`*SNRLt6R;6CmepHf+w30r(5->*1AoS_Dpi!WYF5;BRr@JFUuer4yN-I}8Ae@qf9? z1{$cDVbW7q;o&J3`iY|gro%S=@#i$v;yP;`Gq@!3_aBVRzRu00*K#88DQLal`Dz)W8{>ZU?#BgxCc+2rUP5$q-oD0K#h( z*1`t`4`|q<$mIcz3(TeOkOAr5-~TEA$LK^WfVXvei}a`ZfqS)A02N6IK*@tU)k)}> zM-W_t0Ko)e4GrDCP2eAYYm6V$z|?ZRuA+t)@94Mz^(h3*rp-sYWB^3^o($LS&Kz)l zn-p*$_pY__CgQAin2ca3e@4^q4&Q9bdA0ww63;3IE<8SPgxa;|yCv-XJmD-KqyUhr z$M10bJ)oqQ%Rheqwos`LNUW~jH?rS3@?wNq{(k?;{rmU(@86HP?!{LA(Lb0&1JQN3o3O@gGMXn~Mpc6zG8{thV+%YYCos8&Fx0 z7G|sOnG;*jE$>ERiiUF|;;RVz7%N)S4jdb>F?P!QOAo^HUH*f#_!DB%RJeQ_| z{32Q6ZP5W%?{d0$oV;48|Apy^y~`&gI^Moq`{0)fr-P5oIIy+}e!fz|WchF>Ju3w| zq+rOvZ0?f@zz^{BZ7H8QsO=>9cXSGizc)z&DDNKLr9(J)B3A#)-5FShD1UwUVRvnL zryYOa-jcHJ(pI;R%9j@RMSuUw+#)F6?9d!Lc(J*~&4&K_vAGx-;YDi1?F*QE z*4CDGLKy5Urz4?;Rx((d9K`vvg5$QjvZ6Lc`hdXKQFU{cB49DLvN$xfxanG0t#_wZ z=H}0Hf&Kk&;wLZQOQsIc2ZIclrU3SkgM|dU`~-$iR5Z0zlvY%fmqym<%ZXq96-^~A zW&E8s2LClw1IhuFlmaj&D=&8iX$jON)<`d$F%n@G(G`lg*SYG+0u=k@-O2>~Ofmp9 zK17cNpPM_xEM)*7JTOdixzNynq13{{h=u|#4J2HTT{0Ar4jt&;Z*GSdIJG#o01xZI z2JCj%!abga50ou*Um19ni*67W(QG;x z7oR>;xRS1{4(8hnnaW`PtA%tt5%jPSlqfLvJ3Rby;ThOkv>?raXdo_Ba0^KRrUBV} zn(7~`o~b~AH42E59QplsIBj^9SpLv() zr~U`_2NUF$fPrIUK;Daj05JIEL?qS^pAt8`)tcI8i1eVvWS>>e%I4-u5;eOJaH#!A z0&pv%sbJI!N-RtoU^;l|t3g-@SAv!@OwRwT_mD3iK%I=r9+4~01=G+n+G|*-4;ad@u4CTbZT(mo2J%dXu$t*Ni0LOJU*E}4Lb}9a0qGFe z?vEJ(7;|s~84~463vvO|Y5*4xXcvHoSO6Tt_5v7?GpC*mI@m4nMjXHkdP5O`i*>%-fh^Rvw0YT*Vv z@ET$(u^bEYn$wBLG(azaJFr z*g`Q=<4L;hsS(EgD+U7SzyWw}alWI2K42jnar;uQX$}bWPn|Y?AglqLSHpk$i`=2W z|Dm)55D_~KjW9ei=`emGt!uSl02LLbkw_`W{}M>P&)rEA;IRoxCAyUW)E$mjT3yb| zmoHmX08YAr8XH-G1rt=nJ(P=L4e&E`t^*^I1OWerOS$XrI+ejXBS9sAQa2?4Eqs^+ zK-doa05Vc+rNMQ-zv2GM{RT!F0}D(5`WyQDk(zAKD3I6EZ5L5aYA`qfw;Oo4VRImc z7*Ye5%XQDCXXw+t1qO^H03SMuE&f+<^m3V7QBNj8>7P%g4^j7{dCypZ1O@cg#k|gT zMuN^v;>(37Y{y`}hsr)qtB>h%E)VbLtFN4u8OY`2g)|{Z^yRbUiZ*KgIOTpK&&nhY z;j#xZte}E2z?K?-Z?mZc{?F`>ieChvi<$wb2x!l?QwY5K4q4!H+8d%4;5P3<0F?lE z_WvBhl>i8Tt^a5aa4sQn;0-x|aMDwTfB~u{)0~6)Ut58y1t4?dNMKBfkEv(`7I^k7e^}H`$`{p=pP2@S#D~?2W<6{O5!nJ1j9b^^W1`@H{`8;! zD1dJtQ~Zp?AGd|$Vmp9%$T5A$SjPbykTPTgV0c5?B_d|IAvDks0Tzb{^CHB@9|`@K z?!Z(Bq=p~d78Ag{-DEPzll1&e>Vj=<3~7F_8gj2^EPn+EA8%6qV-diBa|fLE3a$^H z>>e-;M6sV{+Y|sY$XP3|9o8QI_^8mo-y!m^nf%OfIc)rWE~;>H!0!hqTT5HZkB*=T zznOVS^*;=Wr?=*s7(xH0Y;P?uZ|Sa}ChqLCGvxxKXO(AmerUF1b{@qearn+QlHy`3 z{V#T2yx{zIU|lrG!Qqn8(cQJN&M}95u(Oq3 zz|jw}ym4wr@AA)<=AJ1II2X0TAQ(e((+(m`u=k65@c!f9wkK&Ia)Lsvj(Y zJk|r*Yo>9+RPUj6g{`ceeVxA2h$q+uAvg&D2ok6~U|k-+Oi~2_aFPkg0Za!7f4R;X z%xJkTv|L98K(N!jyVt=2-2B|-$a?8|UAfzhS%k~(j?`5!O%jT4_k~^X0Z}E{jBVa? zEj->FxbIS#fW>Hoi#y#$RRgO#Bkm1cZEMtFZU# z%P50mA1dj+TpophtwX=XImj1T5(76Z3hep%Q&Pb@Um-J43VZ?{&?l4!*aCn8s7nbz2!N3m zQUwsro2BN2VgBVL0jNbOG7=@gL^HbpmdY}YYo!113XoIg5VL?@^2KO6!5_G zvI3K|7bTYHJ}~}d69A8!w1Fm^tjAap$4vyAlnvg3Nx6%$z9B1Ey~#pQJ-z|;HT_fI zi3L#P?(tXfNuRa;r+grumYM}jMT8&}1wn-o7~W6OKT_7wparqm+_qxA81^wS18y3a z4-ufMAy5EA1(H9QzCcER>*mebODqfzEs9X|h-}4ts)PlfJ9pkQmsmhYTvPzR93+}Q zI3RRcw?VVN_9Zben4&OPYh_8ld!|GPyvTo~4Dox1YXRVh|FPlD{T=Fn!Pb_Rrjnxr z^uCVP2KSi<;Ny3h_27eDqYu)=!?lEzn+aQe{X%~~834Q9p$oIyi$p&xhDMA{2L@Ik z1?-GCTn-0=GZ}mN?5*8Q0=vuk3rA8ZY&!?R3U&t9x{)0#UxUD~)`Z)z=Jz8zu9bQE zA;`fe&sW`USg@~0?EC+CJwJ;fkkJZ;>w8j-09*g80$f5(lsA9?>I3vI{opH3OnE<=|3Uj&qXo$ z4NC4O=%>Xe%stL*VFP9VLV>QH@US@S``VmVpe-7V_w+?`*=(A6fVgKbxEgUmo%nh( zxgk-AZ>6ooGiNiswHM5cyg|ePCIA6=y(j_|SWKh=$a?vzCwrzR+tb!}>f70#TUmfW zOv1GqeCo%aKZRj1ef^iP|5LzI_scWb*oET#a) z0Bn%LKm;JXu=EEg!$sw91VJ)9iF?>vLVZ;m!u|)+$%?!IaeTG}>f%TW@Q3o*Vdfm}M$IghZ@oeLzS#W^)*c{b+%oUb550O4dxVzm= zO$-2}5mMVZVjmkMFG)4Bcj{(zmNy;ELb2a$|L}s`W%DzU$4w* zUGP&{4+i=-RRC*$MKfW(p!?UY3snz60LC2kK08B5T7WX-_*4VH2YRehtv{bn+76=(2$KM44M+e>J^)5zNe2|_3p*T>zI8wPgs^pLn*IJt%yFH;bRR3vT~1W- zoz)wMFn_`BwnzbOeTg8dB3keZZ>GiF6LOg&<=6;sO zfY1TupU4eWD$x6iRRGfvi?QIbKci%MVeWIIm;{b}H{|qAUtkOxHaPNg^c8(QochW>8R@!L6W3C4jJ24TyXay|4)R z%8DsM#@~d`ub~2TQAe18u3tGVkV-70y&hmeM_;0en@ML*QNpDMOCnW#!TE|F3 z{{lVjW{glJ+QI)=*1Obr;lk`9z5gOtKYi~`YK$;XK@q^=*Vj5pUjm9yF0zqQL#<{3 zlh2_l=mD)0uXUD|m6tkp?(YmN%1RYD5nyMX1dzJ_dTy5bo+$u<&ghTyNV|`j;AvC> z&R=R2GYC!q+CmS*^b>Z#N|6x204hS_{FlP#6*9vR-Yf!zI6w7DVAWnCGdP0LYg(!) z&9(_~pS-)YE(?F{kQfVLT5n82MfMY;2b)$DB!GPDNC`l++l&D?{6l4>ApAZeS;L-H zJHn)J1Y8ZVfz+TQ>{?K3z?71xCjoG(^Pt1dq@)3|ph^O>al#E?K-zy7D65MH_$z6- zeDyrF)rLmt_3B(U;XjwFpi*&qoxvW?aw~ZFG+5!};3w!+!^fQyvI@ty1mjQ1>^{v0 zS@^Y({k5z@KEvrAbmoG=S9xIvg@6@rOFTndg9B2#LY4(s(3jY{l7&x`MK9SPfPw?s z+qb4fxXm)hH#-1?vS|a}`NS9G5iaQO(_i@fXWr2opdtUj{4J7$Ne(!O==?{H0VBBUKu5T9G!u&L*kInNE|$<;vmD+GN^4%H8WOGLOg?EFjcsj(j9Ae zZ0f)^Cor)u?`N~PDJBNXoAv9wE2l&Aok;cA0a53u?vis55=3atek zpZ&)-WYz%yP>g@Ed3cst0Hy#?ejshrhlI8CrS3rYf(t?y4~Joc(uyvT_|-!%kOasv ziy{DU_8Uz=zNj)I0WM(#1F?baP2mN=h6VZ!VODd2LAkg`@{rI3F*jaiGB1D|6UC&KcH_ZKcGu+ytwb0!!Cen zU_ZY(h_G(Q{W)^XdDHo;%;$gB5J{5e=mLon#l=QRhMiOk(6Q+o6b9H@YZr0%gvSGl zuPwkwunF%0y*<85D_`NMAjCs{Pmr}5M#3`bQ4PRLOS|=^yq9Mr173k;TF3N$PharCcG8Pc0&&-$(U>`vmZUmV_I?Hx`%)Y|=*gwGt{lcuM$01-j1*abvH-pAzP_ji0SVov0wD0e{rSE?7S$x8Kii|ypLa$8 zpZ`3=zR#a}N2mVh-+A}bbp7uz2awae@eF$xHUKCf6@X|!FbCh4wy0Wwf)D`Nf{XWW z>U_B+XVPK<^2WHrPX?edFmgarKi;EIbLIr+r731|7M+X8d@N!Uug5ca)!@XW^1^TA z(`h_^{(fv9=))L1VB87NUC*lp^uXpMhB5Y3Bf&TRrsyuO>fh`M z7Z*CYMOx6pze(Sm4d8u$(qgDt00O`wFtfgOfd^1VfSJh!Dh=u0i9{3d_cQ?2=x){V zSUo$FI)3@K)*C8vBMtlnN_)!}pf#Bd%2cf@6Rdz$f1DDwy6#n17L8I&}F$ zpnR~y!ztJ8{!L+jAc2e#xxoHlQn~Ar=_m#Uxn#~t?xln2Xwqu4f?MbOIre+wZ46cZp1Oop7{;^Kell97du+4DziC@_Kr!J}qIQ5lN1iE}G4D3(W z|L$kR1CnDX(f_V-2&X0>DQIQ@bwGmv4q&4K2-INw$W@qc6fYI%QC$r21=SjrPo-;$ zvLMGalIoCpgaxFfo0>!d9AO6P?XXaI@pi$G;~3;G%O(ribM)T zv1zn$9;P0oT?~aPoYTF{=e{NfFd1)A0YKa@()Ng2PF6JoV+u1}t+kV;$=}&e*?q9` zN8$gLHzDCC^)428=mGY5aIp3W?N7r0LlissIZY+gZ+7{SYs6`{1Uy@t$~di0_r&oNM3yhpNjLnVHJDIm2!EFfYW{ji}9lvB2Esuj5T zCvqs@BRpUVfJ+0gfP91?$RpK?`$VLU@D6EIzsgTi;PGf_ofd%g$0M)lWm|=8E34xVPg?iY^L_Gr<+qH;%fcr4e#wC%$SO&|M`mDEm<{07^$7Oj zo^_v{_Fh=;fXn9?5evW(1~jn`YMuueOe10E9|KV68VR#HOfsS5r}pR50?C9^pHijY z71r#JDX7^rKne!p{?#*h17|!|bBTv;xtWO{PTyV;8k)U1Yl{Gn1D@eoU{L}<>EB`u z07)Q9b1=uf*Q3$v zZ77hl4@?d~GR-@MgjZW1{X1hkBM1&30AD~A0(_$K|BNvLf9Erj07D?)UB&>P_?SYU zfxtVY1aJc4ANoK$BqK8i6cf0RFwss9ljThQ`hh2d7L&*T zN)0kTNn6H2jIqq4^~frH+5*R}jKO8=O|7nMpaz{>AQ?X`TI)g=6gn7GBOqYj{QUZ1 z**k-R1~t!F7u7=@ghYjsVh>2gaZlz=@+6W!=x_i2SLGS4{-HUj$*pjIGcUkKTi8jKM&7z)r@wFT2KpAM;_oadcG<6Cz z0MUYf|M_~&qABq8;vv=1yC>QbbAOLWG7&V;7HyD6078`1W zStSAYDuw{mA1B!abirOgalF&#V-i?*f>4LZU&nIZBkk^#adO{2k3jmO6QWgrL-01f#Tc8Gi|g=na!o?I7*EQo-}J8T3{|1D`WNDeUS zQQ86<2)8dT0682BL_6t#dI zG7Zl}d!q1RfdhdE3c1SMt1TkE0MrIwxEkhP+yXnaIEA^3{E_YR=H=FbbfUsE(DPNu z0GQGwrq1-S8g!+P=*J}Z+d=`z1BMFpV@d%k|91@}a9jbrD_UR?4tR$oU=#sW0Puzi zGg(DE09658Q~&~DHHL)VkU$0!Rr(DF<|+%;Jv(hea=dWMKS< zWIlqw$UlE00zlPiCV$ESwE8>(+^>wnI~ULcuR9!EPBi{*Tt^TD6JQ;l zM$eBL0Q?{5AD1rH0iQ^bM;rhwGE+K&*x5}DmUmTNzGn9PsrK3B6Bn2BnD%sRY**Ty zZw`R}nGFsfjE;}CXiCWZQ1GGP!V&iJSgz&xhormx@WNR%Jb6E9qu?xfCk z!+$#Av4!f0d0KT&ab_VA_`rYyp=5+)pkFYYKyp9Be?J)jHc(e@m)Qp7&KjnGK`DkT zYuvYf*UqetaHbklt>;ifRj+xYyA)OwN^wjc%PW=v8>s#UKmiwBt&~=Rljc93b=snEmr4lB)}Zb+g~~SOg<}@yVn$mD@<<>im_ACeiJh zs`fwQ3=iyOyp+H-T~mr%N&4l#eYgRbXH^pvko;JH$M1VD0mxnG$>jp#1w{7}fpoUb za%%feI>XQM>;RyQhR(UX4VtSh#C3rGX`kx+v{r{{CLV52Y17M)g{%tS- z`26DZkp_VHzwi~}TB^S!1eyV}7{Dw5Vj!tNb3qit=o4_Xfjta(c=f850lY>GNJOA0 z3dP#WmUKy)t9pVGfTlnRAeqcSi#E2P*q`ISBrCz78abm8>bC{8#tlpmfsl1qw68~k z;S4KpbyeZdRU8yhe`|%9^$V~@hz39*nC`+0DG@$3+=1qOx1du{$pY`UhLSHc@>I)KEnOyuhPfiF7sr&6hBY(>j4F2Z{OZ zO3#0Ha!{KAWD-c<@48qb8iloY7IFUSO1F@W{MZ%&l=HLRmKx}<> zuKw5P1bs?Ne-i#PnxJ?Epdtt^h!vn@!E~2c0R8-b>ZDy}5s0kSdZLUepquzz@|Bj# zWz@|jvrf8v9w7mQDm*o2H_+68#J#zo0*fn1xFt;sVJ80|i3=3}L@L!fFE9u{#SP)Zh#303QT1Kwr3CkAVP(G!xGO_>%R%q z>!QmlKH1?e3C1Tf0dHL}U0QP^7YgRRR^qxd7_e&Sfr)fP{s9IEdpwMDPY;8+Xr)EF z-&|uQvWL)xw{jM88i!X3T8ClRFAmp7tpMb}09Zeo=2YL7vw13;$4@gIpTdYZT8%Vn z6q5dz3-Rnl8h{=M!CA<^c&J`&>$}+AcIN$dWN9G?kqItR{S(dN0vF{zlmU!7AY%d6 z009Qj0+d3CZVCugl2DfDK^7cAur{U($d7md0@9rH{LwxDp@}+Z8eKpC14K`>C9(H+ zwd1OUc$Pqdv>!4D)(Mu=R^drj)FGEq=cX51R;Fi_??5rQtp;8b8ud1GwkWI;nyt z|F^&T-e2_`szXXdUNsaU3uaIWCrEA~a{U<+$7hj`7tp_JWYt|{+KyQ?fyF|qGF!u> zDQS`*OsrQIh{RnK{~50h0jkpw<5zf?x^zc~TRXV)qAh3_dh`dYKbEhg3lA-|8;2l@BwzXRD7S z(!V({H+xe;o_KYOi%$40ilr)1PDGN>L2rF)l8&+ za6tILJE?}H(CF{3$E^p?+Jecn-xl?5)KoWD6Y=Z~zJj_kR1^S?K^Uz%RN5_rVIn;tweB;&7VFJ%E<8b;Y?<12D?;lZ&D1M3D4kaX0c{52zyv zN*EO(m~uhVu0g7HVX$@dd%&dxujn8A(uhm3gToU21_@9CD3;fd{zFDfm3@Od)fR)` zE?d`SE61f-M3CYlq6Cmk67~@qhB}+kjY7XU0<~04`QzKJ5oS0Bo+lfaq%C1^%D>y> zbnS46h;WZbU7GF)iRN^egXX^&Qx(8QK$Y=+^mXqN!hQz29=m!+y(JY*KmxP@jxonR2w%TT%YIA& zFTeMr>%Y4;J}rqZxWFt9q5wAh_TT?DdLW&`4SaQ@2#0qIM_jAztgY)BciP;Yll0;# zh(AQ{>#7c>^bZ;QLBlhBc_Tf*N&cMeXgtj*u!An0(dFw7Z9l{oU&j_>vD*Z|-26kx z@$<7cQTIVWY-cdFGYJFeh=hc8A#{d+|4Snbd4~P|A3R~nN5SRxjlm4;xI~&csRY0> zfKA7*+;WNT;=Gn#=~^(Mu;co9nXYw5P>`&}+xLcH@*;+vTOfhOH^E!DBJ?6Gc4 z*y@69c}8KLXiqxnbb<#$>`!+kAo+Q%_!-1MteSN8`_74U^;W(z+7~#y__P2f7-a#@ zSt#TH39Es)qwxJ8|3^7L<81{m3$qeGr+;V;pbHxBONcm_CS$+~sFXr*mLw3(6|Q9S zUN!-UPYDD_8T4Y$77M|A5F4mnZxZ=3Xa76Q|2~63s1^W-b&~-;ClVl407(HppxF@Q zPO^mL#&4l904JDHJg%&=C5fB*KaTttH+g>++z3&O;j-+3s{C&#~FKrM(#3>xBr z{z7>*BoT$B3Diraj!Kb=G09yp%LFFD6%cSkV*$xD)c|LxVL%Q5vF*i8reqIs{3>#S zw>bf0S}Irwvl1Yh|Kc2K*G%_$Y?0e@b8HG8546MqrW`QBfQC*i?3t9s(5Vzfz>MXD zA8nm$!>)z83Ztd>vGqmiPdfp?=(|k^$avNkSM53cnY;Bj?28fPE4zFH6TlxK?Kj$s zMgN;=5rXOQQ4#6XP5}FRJC3s6qh7?kKKNj57#|=|kuJBLJO}C@TO8V+o1^-dVjtAM z)4*;@b?rCdjLmi0-T2zL^tri+b2OF zK6Ucurq8G4r_Qyt)?rOWPP9tMr?f0|x4yHkUdOfx{|FU5l%P6FHt~XWVb(4A58~d& z5EvUzDKC-vLlII!k^w)Ng8{xeF{u9J0sZT^M+jl5IS**}R2Vo`BawOs1)`gB(SvCv zfc^rD0W1d))@a3YMChG?zd+tijt5u*oFaTGa>rv2V*{WCbZ(yJpI_#vtDG6W?A_b& zGW)+3Pg{YDJs0j3^zu}(>2g97Fu;}H3OR|DjW z)E%m)Ly+PV>@R9JvUpjjjFj+zUy^hH<=HPHm)^JuEeB4)t6WeUVCwmjdJXVd$NmG# zMDRW&-e?&G2Si|x)R~@9M3Oo-OiM`ptcTW1!bMUqgen9t@;^mB>wZk9_^iQxEQTNG zoLGJXn^OOwJEr+30W99$98ldyX{-w}@T~5>Bs73I628J@5HKXc7it;nn<_#!R-r7* znwaWqmmfP7*ZbcjswEE&zZcal2zG10f4FpsaYwVgqhUYwe3?Ie)fkEX{A=%B8-)=# zI*qH}pR{HNtL11w0Bbdxo0UsV5 zFcP?i(&+F0{qmRJxu0qUCQmJZoc*JtW3fYf03W9c5E+0paPq7)grx((14a^*#~O#2 z0uM?GfaNFT4JSze<#p`tQ@1VG)vYtRQw>0XBHYIp+U6A>07y3p@T*S1qswnz0_ZLf z`P7{V_9S^#(Ns!d@DZVIw+sI(Ddl&M{daxc@?FPWe(XA3Vp&~Xjnh*xUNbX(&O(Y? z31ER00ImUPy8;l(H4cMRfdVQTNevtnx^vT9Jmv!V^YkL-7O}CPypbL)DRTx(C%pcR zd@wWt2R=7Zy|o3i9mJ|DScqDMzI5eQU#|L$Gu~F+jIswYpDi#d-}-tmoz3I>Pk4K5 zlmLE$yY2nFrCGsN4{ZBje5*he;jKnLz=~!P0k|;EJi;O<1L2*L4@~8CYbyXO;LUC^ z0_5%6r;M8Cu{`L-Qwn3L{A$AY0`>u+ z1M>Y>ueV_gfEOEw+7_p}&^__8)d5te<^RWAK$L7zvRL<(!%NbWp;tj3h%p`>V)2}l zrz}Un>Zt5YR>y*oW1L;IfW-a18QYvE2h7DptfLN~Oq^rBwyj-Z4%{CwL;%7FQ?`gfncWPkw)#*yj79d!Eb^ICF5 z!_FY`e5tYFmV@2N{hb$D)t&6z-R%@-KQ#a`2Z0Wd_{G^v67XmKe^zcijd*aO$aC^6 zBY1+4^Pfi=KXtYWpAQlQ7mfNX|IM?r-~|^t=JEJvU4Xk^gAl~P`q&8Ce+l|<^hV0Y zAO2h#=fwY$UoxC*^`SQi=+1iJ`Horb`jBIQ`xX8y1vg?7K;v&zf<7giKsKp|ZZFw= zt~prpe&VL+Zjb-mLy7Sg*J3dapzWuD%ek{MgsjIBlULlSIm!Ea0a;=U^AF0K#%bTgb$#K{}Z46?7J5)zVijs=zj)U&?z#& zyJj1hFTR4KwV@4sw;gjZzVGTcsUi4rrWSx>Bz@m_POu!&pSDl}fdTBdf=u7B@LD1P z$XA`2K;DZqQCndmc~Z?q7l6lqDXq;)MgS;RP@g3_<8&?HeGuo-OmGN9&#v z?yut|MTyyPTRdN}0k(DY7q@tcX+lVBf1Uf<1P1+s+D}CQ!FN$28vQg3i-OGOXqVX{ z$J?6C!A}Q@2V{wRCy*Zm;FB8BtNvG3xOBdwgKqrnC2)YcmZixZ zNkh`C5W`4V*W$8TyLPr)ZtV1r?Uwn++*p-e#cPF4#~fUQIvjHSV~Bzv;vfD%1QkEe zTYdX0>Ds2-a?bah=RD_mggVM9l$$5ic-Xl|MF1|tcb1vOxrPT}LC+va{I$*_^zGyW z^g5~3B)B^S2Zxd15VK;CeJ&PzWX{RqL650!kMMT8y-sH`Ua6oFKvx(h4UBnxK38l# z7axN&n2tM>37=~$=bIze!3~mzlLX*$jJRS_Aaf?&u7!BK5oJ&ZM+{tVT1X)L0dd4x z6QU)2^A4LP7hs9dTd^Pf^o9_CC5o*jj+J&Aw6SEcr)NFZ!@&gD=L%=hEp~1c_jVhv zv#GJ~cLkftW;I^1IIfA_?S!wfC!$Xlx=%b*YC6NMgJ-26I6)&!IF+jK`WXL>wn`@x z&DJ*Iy!njJwW`2^vyY!y+QbD^gOwZhlxjt56MAfjeV{z802U!60Lv;s{M#!428_!a zOa;K|IJG~ayDD!0VF20yLI({ zGl%U1h&`%JZfD9FbOqW{^zP#NG2)5C_e;5gX5QBya0hz$LJ9|da08{$9kPV27Y#=M zI)`Ki0r0fXr?+JWdXx?PnG~Gu@_?uW*z0K<>Ok4GWBOVrWN(o=VTLlvmy{Q0Kezz1 z9RRSeUJ@eq?Q>U10H}Q5xWbChS2$O-QA5~Yfc6hi02-j1!Uzz_8A~#dBUE z@(er~^>Hd$w?Fu?kACc9pF(jFfR%sxRX@FbE=>ktCqPhj{v<9X3zn09Hu&OQ_=wCWn__9eu@Njd#bZ%U6imqnFPS z!}9M@L=*b7;WO7-#(CElda7IwrUlJG(Djy@5*bX^r{F0}6O;1v3Xc{xjWX zHb^%jS5UM7#2lJq5qNMI0pI?%^G7tUM;ri@4%81OrO5ok0RwCN!;(|Q=wCyNQzM*| z+B^P#g$h!7d!zAYb4&qFV7mg%Xji{$z}fF=8|-%ur1p$9S1ig1po@`Xw3!nAjOZk3 zz}4mp8CV~N=*G93kkT|Px^_>yJ=*u93?KpUzkTD@9p1r^kes~0KTyLd)_vmffgT=j z-sKCp>UmerliE%A+|5YSXC}*gCl7Y=A)Kw-p(=zq{>_jN=-yp=h$b18R27+i^!-FX7YTrj%aZkNKh&U*$AKi+|KveuR0zEN_Am!L zDz-!c(zx#Gi>=su>*rg}bAmZ=%2mS|+!tXWE92x3{KL9Fue(8#6!BPZ2!Ow&8tK`BZasySd#;6~|j^?kc0;W;N(Y`?5`6 zDDP|5Jnp%_B6J~jWGScGnw7r-gCPR0Dh~bP=6LN3=#yQphfTk!x}AWv%o4; ztD+EDMHGK9*rI|6Co;{%Xt4?x08x%T?d;*$S4)KQ36jD1>^5Y;L`1lsjRKsJv=6!f zKEqps@ICcVPr~-iP*eFNCd8<$lg<c%A;AS-F-m}H*}U1A^Y2=-@3NNURufB;DVTyXxE@eI&3 zUi*d6`FJJvwFe-`7SaYQ+>>Cs>i)kD&O3RhPYJ+QcVRk6Pq#STsl^W&)Ub>~J`s^t zn1Ff&^lvU*y~2?oq)^eXXGNcIw{PE>ctR@ZhHI+UwXCHN$jVXl>x$c-{-~V*KK9YS zaN~1yTW7{!)bHnWScLxck8l4JQ4kfIB7GwJUdnR$ch!NUoHpM@Nf)HxEsIS_zT6#wk!FaQL{BObp*nXmm^dUFYK(v!>E z^W(DT_>4@xjzb1GclP*aNC2m~bD9{X9yt3Ist4+UQ^Epx2P>%i$-}R`1$`0cK#BlZ zO@G%oa~>~{zD54>e53|BdYs4kJOBhi{~59lfZ%Z&4ORY>0CESQ>X05Vdi}0JD7h2> zGyTI)jxc;9EA(>SL!^{V1N}DgT^-^W4R=VrWm%+B03bUSF1pL?(^hk_ zEl-rO>jnk1*AW7E-p>YH5QVI&6Upgd3WY&euvjomkE>i*P2}Sa;45rfa^~o0)!S^Y zbWGns8KQGqQ^9)>l9%BKgZzE{)+AbSbbkCw7y7yxzjQeU{$9yNdji3Vub64{`kpV0 zc&dR|D7sMs`-j&LD8e3IQVzcNgwmj=OBXX>0Y;QKl zK*bW-pw|Pa9!eBTnQ9~Jf#i?Mhn)nPh++ZnRZD(O&rr*v5b!fCl@lvcN3Vcnr zR7i0An-+(2zC>gy0a$Vd!!_9M1Y`_~O6Zx<*>{i4-o6bO@N=NQW19DY1E?O5SCH_3 z$pLU1D&Vm*?_v*tqVS_pzI`-O(^c5^XGj1gj6%^N<1tIr_NKumpyqWnX;><2t8Z6R zfE{}Z03V>4he?Qmb5d)ZN$gU3Q+*8l(k1P{B&~@6X^DF9A*jZy?A#vuTANtgXKm6fOeU$5C@B73j-~ZX!Os$?@N9|t%;VJ;s z26VN{p+k@$f|%_yJw4H!n1DD3!2j#QslThO-O5La~7JVAtaL$v1%|yE|DOc*VnhUJL)AhcXHO z_+b%nmarEo_-6tF0Ub`9{q}Ote8<{OstvP){=i%9oo^mi0LZ9N>~~Xp^WVdemh+g% z2gzTSL$6-#mm5zPBR=W`kyyiKiKy&Agoq5k|`Dj zJ$@sYHyFpy1w656wN~^513}AQGSl8<*y|cwubAxHv!u7t*^P1}(NFb9!APZXi-@-Y zKP7-ka#CM6WT#G7<^Dn}X%&)AZ{E{*Hy1?H9z*yKLm0V@9&wvE}i; z40&P|RDe$+^ONMJXt~rHZW`|Fwm15JyU}d=SPxRdLUGvxg}6{^O2KLF zW_bmq%uOd21*b<1hU(wd2G|3|SHABp84jR7VurH`{YA)JnMfAcQ#`;JV?>19Jz+HA zG!m4>x9uf|EwAPQN(7V$_y|EMpePW~XAkw{+oRde73qJ9$v>eMj~k48Y4;iLXxJzA zf%ZL+0h;7V$Z_8%)&RmSkyeC%WP!1dUuzlNylIvxR11fZ|y`V|=fEPU{%Z=;dX2{mTK8TZQ9 zPad89ZNu}B<%a2(=UM(?We9RR(i>MH16=+Jq(shpD1Wc+Tn4A)x(w_7)K}PAQ0N|2 zl-uS1X>s#a{{zrN(358W2|W5Wl+#x#_(j;W2Zl2LCu5udEtGdRE+Cg|^{~60{UiX^ zlt=)-M}O=WZ_Q6npT~0S4cJE@5mX$q{UXLC{v+}4pM3_5k-WnoP(N7N2Uze11;9L( zUv2HE9rU!fcV0bmfppx_E8ZUjYgw%EKZH)-J-RA3Y~}VT?{^_Q+UeG|(3(3}K^rt@ zhMtFVId{rj_nH=P`d*`JCeY(@&-Jc(^4@5%95tyLH|CfW0InwExCb<=xpmM(W(9Fi z7-ktP^=uivup6B4A_0NuYKfe-pBe83UZgm6f#zOfjVO$!` zQZEqyrSTt^!1^~M8%a1mVC4XO4Ubub?%T>z^Ur>3uULu*7(hU<+8W(!5GSTddSNrD z#eRT4kqwi}Ldb)TpNu55xlfC3l{T>mBLm1CSQvf=DiB5JDB*u{6fE!yggC08U!d-n zQuwj!KbK;tJrGpn+nQi324AOX;L2n;`x8t8Hw%95`mK_G6my?KM9^c_Ofvqn9f8RL z(*54uN3^JHXm==?QUbtMthvDq05~2cP*9tw&EuPsn*(>W?L`ISPZU+@8@loluG!CC z|MaojCo_fh`(c?v_+${}0VQM*$h6?l*-Z)@dSI=8%W4`;h6mt9DWDisS zLr*`s!ryv2$(BFhJ{dx^Url&8eZCFdgJ;7n5`c{Wvo1q`-|C(~FOuUy0@9P+lNY)H z40LxAE?oCr^@}ih00S10y6SPe@Z|FPiq}6x0wDbD0zmxhM=S?zh0NT7yuPFe*v=^6 zR{#JA;~mlh!1dZ4xtw!^!yj{>nSqf(?0Y%=ZCQ{v?lP5MI{$>p++`R$uN0j~dtk@~ za4DH?R&K=p0n|Ttnzt`W0+0w)?N1ciV!fXy7;RimPykCiNOlpgq6z5>HOI5h?9+a@U?{tnEPx8o#D#Gv9Q;jtvlM2xnMMetVbc{T}RpnRbbZl z5*fpMoo|kxWZS1`ShZHOkU;xRfFIZPGdrqB4@4qr`Hg0YT0c=JW;5H>Yym%AX#*9j z+t~TvN*6#7QqJ;1 ztK>tYxBW<*_~3bo^kXE*GL~N`hJQ3uFe~@(^Eb?fw(2ix`CuC)af&qnK3sdXJA|U7 zyoZ<@gA=U<5QA)6>j}uU^+va)59G=#I+&&?4mu(6pfnW+g@qkhIq59{U zNCKF6GARk^Ycz;xMlUJu7tc@A`NA|?p6r|MKrIF$!qE9{G-9rCj4{v-Iw5_1@WY?_ z*oQs@;o@T-gKP;E$X|aM+S*zN-iHW8>)-Age47mL#>@SKqLUB4p@Rh0=P0g$g&S9 zuS;Ga?qR<5i15_D&JMQkh003FKnZ{i4JtaRd&#J8n&4*}PvcgX!*S8;Sn!4Ykx(+Y zP`-EAI^vn`h9DF_MM4LG>7TdG1M zucBkSzjF_C7#xtS&LrX%=Y}Q#(u<$a=cc;8-{bhtK#WYWktjwEDal&2xKyu!j=cY1Eif` z0>T5L64*fj2t-U&VKxN~?Z%J*7}utD)~7( z8j{tZOSYk-wEt8_GUl0Pah&-7I?ZQy_Z`|!;NC+Qk#9>dL$61vt0(F2C((_U?lbKG zq_C$KZe8m^W0I~Pb<8C;*j4tYQgXFPxlx;PJA#Wc;OYvS zBk;x5vzG=xV}k@=1O*_G@Nk z|B8@zWOjObN$4FmA#@3+Z^>@+MRShy#Q`Uhi}ids8FU~~2R$P@KVtSu>)|V z``vM;E1#STYM?yl7Fl>b#>gPnC_>3EI}J$bJG*5Tx8!BFzjJMg|7fCqf*OFWR5Abw zK;2@u#G1PL)2X?@pv%}@iM}rNx(uf)R=GH^9-a&4O{ep<&t1$qo%bv5M#(!6_C$T5 z21I_3bG_oM)Qo@`GT0OhXA5DbVnzd6ufasn3iAvvN(k^^d-I?*>Tgu{{7uPdT2?ii zDVm$vB)fvyshyfb-kp_Nr%*FuqCXZlI0A_EKE?5la zyCbXqZQe5gP~20jvmi9;$J(o2h*)`#fn`(7OaHs_K$JVS0ga7llfNHe;6@mXmQlz? z(&H=7-CJJjCH}n&UN7K(kK2pMLwX@*`bksdn$-}=#2E8s3!8qO_|zCoFZ_{u-M>mD zV3A$J7J0bLb3=CRb_<{vu)II1T?7m0AS?yTQ$#ZSlPsJJT*(16lef4xOShX7eII)@y0uWLN;gMPdCd+2v0q*U=fGu0Fe?CU6=-HRSriq zDGRIO=;t{7nE`zGQy*dg@ctj&&iG#xNC0^=KE`ezc7HDU^3nuAf zj5H2J)AS+Zu&!ZK({=U1hUYo?Z*p%kFxyqbF4Djjg+T!u3GF zszqI%U@H5_2;!YmEV%>tCq?VUXtK8DjNf-U!r?OFKSrdm;j3A3aDHF5>W$}(WWn;a zMoR~YM00m4^ z{+BNt<0Gv;#Xc3m&y@)1369A;=!|H{|2sqffVtlAx%7;WW#?U9?tt%@T~eD zF>V;uV+r!BMzV{@?G2<8%3|8ulZg z4GjPe;JM4<15Vz$iR@TXvM;WV^K6NBwzEJVbgkC2)A!^CrI@ru2lPuzuA9 z67be;fCdG;>HLcZR0B=qp4ls{}FFkocn_?f5E!-H#B76E+rw+)g3r>FoX1fooL zit)qZrbO6D3Gi}A&K=A0EtX-ot$sKLWZNwhy{|(6)3-J?qF|^+uf6jxl$hAE*^kd4w+jULIC9ET(1P7X+o5T z?sg#WCWS{m3Hla0M@Ae-5wAN_jptT*rT%=aBUxQrspWfmDqhC5sorGV?F$E;X2Q6b z&N&)Z%vA}+Vq5iOwEo+Yp%0|h ze`3DMk?t=hO9{jTxl>FU3<6;J?^B}i#xH2^WfQC_ooZm0tj%=QUBNmvq}7K`5x&G=)6)Bc(znfkj9?lg-5dN>*-` zodYyqbX>mb{yQ6VG0baYL4suCCV9Qr$M2mldAAb5}}RivtTCMx%= zbxilMFV+K1RVMXvs6Yb^=O;uGvM1m7aA2;k7!2c@*{`_a^ z6aWHFLnt^d?T}Yjg@jG9efT$N1Or|E6a#WG6Myp4sYy8moc;*kXJde56~dmM^VPHb zECGY0B#IJ5mF}<#4E3Mb9~qcyYkzzG$*WgK-W0!TVo6mR_-h}Cj8Fj)EY~=Wwcjf^ zpr?Pp@t+0fMTgf@e__RuT#0#JgyOmN1^i@lzU_$N3RcD(F=RrjaTfBuj<~~ZR9D>2 zHn*=yC*VRl#uaco${Q>4Ak?bhw8}vo6qdB|b7=81)zJwc$T9F_5vP=2I8z=^Px5|n zt!lkCVqxt9p6@`COZG%eNNvx|4bDfT!jeTSz-%Pz=13!j-(wo3F=x1rs$e z+%NOK24{9C)GXG0-Yha9X0~b;OpgT;=x6d@wW?lE7EF@8IzCt2O8R6AXf=H$I01Qs z?g7qF)e|a|nEe0Xpkaa)?1_+1<)3IJ{Mm$x0agI~+uwLRDt(~L=J>ZpYXxzEfdZM~ z`AGqrV)O_Q5DoykC#}$*e?ih|H5FWgT^0JlFVmJlf5ZHjWn0Pp~z8u<&QIJA%oZL!CDR zDTs0eJR=YKp(-7MhJ}Ei^?E@+nb#NfTEU-Lg??yp)v!DlK>_+A>;r8jhjaMV$#}yo z8gcL3P5w6_^~lLX+HF!7W1Cn2q5Ol$19ACP514Jwf$`Iu#TwZeDtGGt0|^#uh!}Sp*f`fWa>ZC&N|>vsi)ha0;%_Br`N2nzlWuG zI7f>civc6ZBn+#bnRzJ#EB+U0ROv|&4nwNr$v?>e8UyNup2Z&8pAvJ248X=Rp-nid zy+54F+m@YL%R{Y)%0MTw`hN-p&T7&G5f5RH_q=LO__Hq_r?cQ;droZl{^8g*;>F zcsSWPLz&L|hXfSbWHbd??@ zfMpc;Ou~@;$DX z_uHbcw^ED~f-mwe$IEp`EDRwb?+P%0#78w~!bMMo6p~>34|@zIgVkoD z&;& z#0(9%x+yQqzo|KFy(ARoHO=sPxXHRt195N+V1vzC#$OB3##ABum~kmU@1lhC0;UmU z2EVSlk_~k~qGT4E1d&-(%|Qq<+DH;3nVFi&zn%PHeZFwf=Y@?c(Ae(nk#3dhFv?9* z{iGLU$JsCb4?L?6iciHJ(0Vvb zNMHQh2!`J?tip4O;=$W4Ho#j0@p>)qdoP#c{O~$R00vr1xj0|`77qQ|1LWX8&ls@p zJU&9K0Y3c04;ciI0@VJa@r@^k$h^$;!T-VTP%&i@P#7Oavc`K#0YtgZc|LW>%M&+8 zs{zD2Uw9bzk$IfXL8LqyE9nS_!#*yS7ZznZ#4-(ipb zFaYCW)k+`^2Izl6;Gg<`n+}AZ;?w0Jb^l15k!9Nk=O11@b?S)tRgeLDdo-@07^ecz zYCh&Ws{JLn*VFD8;{r9VPy(02zL-M-;DSP(v#O8Gr=#6>l&Q}1GxTK(E>r;B^L6dcY*qdb@c$`0$xG=_+T4^3zz%( znek2H4v{giMfv~>Iby$sskgllKpx6cn#i4qoy13p=8ilYf-sY0V~9TxEpj*tuN$TlW_tBmgZhk_lKp@Xs(;Zq)0rHFk(ST(`ew$UzQg_jr??GhU3sd8Jo}1 z)*uBDkHymIT$oQ7v9X1Vq71z8>p!pnL_@+fzO$3X;gPp~^@m?iX%Zk#q6mTKK>eWs z0s>NSv;W5?APGR=KefDY0A_&z0o(wj*QSm>gM%MUfZzabGx}HXiYGt>+n~K7_i!3` z>eXZ)2;_P8k~@cyFTgfss#D}djVLbRIKWThOXWUl7aQad@reXN>fz`Jm<*o&HnRVN zU5tK&dpg8`eDdn@5vg?|&%_jfR;}-b*ckHWy(Kt~f8n8_z<_Hk4P;!7#g3EW`XqS2m?HYe5B%jdcD{gHX^OK;~~us8ZTdaZdoJd_Frm&>74 zk5MnL^B)lAi!9o@^Kx93Zoe3p$geAtId7u2Zx;SjUwSE!;NWS|3t zV|Zk*nHV<>PiuS=ePA3y=mAC}rdcRF8$Cl9W>#epkntyuox~o%uay9~{8RiZ0}w{D z$85o1H3!%TY3rYPpAP~imk&o#km^BjnVn#|Fc-eifu#IS zRkt(|(n^f0+z|B0N&hG#Z)29|>h*}dj|?>kEO3?Uww;{FXGk6t=emcQ#*gDYPxj9$fVDET8yGI6qG23}Wu4^olPJ=+mUsMkW*7(l$`#-AQ|DzxM;QKxb zymD;ywcpwb#{mEw0}d`%n|Qzg00;y8w7tEbxo1wE!0YQK8x!n6yEuS9 zAUlBA0^rJtgeck}ZxK6m0e$r4wvOLroVwBYd_^OGMK=k6^853)4}f;^VEyqybbO@) zJMwK4Z`A{QjrN2!|07NCJ^DgeeDv|W2qXXkI199JVe!oim!71y}lLsHOi$N645 z0Btc$o|3WvkECLO$s-{zG?5B`$D@!S^#}_fM|>5SwXMe;?C*m6AluW2Z!!fy@iTRg zY>H6N5}EpzT~@*1!f1;Hz(E|((lP@8D9S6|)4TFu&ec9FBT|yh5>0?CVAuh(?NO!~3C%S& zt_`gjW;~5Pk2gNj<_yPr%gI#3-&%=23iZs@!q?{NM!;i5lg|s~;QD=ckX>XeeAs7DuX@tS(sni3l=UAiFysB2DO=E@zNbjxj*Nf@ z?~iAg{_=%IKOr9~w9IiJ{d(hITa`e|tgd6XlL2s?j)jrA*DZrC9|Wx<~<{Gk*`b$H0&E zd}u4-=g;|l9`i+NPH2`T*BCQa}?SGp!b( zIfqgpG$HB)grS$=1&b8ecPJJHE;P*>v_QI2z-k$RXg35@L7;-P9&EoBi6? z@7!Ac>w}g3Sz@0G;C(DwypN=MX0-Nl>ptQ9i;?dSI8ga)YX|>l{`b?bQ3Z6lyz$(a zyg>iH9`;m702r>L3|v{ykutcxKk}19@c=Twya0juk+I6fnK%CM@O-!Cg5v8j13&{5 zEDkuH>jWPNdd0ZTd__3%ES)~B2oazPZ4i6TX;JlwRS5@@3LpT92_WSFmixsZBt`I3 zSvX=GqE}QxNISzF^Bq$lrt|}MFQ1THDlGsjg(4NJ=NABR?(+-)wT7Uy!<+#vfX>dI zIjA2Pp`!*kgO2SsH-zr_3p7UAYil|x|F14`%PjM|6sqU1m7eRM@Yi;GZ znaqcRjYs}yjzL7!%115803kCN%1fpM3jOo#kn^1q!3!D!=llWP(I6_@g3~gDExIv>FeaDd7}q722S-^ z#l3)b8wvR2r$0@HpiAxcNulx585FNL!hw5u_-i^z5}*b#@iyf)2nBgyd;cD*TQ>uN za!?{((X`W*3g;R7HFj2d5PlSTw#-{^AP{J<=aeD(eNH3+L;)^FR&gS-vVLh8YW^sY z0(3GX3XPfPA?QM&6j37Q>#QV@4wS@BQrm>WLJf*Z77lzlgmjP%!q-Pu&m8PZ0QR|p z>qoFG_YhnSqND{Q1|LEs*{EJxmN!hH!lrq=Tv9XII*!Q9D!StLG0I~X? z&Tz*WL*VzlTqRe5CtNWJe>NW~B!EgfUZFAoHUuvq^ySr~*}+e~KX(7UnYTd(Cm{qM z)trtpT@t{=K0#=&^n4yN9w$2CM`#~LIOza{UQ;?o{d}INRV82p^$hOquTZ7{6eNoE z6&&aC6F#byW5qrt07(GP06GEYbLTrAvJG$!R?%@){cHljJOV<{4`4CS?k8->L69Ix zr7eZ8NMn4t5PMYMYq~z=P!f3S&T~{@~amHrHz8oIux?FVfwe3 z7{io~nqen_6pL@IS89GwI=@w^ubQ#+`hwF)|DuxhEtpnpAmlGQJyvxCB2bJL%ZPeY zE*A7xpouV2T$=ul>_gxhaHE?Q3t0m7x>yh{G8$q5z0JrZZyF}+snK4al^V*Mb3N6q zU|V=Au$GUv8TDu?TXVWRl>u)(AMn*;ZOO1ZPz$-d$dklg-j8{s_Y6cUqdJ~dAwS6{8m)sgnRDtXEp82!Ba4lq%R!fb@X0vp% zBvFAzR^DK60$Q3Ee(PJM5VAkxSOkIwauJ|bFt4tdQmq{VehSX_)}14md;t-W5ZT`djf27uT<${GNVB2-=UCrBe@? z()lY^eT}NI+mP;`vESfozGE})*o-4GxXVs&teV960UwXvA$T2Bd+7Y7*aqCU9%U>8 zZ6{a1%N?VGkq23o6Hqkc*<;tw@aP$Y&17w^rK3>ckQ=~4O%4<|M3s#Eeum#J^P`i6 zt>-K%?$c;UFwzscf8UeyMp1(3REyVVZ*$`kfPq+$^FIkFjQhOXHP>@<*>;KhI;=sq zPoGY9qZ%+NQAkq9B+EZ?1xGi89hIjj@vVQPq7z#p*Z5dT*Tz~=z!Q~;0!=)>Uv96t*RAV`kr zCkH47HH!n+(~*9blbtevE&&^FLjDjd;Oj)&qZ{D9@In<0pV3k=^@aS<(|A6~X02Y#24iNxCo;Tv3I)vTAFY$Wz>3L{%^#4C04Ysw94BwS; z`H`L;7j`{b?w0*~H=2Q+P&bE}6gn0tfhrYG$f|Eu==}5bd^NNl<1}>ovZ1u6u%4^e zDwX;RkLk|~<@1E%>5@O3v&O><#cCOD@P^TNP>x?5i^s9El6Z7#=OzS$Apm0uQ4h&p zYI?b2ejZ*Y@J5?ag%-G6j;;)?g~~(GP;jGW^ai&agJ#X$Q+es_iof)_DiyaAH86uY zzel*g$K_?cFd2%r#lrz_xM+GSA)hzw3Hl28WN|#}Gbjo|eowd_56a@ZwOWj3Gs$G3 zz>0tsEsiFV#cVi^YF=b}ANY@#QZi{pa0uOOw#HEg+-y{l8E?{UZ(tPgnZx;3HbnF@?~>h*Axu3gz<+ncmrSvU z$F+tIqr1t2g(!ZL%mtEc2emQK>6PSPFv|UW!6N(zSk6Z79|_%6vO(NOt>6LD7=27z zl7YP-7LYQ)XaQ1S25K{`z;K*si(!oAjm5)TrYG(}h}*N}f8?i?z*Bi+F5pea7ckaX z&*g$^^a6cL(s#HC3XIKMQops2U)<+*wZRJ%NcgmGXt>ukGPg_}u*?+t)&!#{*yo4> zuxd}|Z`b{Yvjp`LYAL7#w4uhp8dSrDRN;c7qa(TBI{68Tf42YNP-t!eqnY1>0$^ld z1YZE$!LYx0o5Ozw1c1T%_Z@Q-o5k{Z2Cz7sBy^r_e)r zC%aHgNK*jpQ1X8Q8uX~30jfcSK^{MPV;U_jRsaDH+voxwE+Rm9_6Qt6$lFZtQI@AF zL)@Kw;C6g}z(s8|aItbf1Tn;jSD5Y%0efp$?^Z`o>QlM_ z=AW?C@MkIexy+CN0DQtdo~DvQwMan82$)-8A4-jK?k&5WK_dbqk@epFra^;~yGWlL zz?NmHGsT)TqYIWa(F@HNKmbgLKU5W-c-5?i=yJxTU{c7#5GWKNG>Wq@ZlSZ!#UKFb zqq?>3V~Id1D7SF&7h04bGYi?OulbCs$3P1ZuBpRISxv_P=F{5?K)KJ20VJY6~d;|pU04(5Lp05foCQ2{~}jEivQc=9j- zAow9w)I&G}<*hu@VLqQEQAWOhu^r(MJjF)-gTe3!$>484{o1eIo>Ke&>eVY&gQx?r zD>(-Mcl0=XeucfDeo%cPkl5p3KLqyeK>ecq2L!oI4iG0uqdI*VK`bi330P9W50!%FAQkp8mrWq7#rB7)VFF8 zzXdbYUxOT0yC1rpxnHa2_+a|qjTAXg^YwIv$$7XC@z77{>17u2kz=rGZm}r9ej3}T z90Dw*vbD9grsXYG$AP2Q=1`nmb7ALyb9$+_Tue@PI=pMwP@hW1dOHGv>E3ehbce$+ z*yf7`)^->*^{j0)$}7!`k>VgpT45f;7f9xPobhHbY1Kja!pW+*KoI?`I{fu&leHdP zfz0H=HgciJ|L$QOoDDUb#81MAwoV=x+uMj?`CE;|$yQ@;v~`fMUzAHDnT?USp|ZNiXnN1x+x!;1w8Vm}YhhFvIA_dCWw*E{;)3y=b? zGLP-dGSHpL3|a;zS2e}$4uIOVCU!Gz^M@1tl_Sy>BaLX5_exfR7eNDhKZ-o3Il)@ zj6RUEiR1|66F@3B@zrz7y_o+3nxNIZ78snrdzGo>sUsdEI7a{i0W+)z1`o%acqgVk zCIEP{>|R)ZfuQ%a^mN z(P#`qL79pNLF!2{zLhn=sQ_at1e^!w@*?ynso2Q-6rlnL_5Jx0vO%j*q+_rXhaF`=sT^_?*8VpcMkS8cJ}v* z?=(uSclJ5=4=kgM>b8t`_YVNC_xFptzj=1Bfi)i&YS}!in-sttz%5QRCJa?Zjr?U= zoX}duBjL0V?+2*o$OEYpXr~>%O+THzJzJyA7U0HsP~sb6+29728HYHi>=5DN*tH~O ziVvCG2;~uKQE>1BBZ;628hTl1CIBkC|TG)+-&Xh2UO^6B-BET0*Jww z+(JV&>SMwC;Gb1w1N~T<&9b~i(-Z?4Kw7*6=8(C4ax<~VACM_ZRkQZ!_Pd`ZyfXgV zKe~PU-N%p5Fh7M*$nnXl6x1u}s5T84*pxnn5S@3>7c0-91rg+QUXf6EnZYXYpKao1 zJjWg{vX*l`xlMcB!<`)y1UL2mq}=1M0T2WrKW&i!fB@MN(mj%cY{hvKcNLJ}=}`>_ z0R+7)$%&6=>00 zO0FSEEafN(m}(S^{`Qfs`M2@sQuxo~1FG@g;vfF-hbK?S0Q@GW$N@x=RKO+e0m}Cg z;eekPdMEte9_y(R7a!2(_)Gjlq(H@~$^u>D3D|0Zd>^5TnF!xbdB8{k_|Nu7oP0t8 zc(VBL$>L&1n`5kPa0*p~;i)&ycTQZoaFhw6a0IYNehv@$B_<3)AG8x7LqKkS>C`#I z89PWFy*;k>)Lh&A-8ZE_ek9DZhFNhi8P4-Gs2Cphp?|U9sb^M!#FF0J{pXn)9&1|? zm0%jQRnJ!{p&I!ha_n{_Q?D>>r+=*Hvt}+-i?0_*37Hp$$%;LGvYuRwdT+y98EbPd zID-LB{T?Ze5cv>NdZ+{XFAfCUVYbzq8@+A4;s$hgLjYcZn|N&rky4~4WHFA$1jZou zmM3q)|9Y@{t#5j%V`Xk>>A?f^t$K!5HdcDq$~`@09wJr7*svU+)J&~t^FWf;oQ%c& zo!u%t{(VSj2gPKgwHs^_)96PROC0Na)yA{L{z0k5Dj^F$`+NnE9)X!MruF)je2sp$9Bp$}tZIj*{ zIA7N`Z|d{yRIn;Efc0LcX}o1JcI?Cc=Sd0V-{RYO-_o%kRsqe0;g9Xv$Ez=<@HZNg zSpi(b+I1sK7tumExOtE*Jb!(gXt}^}y{C@@Z_mxw3E{O{)L9UN~``af}%+tZ^`jR|(h(bj_ zK?y2IVB1>&S`Y*kEaT@DzIfqIfWN-vg++KER zXap8nOt|q?G7#wF@zb`dfT|51h*ZKnfMg?bv68`2EnF=v0=f4L_u|y`|fU)G*KfBgbEh`PzY5H;fq|Lmhxmct^e zR%m?H+A5e~1hcmyP(fBB)xBd8w$B?Thf~w^rDMUhId@OHt1OF%UILo&OJ}o;pTBleZph-1~ap^1VOV1QHQE`o41wv*^G6$p-_n7gY&0+MxGetkR~wOuD<MEcGLQ^g96@P&s!v@OrZq2K^2ey6o@?f(YrtY(IbD& zjC$i?vsDx+VSBH50FwZwAhO(7oOXnZQq=55_4k|2!C@5(vLSwA+F) z%nGJEu#^^=m9K*b@ZjLg`?X3z0yv2T$e)n`hX4SOulk>!A00r-c;vvpMhnp1-~YFf zUo50Sxb&nZT>)9#X9W13FI>-ZB(a$=GBfzb`3_9_?qVZ+7dGI;Ki@4wI zg9DoAi8^>3oiPB><=)Pz#feK04lchrJv4>e%0PR^6dwP$g-_5XUAXl0!lf&xG!Z}< zT&m;BLbge$h=6Ja&OW_>zyf(h&{9Pi<6v@O!BjmTZkc~)Gy>hMC zkmz>6Jr-m63x${z?Z4EA*MeFLNHSM9T!EN-ZD=T^(C_O`CChk$UAqUmD|rc&30J;z z=N4jfNM2IjH81vdhBzJG^q3N2A!}V`1WUqjrUMEwJWBa3i@} zhD{LMfF;g$T!Z;Dpt+Y<=uPeo0)OWn`JE)4?sAZ}m(Ma-jyauU&XInH(-8#jcJjtD zGq&i?sN}JCQe5&(w2hKBcwB9vXcTAgMjB`|g}fE_H+G`(@G9!!@D_cT*KMi$@b>ue zW83-5ru^ul{}%%o{vTBVlYnvZ0&O-Gfc!^_Ad!|(z%XYm{eQK}%o_YU4wb`1m87+W z`foNI4eEE}H|N5CtwwCEX`-y^P={9to#QCWeNp7 z9YC;q;>s1mTn14%NFfD0iAl)Bp=y)_M7j}?Kralz8VLv9+udWgiU|PwKjV=tJbuz? z3V^?%fY;v75FfX1Tj1kYoc}+4yM15;3?Lnkv(;9qP`_}v0sk+ge?bdOUEDi3KaVmv z(f?#}a&hS{`-xAm??V*q)vK#l7yB03StJci_F;z)A&-~i6atz7K)wSDAnb^BE&6IU zFGfCqob)=>3+S*`8c^R4NT}Ul`##e`ZYYIl9{@rD{vhN*EPL+4Iz0F40$QNZ002$< zyHG9~rYB)!B2AsEOa(Ak`N~nKNz4S#9w&eBOMQm-*puL`E{Y<_;tD?lLDf5#Pm_C& z)C!W9359u<+y4^xVTU{KN6yS9no$D6nO?8gI6nNZ)i9z6Rbw>Fe2D$(=jsc#NDG;F z{U*=7>gcTDDLeuw#FdFYlDTS=ld+sy!#~-LPF&BuHOhG@4avv>QfM{t?44`9(I`9Y z$kZ>xM81Y7;KYqTbuV{I2>d&@CYM+4J^lJk;LAByNtYi;oOnsbb?hrkxO9413Rv9a z(xxc|h8$_6c`es+?TV&Syass-r0_Ww7nA`9oX}uU&{dFWG`E6Iwm*3p8$%o;wvdjc zV+%2kNBCntBd!YD@NgLhd6Knrg}frZshCGL_ki%G4bW?|0bGb)D4VI0kOX41CP?Funl<{z+{Kkq-1hBwJfHP;^07ha)tmSr|wyN(=!AcFS(sgFr%x z<5rbL1b#t=M4dPIPcx`AkPjton?7O^J!T(t3@?7h%qvv5(7G&K_GXjd61xkF&aJiyIN|qM&hM5J9WWa`C5o5!-8CQS5%P|)8dEZ;l zVxdBA+FIv_yT%}fgkCUO0EVwYj?R0`{QY!$R{;Ef10M1L;J z*g!3%t!uaw(?GU?pP*8VQ{UtwD7*xLt}=|nOJH#Re8>4|B$lp0W;)8W;wu18K(N1Z z=(DGMgu9@5aeoe%09EL#7Uzw7tOy-H1o|#EfnPd>9Dqup zC13|o|Er<`y)n!#z;M73=$LxA_{P^d&%ZG^_y&R{BqXFt5G)oZzVcP{NstGG zumll7_#-p~nG&DClj!3-`Vopv%xw_PLJG>q#VTFfKssD1;y9UNAUKx0Ps6V#RP6*3LajIZt zD|C(6WMaJ|kNCAU6lD9bi(x;IRfG$Ib=gt0dpz3(xEs00L&QTTr&!N(f8%W|0YUormmz~m4W1gZeY0=(tX`{lc(IKGYEPRzZxz*EG>s5zjm)oDP#DVX=2 z)3gB7h!NiHTx1;>C~j()7$h|;Ely)nkB=wXB@zd)+5QT9EculKP7~KeIIm;@HRgoB zi~@wsAD35vTSf$(RvkJ=~Z0jG{mc1s;^lGwiV3d_KWsm>|#QrFDj zFx5}zJk%hN&0#DIpG=@xOdh!K3hL06iHXbSIQ9`Rh9ks&BESS@Br+wGX&6)l5&+X2 z_GM|?yv%j09-|WX(l9xbI}ge$Yk`z=VI6X}`XfpO`bZ9a?uQD1)EU_6I!`PM?xeN4 zI=)%rACWJ#eBls+zuA1)Kyq_mZgdiiMJby19Mjq~=G;aY>Kbw}*h2v4GWn5>G&L3Y zUSH>wIIC!ppiDf~!OU|C%#daAVfdT83=Z}nR{+LW?ZW;+=B9 zp{2OM0ZIOHWi%t$l)zvKXbRL^Qw7`zlnUsugad{F?pZj{C;>oG6GlVAIJ8v|cKy%& zCN)bn%TL#ea;=2bIRpof$A_<>$wn85jsA9~iHI4qzpA@`F15C^Uu~JF12DA9^288} zfge{YakhJ6mktuY@YAJu6~8g7>pld&GC@?UF*1w=upr$wHpOTGSr zzs;lDSo3*YQ8QtA+~;p;+w|+A{e#{u2LkOl5;Os5MjtZ{ef0VzHoo5%`DSDu6@JG2 zX{c?zyC8{z-icfe|u}h z9cTytr|MS?@W#VoG67mZ)BtY{3?L>$0$7h%$e3n)!Rc_Kt@dCo&^FftR4r5R^NTXR z@9w+MJxO@6yv|xs_uWMSaBzX((@GIsbf7QsqICc*_Q;r*Qx=x4Jo-8P32sXLL&Bd} z`U-S`b)3tGQv!V@DL`4`GfVDxxYR666Z_k)k4eNejtLBahUqLf#Wk!Uh93 zr>(>ehM8;|X7QN6l$~i30GiOiPz7axnjk@L)&Q$fsn4v^4lt|&0j3T(IbNrhsJ#fG znNPHZswQmNX9wk-6|6>80mwpD&T`Bnaj}(Qv`+LT_{Uo1-NE}en}T% zUCxDs38rKH10!6N@Q9N*ohsTFypkdCCJ8)>bFFiV#k~$Y&E!8bD`E#=b;$}b;Pr!C z;M&W=s~kHxk?D)g zXrXjCB(&npE&8ic0b&=*KwKKhdA*9rLl1wN2>w+}bqT}^56KI|u$G2leEa_YK^8oW|RMmp~7<;XiT;Z-Q3+Twu+u4(wf2IsH8vWQpO_$c@yVo z2TTjaKSE^u8X_cTEtH5T3@H1XOe5%0jA($P7=ySGFLelO5aNNvV-@p0#en}sKISy6 zZOe@HcHij35&o-i`ZUBJ8iJfs=K9^ivm)6rQHOM5E;!g@n34WnH%Ed*Dx?8^=`yaF z-$s`MYB0qj5;mw+(X0ocmoWTjJpSY-k0}B8u>!7;0DeIOKj2LKl!X#oA|S7Pc;cw3ieY%;4$j@X*W1J9+|Ekd@WVx_f6zy96YYn;aN)9)Kt%%7 z2=MIjQ!)Ygyyk$c<0Hn)@ro9J3VU1JkEmAFPmn7oXi~^Hz{coh5Fk4MVRUw*aJVeY zXMhH60Z|*z`w}ptwxJLsgLU{79)`)o1yVo>pb= zzE>Po1c78OghvJQ02pE&awY6oo*wFisNCNEleb2&V5OoYWfSRK&M`&1L@&o;I@yzB zP&dKBD-}Y%@@FDGlwUXRb8XcbPrL-P5U?QvOGF6^B3+%Z7)PX!23+(!4b+Rzo)!0r zXO`2;yWHTGAbo;-7eMhH8Sd`LZUbAy6+Wh*EOMJq>i*6S^#+kn5RnPWxNHCK4rxH@ zRFoevrGRoOssSLRAj$!Q3_vm7Bz}lS-tCcrs5`O=B>+tY6#ay&Mg$B1w4+<#(m%6R z1ZvRMU&tZ@@Cg_@fcNYrfJy+bfdY&_9?#gMA=JN|Zs<0}0>Y$}7NEJwK!8;!vQdfN z4SxeSo*)4hJf;3+#+hO4mZ9jIVgC17+RSZ5MyW`w);3vy-&~abL7O6sGaA1%HBg~m zEcsuPVf_gMJ{=mgsr zkS{G@wg5Uisj@pb7RpRzfBfU`k`ian_`xd^rq4Chi3v6xa5oxZ|7GgjW1GICD^A5K zZR<9WHDPNunr%6eO)V#tt=gVLY-cgpNmvBNaf}j{I3|i?*-4OshY~>&K>{I!Hc3ND z${##N|6pZM#5%!Rsog4yR8go2##>}!a$nP^9@Vv;aIp(U;jO}l?=T3cF7 z7Kr?nyXF*J1Ay>C+bRQ1aNXsgkH#9)Tn(X-&(SHRKEBEe;=UZl>eV-Ov~zef%3I(Z zp9Cq?yYkGxwyf}8V64z^+ac@;f1rv5lK?0JP~PS(r@jt)=I$wxk63Usva~`Q8o7YD z)O0?zkRtTmRWk|XMNdSfyqbxUitE~gXw7*2ao!6fR1zRti7>1hpu&~oZ`4Zy;Hw4i zNoi;wlY$XK(Qc>HL6iW}Ahe85esCt2%x-8nD9yQm9hk#7nmtxW7-)ZlqMo~K*6B{_ zto}fQyq%KCDF?dS6*zIiVOR0K4l=Om_u(T5qKWPjD?XSD@H5cUNd|3eW(K+7`ysY|w@XKHMl@H#1_v7B)?Z;rG2nS5E!OYoq6##%n``oD`P1Lh+0;FBR zg{=hu(GuRakxw7}*{+?3Q7kxcVa5~yQzQUnbh$1jnf}QRDDZ2)PZ2;Pz%GEZ%Yn+h zHs0cHjuUMR01+23JZJ@M9!NWFzVt~F0RH_*fYX1o??a>CgaXLh%=_NflFi%5|7qcV zKV7|11C+p@#{8iDBI78aO+;({&%1&EgvF^1kmiT>f>``j|F4uGi*J2KSc1%ineWrF zv$exCpy>tF9h^tBA8`8W=-heM5g7omT}Mfcr9T{-YuD~<6g%dZH(Djn*!N*8OG7MI zF!XCB0o((+7`oYi!gFr`dDA&~jAwS(ecAwVxh$C;R(A_j6q#&h;qU%#$t>(F2c2D6 zW;|JNk=QJACVn>wGRA-WL7=YEC`ic(nAd>^Xkx%BMV;SYEvt3rV9ff75WME41FXq2#uU zd{dH7KDM0za!?s5wil_jLG zxQf2Qr&1xLPvMw@Ge6w2Ctgy?|9CXv|tNvk>2cWTNm#45W z1*wgS3gTb_K=>p0r&8F?>Lw0=K#DL9%(=8kkB+6OheG{#{d>wJ62n{p|y}=-shGevN{|o#`4!eim0yxGdMtw-)pHCkv70 zapvr8ZA^ck{KX&r?Qi_;-~Q3&H{X=L1eICG-)v#v^Q$+&;idVrh~A9>2W#i zkgi+8mE_u&f4u9pz1#OMpk_FG_g`N=e~u9-!T+0_{OrFd{9ynZL!gVlpT_hzfY?>y zA?rXB?v>%JpDNhW-sNONk%{z&14I4yhCE1u-aL)(Khr;!aR|KAjMyZ-0kjTLm-iv} z0roS}2Uj!PKM1@{oUIxL3TA7J<7Lz@T7ZH2#R^gyFp~k50b&AinY)`5!lTfo8!1t~ z30C4rr4amYb8536toXlq`pkJ45ZBM(<#pzrzAu&rSdhN+=O5l#Sndd3D-=IkytWKu zwqu}~^a!@bx!>v;8e)^M|HM6-g{!O=0kH1?8_W7=P&sUE&SanqT0}Nen3voN$Q{SC zB@0U6xA}*&u)Slz!D{q%(3WE}0s;i{=%ku86oK3?YS(jFd@%T#Pif= z9bmu+O)C7u4(U!$f-NfAr9W#9`8;m?Fo7g)StK85=%6K6?uN}SmrS z006;{v_wR|PI9n1*$8ybNN0x^fWHy(fJgw6<7^~=5n34jU4d!bB)t-_JV!nDKm<6+ zk(0Z^vL3EiYXq%dgH49=H3o;oIXgpqE4qI?|1-9HnP#3YBN|2Fbn`cUW7AG*1S7!K zfMQ4#ykvk5owZ*?QM749;Vw%+v3sfjC^a%~-0h`Z0acXh2_$2777o9^=P+d8YS;&x zzRv%f3$IbM2o|@r=;+Om?Oo!Mw8*{0<#0nILq^V$9%{Yx;Xw1Ur**V%-sJ~XT@86! zRkn`GP3-Uf4u%l(T`a08#cDJ_pB8Uo$f4==G2DW&6gdZ{1?RX+M~$C=CPLJgx3BEp z^%^EXuU?JN0&xCMq4saWpTi!LuZwv7s|;vd$-rk?2gLhZzljqJfF&6mNFt2`4F&Rv zgn-c?y*G4}SnJc$&ly5*?jZQN8Tjv4;}4_;gO8Zj1YZJPTKpp=K=D^~pm;WjpNazc zfr*l%o3yWM)`ZNKAnyt>;89dG7y&WvgNhGd0#`_UAYcbp4p1A=_iP$v73uV80RQVv zN61Czphg}$B0l86J0IRT+l-9w(gH^tb(q>GkB~x){+1-h2c(XdNGrm|C!t}I0PG@* zxQTztao6y$hTh=_YG8aZTjhfZ|IGc0JH_D4W5|kSNH(6azAq1Da-8;8_LE zNt9n@_QDo|4%9bU@+lVBMJKSp?1QetLcxJ{FdN*qspl_&dFb;UoF45o0V4iTaThhu z(`bhcfVeSiyqcF>b-;U2#a2rr*WP5Q!m0P%n*=lu}w2(re( zu9#k=#sF&5@}xBgasv6#p65zshR2BgxEA%E-1+FX1_YEbWzK%FsdEZ_+IoCLRN^c8 zIql8r--Qt~l_v#^z`D{+jb#9M!6i)?5iXHaAi-GY0>5AHmc(#(C-ZK@Qh)<#Cs-vN zxMgzn2ozgHe>C|Y>Aix0Sk>)oXK8Y4k#r~Tz|GZgFbT$Lb!b1B#%(Je1KXF3NtvGB zO*K=`&E%@gAE_|%WTAQvnNvZ(+Fv^U)m!h2zjUBdQ-4FVg{cUlyBIhFZ|0)eF0`kA z?N-7^KDq0+E^o#J&X$J<*=J=cOkY3RTFh|zfSmB(F4muM0l*S~@RkNJ!{OzPh15dP z1A>JTKZrO@_%ZfluAhey*vBwHHZ~Fmi7b5jF)qOSwlBPT%EY=XnJ*UjIq0DQQtuCv z2?YI_1OOlqFx!Cm>_G-(Qo!Qxb5y|79nJC~gkN+C%>C-3PWp` zZ)K*19-AX4OGLSBS+P&NF~C0k6(PloRK0OtEK?1{N`P%FhTkR@{n?7frer9g@`Ve4=q?z6cCWPWMm*@B{6_mG^GpO&496sevd>ieK;bXPay{( z0a|*P(JDdd=keL4gK`NkR|(*Os&Pqh^G_x4{|7R##P}W&_0Rx0`pF9F z3J?#te}SN&`OaWgm~-eNZBPa0uX@9vBbcmEP(o4#pQ+7Y7TuPT7Vjr0%O%L zqovA7_u(r%mtJFWy7Mq?z+;$9o#2$hU9qhdUHw(5Z|48n5f(;hQi0(NYll}~MiK!6 zL_vgnf>t0X00VuHM|BoFfit5d0QwRLNo{lI5DOtC9XZyt|81Th)yXN?5@*;^esLN; zz}1r;!Gdz$ECyYi@*u7q^a8X5o}nFUFF&%NaD9m%|;xac!Y61`K+av&)H>i}(=Ut92 z4pg8$#Sp~?fvP0H$!nN^tt5R=%zoYo!9>AExuBo^1s;%|GL=ko|GQ-x*6;jHOLtEMZ{*s z9R~|?+r8k{S`gu`hIk(@^F|-@*Myf{*rp)?r~;s&04C!$B>?{qdwF)&bvKPnb)3Ax zSMl#d_MkFnwx@6vyhlsl5D3ww*Yp5Hze)o204fe762Q$E1FUmJOFRKed+qH}eDg0| zk>nukLD4fip$u~m(pJWWL$3Kh$`SsY{#zt)OBV?s>|kRO4`jP_9M~?HOm19T6rUd( zF|V=!S);opY&D$T`Iku3?t}M4&!EgfbW0hIF!ZXm8|6! zm0U2)aqenINM|a8FR@#&&PtrzQ^;@0)8q=6L36)s|67{}`sQvdxc#n<{X<3 z(b(@0;-^RZ==5oP85Nv9H+PiH_tWRe4X2eIPT%Nb{wJN0C8X@h0d4O=4gKhw%fI~# zuf4vH9sn}o%F4^<|M?IR5GQm$PI|??asY}xf1`1L-s7T0_zdI!hUN$ApTH2p6fQAm zoWsBvZVzeb{LukNIOJjfANem0;t8=MoXyhiq6mQXOZXC{%m7sLA8KfQ@xEOD_Zxvv z1-Am<*aT7=Xa_*FU?BhmKd)p7uDu~69WH}!75=aT1jo;~IaR&VwP432 zrm9xH*ao~_uf@cg%SEYMq1oJ?1Vf+n1D4Cd1#__;!Q2~c z0TV`o-n%foD8E51So5YtVb_abzDqP#HC$j)q5H-$xZpEo1-KhvO}L|S1Rs$Tb@r;Zm}-nX+Kg zQbFioBnEnn>Od)9NMX9HZ3wrcc}P@#LU?rUy;W~LQ|##I4`2YSbtw`A zvpnB+{ybfPNVO9FAXr%dg1yI!9KGKrs2k@#f?qoQb*yufhI&R8R8v6p1=V0aMz1m+92jaH@&tvF+44Iw!0{7f)VSF(SbWt@!on9)pcQDB zdn`RK$L_8@gNzEJ2F=7M+rUyz&S!*|CWvJ^h+&^LN;PyViRS_q&9%sIS@7NDHR0x* z{XY6~%6(qRVLrdW&jMPdct8w z1b#9}yQ7Cwe)=B>X$n@&2cw9;uP6a%HptOU+(SvwsjJ$X+606WBE9k)7oiXQLEysy zfeB8Kmt!35YWIb>Q6H#E;DLF=rL*5u_5w{H^;D?K>if}#Qs}veT7P>xcWJ9kik}o zaDc$=?oPuLXhFpHGQaR6ojmh66kf1{`%j~88v{Qw#3wa-X4oc?FP?gqG>dwHR(dM4 zWq*bZSyxJFizf+ZAa?<#UQMV%_p^BWcqAO<0Um6@Z?47SnM5E|KzuNaU+(yLaI;hM zRT-3V?-2o*GZKr@@B%t0DWq2M(q4HnNe6}i5{`C?P|PnyRg>ejvCv82e!})BW%hiY z3CUzI6zJMO=oZWxTQGvJ>DcIvGjs1CDa;NVO4Xw*1t1Y{46|I?0;uv_&YuN0uum`$=3Inpi8nxTz?(@hF41$V6;HY!7pJ> zq4`<~7XPO~0ML6xH7&k$|3nCGECcg#<9&?;_()bk>KUXPDm{N{1wGb&`T?*2HiaPZ zAUeWPf3&Xyh!uHX*nlVcB^Ew}U;vT!VKR<1m?h0-OC_%g-{|CV54J|m9aPnu~}}% zT5*_$(~iarfFEtXZ8D!J0Ez(|(t$HL2d(FfX9Wk9G5qHN_a!2(NRt1OPo^Vaazv8z z)UIKIgQjSG_tp%!MF)@Z=5D9}ySph90TA20#!35S_?Gd`Aeo25IX67b&Q zE<9)W(p<#0NIwn-6@Xbh5)XhWqu~N60Z4@-Kbc18+3TLSDsDzE|@s2-1v(2;B(-+tVURcSUO?>JG7 zS&L$km?2}BIS||lY9Y+GvevV0my`hTpZN2S{BfCa3r}?074jwV^pTGg=1W18=`oZp>1q0l>=k+}YM!ppbU~Jge zlMu1Y>FT;D4*(4UKcf-QMfmFtzi?enj-B?4xb8H!(s^>mvslZZk&R#a{_AHNiXXg& zg@*_b7^Da^visA7{wEcTS08FyT}a+d_Tliw{JKJJx6-Y5frTV6HEp2~~hTTcWESIj6BQ zydH@GffUeKzWfKWfeLBOh}2e3B-#VFNTd%lnJ_4S#1nF;9rnjp{Osh~G+|pEHt>HQThUK+XqSC>c!KH6a|>)N8=OW<7b475WR|fkzD%$Jo->|-6Wso=2fLOD zfZ7RtZPwT0(~D2i?VQmFRRhlx<_czUx=acn=&25Lxp~V|1_2%ay1uaG;nzD-kq0^D;=>2Zh8Ljc{aXPDw7DJK%F^zq51Q(l9{Kg3pUWAq6yhK>^a*RP*BcMkMVrs1ba3`dQD zEKpIKCIzt8R=>RbkH7ue?p?cgzrJU?g>8cWp5T|PhfP2jrLK0xzLtU222cv{!T^xB zw=9F!z1 z@><6`JG~O(-B0v3jJ~t6>5Y^oIXS7SJ96U%=hXN2(^& zO$Z6(|5$yg%|C+Se%Cp}h5}tSaiot6OP-}4nqTg__ZwNt- z1b~SEfkwzey`UVJKn21T4qANB{I$3V&Nw}Wje9%9K-mB6J(H&=P;bY@8Ii^*%LFX~ z(2{S8yn(HQAJG>9i{Sgb(pye;#k|(L30Y+Ul>%n1iObuFXfBfgKtC84Xu!wqyaMag zb5pRB0yyy-caKwp>m3aPMF8Rz5T7e3fzyI02!*1g0_b-{HGp>z=JQe^KvSqD-yQG- zF&V36l_s&AMEGh^H74f#7u&V_NI;xZMgmlGK)v})a$vfJ`XKq6)&NR@IyXP>gD}sc5ea}Iz+8C#aP}WV z?puOiV*g$T1Gs)r3cLdEg!P#iHr^XZ{;D@1JdIoT+nVDW<8KuMOaXpi1sV3A-m2Qy zQ4b_YaojF;WH0) zGN6SmYwd<)I6SjDYsEOA*hYaakq8#pSohKN-;!IK(>DdW9{`-;r5!alT@Nv8n1i^<3GSB1 z@9#V=(ejb|(g}=KQ*fiCHwFg4|My}r5C|4lw{p0MuyZRVmrmIuYnQn9<3|S`tv`U- z%w$4o;7aH0G<42vHJ-tQD$jN@+|bn&&tt2ehgFR~U1*;8m#!~fk#(?Qd&)ZQf$+gb zNW{}Wq~W!8Uq;)AM1Hej2Q(c-%7U(-4}^}Hz)iY;#@sO9Nht#xB-}4Q-t`N=fE?)V zo%^6D&(Z=+yd0Y%{0VmuzJ?DNm$`*74=px$xqb=D&`7xRRn1Fyi z#k+Z5qJ5GGRTH4-SJcA;Hp(DPrU+1Zz=z!Thj)PaPS4NtWY5r@$T;K-8;LLasQgdf z6fW2yC4=!HtVGin11Em}-c53W1dYf8v^G*PJ@d|`xw*a_SY~mqv)aQPpQEuRqMx8< zDnJE#Ol!c*^{78{9HBGvx+Vv-%+_Qc3Fh3(#jkC(AYVwR!=avRdHqT)^L(nuZC{;S zmTBupY?=Gjv~%KTq&q+lPt<|2!3NMrt^^zbhr`9WF385|@b$E}x3dw53%c3m{*2I9 zG2rZSCXB;d!jW(#r~v#h2dOEv5y%3LeQemT&5jigc2wyp1OP7Fam7BcAb-dJUPUyp zH{V@Z;-#Jdb-r=};jh>JXs=U_GrcUr>?L`OyP0RK(gQ3+jEf6IPHG94(?!7R#lBPLAfv$eA@ zxmaRh@baGSbqWW8kjh=;o+$Zt7L5|*#iXidO+Yb?u&&|-g5d|pY?&R2`8GL%{@tCA zs!>1#SHvTe7!2v=0jL@Ja_-+C_~(=+C;$MnX#bn8(-Md&aN}s(SthFoZh-AqkoCz)%7O8T_xEumK8974W9+pr$~z!`u5$9UGmaLt!_G z&>G?-rNJYF24N;uKKy92-Ksmw73E_l@8;xFNzX4q(F)Z44Ao@)ejK>RO z^BKS$3VaasDCdp{=`%CWUOj~xq7y2XNle6P6=)hNI-EKIu{F*zXV;eK{;bOsLl$58 zz`h5zhtHo6=fjKu|NPJYeN`u-4ZNS8-^Oc}E_`GFq7?ldcy~Al;87cN%7(aITYz0Q zi$g5{Dvp6J3z^_n7ok5$F#uiG8z@|6tXn*V(#gD7c^i(TwxEEU^aNYG1_;&P7`uwai$*NUMLG9 zI7SnD`$=K-$rJ4l!Z-#8h3-rOFf7AKh63>1c#(3J2nBVf6t})P(WyLX~pXZUF3Z6sQQfv=gq!+!{46jqb zsoz`Ucg$ZbE}`go=US<=Mgh5j94tM-QU}zo{?_?s#DXu;{LfM7QwtcWt2the>i702 z{FHzYtsH_>Y{fkK&2>vk~%73I8ER5u0(dSBQ9_ZA4C ziJ-pF;DFQ*Dmdg9f?Eo3#S6mzpg;?)Dq$hWyn>40%$f6Zr)6d=JtIm2385m0n!eb7 zB8hlNI*Fzjd-#k|cf3N}E4G5U8Vk4OZ@-OkViG$X6)ep+G~uZ}UyaNlPCM)zv`nvQ z0lpe1B|CV-uZPUl|Wk9A6_stK+){c*;1GitC0OwwzKiW z41hx%0y`vZ2h#8~{XP7;u?DPSfKH$IM=n0$bRyow#m<000J{HSA~H)J&=7$s!j#0k zd;ooR5OQ)A`*|+nurhW1cQp`&Z#83p)4ak?nD1>z?($yT~crGQLmOeoF9*)29^3&eG zmq730b3z>$I;?)Pj%;j=CnJCoB)Jw9q3N*fL@_$W z0a?F50*~r=J`W6_pD{$Aje12PkGLXvHqYh;Xaac;S99VYSdav87U^0=x7c`n7y<|p zxMB8xuFD~ehCqC=lk@;chodJWBedk|2fKg!HLjoiiMRF~XP~*}oqoyB=(8l40a&`I z^Ev6wPC#QPP(wh=Ehs^(3ZwfM=m0Ehy-K`aXZFC^k1xp_iUaBa&NWGy7xKRb@lgL{ z{l}ipLAn6r#;IA}&m0noN2`E_#Ao&ZDbx)Iz#Q&s0{FoA{xwj3{o&#R;f36fN01^Q zYr|R?HrRnd2n=dq+6lpgWQRlQ8HoR52d@+VuzVQs^F(1vk*Wk=3bZ)&LOJ1l>%Acy zjF1Lk0cd<&KfH971~tw}cSPo~VcEfno|~D8!JNYOE6ZG-q1^L{F%5Q~J$u%|WSR3{ zJphAqsMRm;vc1NMQdV@(Hmt40i8wpEU+fu#uzA3H1mzg}vb8-CpSBeiB8UN?$LF#U z{w%L@5&9Df)t7Q#2GUEQ8#^_ngNDkUe1LI@GuT5EPwCK~O7LeYk?46oHN_2Lbxz5! z(CY=z8t!40vvHJ7@rTr6PVe%xkUw;c%+4^1Cg0A0TDSqo@$sqDc#kxo4xjbqr?YqC zF27@a4nKySFiZJV8yU%UMroIprTXj{khT-r zLx4hynXJ#}onac1!|$~SEC9oM8D|*J0O60?k4AkY1W9iXtP-d1(gbJ{AS|0W zG9tjt2U11;*YGR^!fl4m*NmW-b!W4VqL=gA=m2!J zI}?gzpVOIaxAkZ`NW%~E_ps|Zubxj3_lb52R8kbPLl}TOPtbq2#CAwR(=Zv$)N)3e z8Ed>^E@-!}h`B$_D;i&W6ike=KA+LTp^v#_wq8%UARN(0uoC2NNczyPyDSnNl6qj6 z`({M(=S7!7U-u#K*$m{qyk-NTpgrNjwN(CzJaLw_PmIOkDpp-SHgr*#W9x_#MI#5H z0OeW*6u%b5{)eGHi24VgJZQKDsh{dqInS1lSql&%(8=e_wg3@r@IsC3SPWz$09+^! zZ-qaf&d*({YGkxvO#yDA_qhqX~Ei9Q%7(`=u>p+{J&E8B02xy?ePg8+W=4G)nkt=CcsOVE~bw;aEGneWFoGfBr79m|u4 zv)>D2BVJ~71R%kGL0(zW$NU6aGE^1$w!stxAuUr{9HEZ+OP26g+6$_t{?tX_kBdit5!5+oJMY5fq$xdN&>u|6c-3P5@vxx2m-b+1jw+%>>E0q4hL_5LH(G|5|SwMV7FHSMmMS;L_W8! z=VTP1Q6D||BVt;;y6jZ z4^SwoPO=*UJ**lw;eS6oaT3Qs z28k>IA3ZyC_7Y8Ohvy@N#Crd1*RI!I+w~fD;L52pD4PHG1lxTSjGFmN6u@GT040DE zIQ>!!B$)|wZUC^u`)9##KLtPH+Dz+c`?dF@ zke7mwx_-I`bIL6hTZYZ+3wRWl`$d&e=7@zNJ4AUm+FHBpVcMB^E`of?wFG z4OW?SccQHRq6$+*h3>8Tg!yS{b5FDgvV`o!@;7- zP7s-17{$boqNfmMa$cAWN|qnR0FKR!#iorO>9p`1IOm}y`~$2%{s1}P_E#3SU&a?@ z)^6%p+~y@X@3bVs7Do!!0nmFmL;l&Mz~$@eoi(xFYSolK+G6)3O{s6T66LoY{J;jX zKe=g7fl~S7sZ_KDCM!Z-DW8T^IrdyJ2uEcz8r957tOIY%o|peVZXv?8Oe#?IP+&|3 z8=wf&+BQiQKn>&ylU4YcNJ6(irNyWmzW}zc?YwFoAwMpY9J}3+aF{TF=1_I3NKKXv~Y(F#HT=Hmd})U0cFA zrlH?lAumO<@!av9m$5e>HSOcsu))+VT!UH!hV$CWnEtzM8TD29u#112aA9$)+D{xe_rDOV<<_ppz{hdI0u)&CKujRRN^UP5^6)L_dI7 zpud^bvwP=Xz%p#Qehwb(+$l&%Q0F=SkAVLX?2`SE!Y7%4!e5(tqSXlo;0{f)=11gP zDS;3bZ$}5VQi16Fyzm|=LX7~gj3Yo}@TainR_-jik0ytjDmH)w^H!)qV`Z?BP{cD< z2OvCxE&vb$(1Ml)&LYZzhsXuzIQJ=C5G+K-2;Bf>K$^c4`T%eN?sZy%sz%mC_tPI? zhdVA@=mh>3p&KC;nE-+Ezj`|BjniftZ>qHw(br6|SWjixu|ehaSqxCj%&uba^23>Y z(dr7FZ5`r;?LAV!5NKmy>K~>v@wkubH#2Q#1ZcyOPVRN_s>EeDkBzUb-E7u{7i%0z zi!chffa7(Jy8+D?@z;@f&F1E+ls-_gp~KG8kTZcx6;>CP3k#v8W%&+=nJE`sMMpTl z5G3{;o4L|tzLLAWdHet#zqM*_i^~7?++sGC^OAfd0N}^wxbEwL4OniOk-;ZEAsq3k z>Q-tqy7^^p(O-?$Z!=XR?%5tf=@PAfW5reX8XSz4ItHzh_j+-}+I zCGs!td~N660}K;nBOQea{2LFBqtJ60V(!@3giplqLP$LX%vFmw=68hQM`W1ej%i?w zUUYH(WNRxJA1AC7;bpNo0F;vWdm6aZ+0{u=OKVEhE)fYKssX7l_ioAor% zIz5rz00#I1(mmnNAdxC&Kt-1 zo9DC$%nMb$(FE5kGY@1T=xyF;P^kN|Uc?8K2EzWdF327ExPb#uN^tA{?H2GcUvP-j zbWqz7ocbL9^a2P6u`o?xK-AO4umB_jP~ic`4$=<1BesCdjAc2f#bzo7T7U`$aEK`B z0J(qv6^K8;@tT@^{u_<7CYxq7bCR0kNME6lqXUpMxb}lM@SLc0GgasSxdrBpm|gw> z4|4ym>FJ)VozvL1@@y6ye#{+nZ>=3zt0K(M%)Us(9`8}vN49$eeVbI{kX zbo%pGF2ucuzj}byEWi6~I?3(Z$7%0p#;;jCga^6Z%LEsI5Rf~%iMLhi(@k5w9xjmp z>S!8m)z}XvDAhoM|S>~~qJ7%qRQci-j9dylVIb1=ugS*&3vV3(i>%3rx& zp5vZ3e&YQDo&0u0k&r?nAsjtGZ@_ZS)0wpufMYwo6Yg+`Jb0{rvOEM6KpDRzUcLB& zWyW0AaB+j_;5&G15c*%-I7b0M2QdH9suvSm06)w?`!4leg8z#b4&21ZwNG~K{$Twu z;lF#Y`jFn)82I1wDcOO+BoptCHh@bj!A1-`sWk9cfAcqRfXA`ZIN^~N*?U(+TT zz6AZT(*zI4Cq;IXh<8HzY06ENjuMF8fv9|h&~8FF^vwAeWTqyH0KkniObAC$b3dK_yx*iWup_3~RC&e-ecwmE zZF_tBG5=xOp<$v#kqusjJTH^K4e)!^& zcTFjbSH^7d8B%wX3P*gjIUNBuTsq5;h|xqOg62&cb0F%G`zzpa4iz9-riWPEs_Ab zzgf7QD^dEd+wGm*W!75P`PHx1dx>!RS9_K@oCfe>-%4S3g%*_`yBklV&g~1GWw!`B zE)Yv^)b5wN?^X;UB)gmwS4dxWKjb@na=A8D%EVy^BYfi4j37f6u*;SZP!QvjkFKc+ z2tt@)Be#mWB~8JywuNMnFFH+$tCnkl;d@!fx_Wsof}gs5@9y2ZFJGqb@5C!uVM%?x z2h$7o1IZLZ0L+O`z^iO{^e#3-y`17&EtGpubcDy?5n9T35e%Y=lX56B{(L{6K~Moo zjoy;Cg+76{gNA{uf$twg9)Xev_U+rd?_&XwvAlJBJXs8<*e~vWbo}Gv)C5#3H9O8v z-aH(7YnVlYsB{W&8ABjQj^`DxW$>cYk-`~#(pZIIU3 zl%zLvr@&k`%U-esuA!H32c`VKaqip~*niNPqx-Z}EIiq~{n0?%hnFtRYf6Y@OmlMM zk3Rl|Kg+f3B?0VJThzi#n?+#l0g5})p1er>v$rZnFk=8r0n?1csRRDTn1o3lc!*Y* zje^tXU>cn{hJTh;Wnjt@+*J1*rx1S3`mYK6w>ilhvwX$wkzx2yA*7Jk=Bs!QOo2$(EyhkrUnL*$hFT~i zMsf<;fUF`b%*k392nho~uz;j>gy(dSu%`hy_2R`GXFuv;QV^r!A4TH0=^Wx>Vh%DW zfC{Y1v`i6L_*ui)bKjn?zWRE5L~}FnTj(9`I$b}4KXR5HOb@CKc^##}Pt z#Ukf+pPQhB;9?O3$- zVo`G2bjvo~HHMcX*jL0jlA!EkZf2)Xuvc-+^HLczHBWSq9)dqm>@%zOa3G8|^(Z#Ov7J?tUG*>+U@qbzIa90PwU7c#P5K%RM~GLkdx# zpi>Gt+`>ohZuxf#%F$Ofc zV1M&hGy^gU2NQ=J2ShHtA)Wj5(!8M+*w5-cj9VG}QgNdKU;uw&oJf>@2l_lmASg8* zApx*s2Qcuq#C-{QGDU;lr$#`8VG;lz3(WgrIY9M*5d_i~9I*KZs+BHr}z+;WqzKwbP%guq6Z(~}w zi-|Mpbojk=hz|gm^EUfd^)`DQWn^)Ec``x15DHl`HcWseF4*OWggR6lG(5dp&n`8e zElvdbk$@QJC@!5{4$q&xw$bLv7n+;Hi|D)L2il4$SaluEtGP+fV)v&@k;NxV%d59> zY-%o6OT~`cPdXMpoxHZW3Y%uw=`80SRJ9_=oKfyWV1d|=uEaN`!a*f~j_)dn1j^ukhym0t;1t;IOD4h2Wq<^E z{j95LCmo4$u?^t2cjx;Y^56hm2L${*{u*Kpu}@U8?E2Quy?gg{GvRwAe1O=$0_G>G z@RxP7Z&U)W1xJMNL-~DRU5iJ|N{oL$hG7Tx?Y&H_cLHWe$i7NKZyweyOZ#r zLTnn2PrEfKKDiSF(Ifx}z?uVy75w{u*gqa(CO(ekZ)>YSv^gPusOu3z9Fki9Jw(?} zyd$b%X!=AOaZTjY>r0yNpnN#beF1QA)bxQ9Is9_-ow#_St=4 zrNYh`*T7jE{~flZCp8m}Om5wtZ@#uL;3=d?C+YD?yDPMMn-*Z>&Xd)Rjm2-CY}~FE z3Y)Rb6iF$cFV*b8_yEAX<0UcnnRp_*0YCCWtsEl(Fnf$`L2@hAWb;KR;|~n$ z&#vRW?564e@=6XU5fwz~j$5r{LF`c37eJ(Vll$YMD`7u+f5Y`-c6MO{kpWpjr@?&FUob(rtdXTu4MxLuX3Ubmi7PE za=}#koVZARBis(8<1JUnF(f@E1f@=%2SS6F_q_iVuEes7A+2LS*1IiZuaYs&$tu;+ z4?K{SStYueDyETfvAQvakXuOb&Gj6f}w=4=rKG^2^RO;C)uzEV?#y(T(`@lF8=wdWw4_&m zKi6C zxU8 zfn+l6aA5LDdSG>X#@BjVps}rz#%x~(=w$^^2vL@JcaDPOl z!I4vvnnD9$)&0DY;iVZ6mM2JWzapHf{s^%9F&N6whvrZ!J48ngW=OxUdA`s)aRa3H zVNP)_A%|wu*F>LlZr z(`^4^1PoX!wradC*-ng-5aD23u!S<qRhVz(WpE#`BUBzAK;!m8j#`=Xm%(@?)>)z+s8k>lNSH%YaJZ{xbNZ75S7I^Y`z1~E(CWoF zs{}&G6fQ@EQ_V8uLKQ`mjjBv|QiA{Jp5*z%LuiIBQ&0$WoV)M8@y1(kyzvunyuJ@O zVtf0rU_TjzOB?{hOD;afipA^*!{%eF2Rfdfj)aB~+55B~Nfr68@AAq!`4* z!4X&m>c;W2hB64b%}YIV{A+$-%84i`frKLfup)~x!oSLt7fJ|_zM^UvMpn?3qG5*_ zK!pO0h{}KPB7L-V^+C5R#K!~nO82Ans-y?Ex{U!@xZTO-&j;^5;$K##pHIz5*r{Vl+k3`BTK#pX(1#G>5R$ zge8CTEUy2jspgT((xFfH&si==T}%JW{f`(20AcQL5|4uanf*(|4?TXGZ>0dy2l+Hy z0d|AH=ydWcu7&jLOZeL;949@@vZl`voQ+g7P5#g0}`0Ox`WyZ-~f_| z5&+eLek}2XL(@* zR(9Xg8pDv9nSV}bhp%}D)~KQbAcEp|j)MUQPP z-0ZTYoAb-9o??G<$T#S*`y*E8c)uq+YdNu8E2I}UJB{Y2lWRrn1=Wrj$EjfjQL50w~ZrMnjBZ? zs)<5zdW7mhmu0M{l6%XZl0z{B$R3N&580Y8u4;_{DnhC9N?01-9oD~?I)ol&$L`nO z`01bir?=1nBwE1)Bo9bp*a^4gk>ZEgZISO1+d@(exePV4ml8!VBrOTzpbB-30A+{B z{lp?Lb3<-qegHa;3`p-f>qlq4e|&-ZA^-QYEsNvs=dGWAf9Az$o&BKaoSP!pLa>nn5bB?@_yz+Y?CI{A+ntjw zfRO@b1_vGJO{tUv8Jt2h#0&`K?=1wdA@{A0Uop%lOr)#AHx|K)elEQtxJ{35*n*M` zQ2g&#(lBa4N+#w@m<(uLKs$qi8jg^U%pSra=@&8Vf1w`%L_+ocDM5gT-lR$a{nN(> zMHRdufTM}NM&jW#J0!XtS88$1wk(!x46Glp&l@KLlw%-Z?y{EFXTDv7{4;Wa&VU)q zEFbX?ok@XyUt3yOv^8Tg)K-ij(J-*EJm6U>EW_)ZT=g_3dn`#OyZqLv3~GO=8Gp!k z*N0C`CKP1zFB$aP((TU!o^YYLBjZY@)8iq3xIgK0rjxD}%XrXRh}bvTFG{6Pgi?`Y zx{z94{WKIvxY91~mNil5SD_xAEV!JmMR%dFRdTt!QNJU#S$E+zVhh_Ls@to;QsT(T z#Aal6EIZrHxlg_8W#&l?62&!WMxfVT`I|t2QjArO!)dwSNvsj++_@ZmM)R!jBiLr2 zV%3EPLa79&hXhbD`+_lmz|^J|^ianlHL^(Ov*0$u-ZEMx(=qlfQ3B8!0V9)=@h^;O zsnSS%h}ieWPrSbKpZ@8sH{SZ{YtDH^tcLy!Wc3CE9_9nkG_hoa?XM9b;GY7P+^tY;Hjjub16nD1-9- zb~XV`IAR}{$~#H{RQQ-dH_KYj31pJnUH3`RdJc!22vVwa)jEttVb!h?Y^_YYk z*pOrW{K3pDn{#R?`OEkQNgeE9f#TTQ+@L}q7`l2jvfO;{=GldXB@YJvl2(ii^e4y0 zih*{dBofaZ%V|a^lNM+kY*0d!a9uH(3Y@}wV zoXd58DDR!@u*MzzA$y9ffcRDzN_}w9;@xs2R=h~>Zsl!0NGSQBt6m4JcKcW=Ao3Ck zc*d2MT8_athxUQvk0h>FnI2mkfgD?A&1q(Y((_6VsVlRq;9q&BpaB|a!C3$B&fSL( z(=yZTfPFn<6M||LxUfa5g0o;^VBPDo9bbpdFR8H-v}7C#vFUcRL!j*+3RugOEdynk ze^36|yyOCwAXAH*IaaF(TZJ1UOKkIK{(pJbuGc~TzW$ohp9wtI&fR-~-8G!!;DlVq z71aiIx9(4u5A+`uw?Y~p9O>%bB^9OyKvjTNh3NOC&)W&LhtF_PvT$G4hbeesz@S{m z88;IBP=L!`dwuHZPvxJhcW5@PqVsim4T zZ(-}(K^90cd@ki!*@P#CwOSg|SCA>PWy)C$Pi8FE4lMiA%~o5UngNUScmM21e(Y!W zUhZyr#&Q~nRl?dO_%9iPo??|L$@rkz`r`#*@W21VzyHHu|Mhzm0e|@2y_=u^&dsYw zZ(fz@=L>mmQ~N^#68Vp%9~OPMNp4+&=uGv_lpjuj5%OUwsD>Z$?Hknpatd!^6wj&6 zXSL5S?w*<98Jz$BsJwigPWD6KfRYGn+|k_3Jw^1>4Mtgj+w@|lfV?AP0CK_4^BEN* zyjJ}o_i&09x_b!z6aZj-^!C*EI@$pz_Cdpt-22={j-te%41 zGZ3^nf);0Xww|=EWL+NX*s#s&b*GEoa?l#Jr-Rk4ewmvK3*;S2*dHu|Hm;*h|F=1if>Oq(V6yBz7cXgp&V-%aW|Ez+XM z_`13>6%xV-7dy}(bB1!;Al!Vteg8glc%h#yv%=J*k!Xr>J^);gQ_9i!sE#?Cm%MRV)a_@0T@S6tjRB&iJbO|*yHAVZExRy%*1c!Yp?C(+Dii1 zt>am8U~&_pL7`{g4WiChh|MnzHx)M~h3qqI0CSpO=1Zy5HQjMb4oE9O^cnIT697oo zh*5UHGuQ*)gXl*>q*QJUBQ+k_l)Q><%C}jnXNRK@#cS)2j#CsofZ6-$lgR>@dVtzG zS+Lix7?z;R$!N3GNgbeS4Bye)VHXJi#Ylhz{p?1-v(L@nLRWEH!`p z+U_6Su?qlThPCdvKVu1oT|C2N(i*T@2a|05p$2F=0FD3D{Qvr2|IWSd{^Sq8!}#y7 zZ=OH@`S};8&Iui7tf!h7^Iv*oCj27%|G}Rk{zU*A0)Vu+i8wwYL>sn%fBHN-r&ST08j zvrlb_0IZON%>^Fd5qZ#>2n4q}T~-huPsr+Ubc|&>f(3st@~pI6jJ8ZRr-Px6P%#p& zyDZgQCXty8g=@9c)|D@>rNqbQS%$L^6Mgm?@yL|2sur8mE?(;<(m>DEK8x9kwns(r zFG#I|aX+*L-)86I$11oF199dA5gu1i5{HJJw+%63+er1Rkp-U2eV1K`;Iwg+J!(IG$ywb#mP- z7MWc_&`(jYy-4@H{+h1$x1Vy0G|F>|aaKL-7>9MCy5vao#CtEyOk2W>i^Mrn|6hKy zdK+#d2P*{HWU{zg?bMJ+0BRV^aPZN33YJZB;r7-R!x^8u#te4YVrRh-h9JWdo~;U- zJr|0eaZMKQam=d&?8D_eFq4QYlJ3(l6?U+9Pk zW^Nr?LXLg%o6EoO@!lW(?c=>OYw#T>CmlwsmkDwJg@GK@E=aX>I`(^o4*tk=m7ADT*?^a5WclU zgT)Bo9!K8Pb}%>U3mQlw--GDou|)|!^y2g=s2+*{axXhXn5*vxplG7wSDs>vft~?C zL&LyBqBm0R=q>^QK~ue1i{;_19)xn3oO#`_x9*m6056dDfzNWA!AJ+DDD#Llc_JMl zhw;BCzye4&BjN1XXgjIwFw_y6-0T<%EiV)Y-s6tN<&I)8nfW%myx?98IVYUqa45gI z>hZfHjxoP)3Z)Mu=G}~ak|)wmr_I%#aF?AIofcct8T46BIBm&-)z)K;#e#$R(j*f> zn=>?Nb(J9&ySt|1enj^24)>K>(P5AI{d9+Z_LA%~9PTVA;#b&m;ISrsF}Si6xi$M{ z6ssLI&M|@jw3}&|yzn;j$cRK+?r-B^{g~@MBuFNINM~+CyoGUxW#>M!0Vuk*a7?10 zGstb_#0u)IjOdWug!o=8BAo*xVijcoBtpJ{Z_7TMifVYk^t_1aG&}%FG#Ki1i*n0N ztoW+?Aj>f3`-!(8?}>!Bt=OiHSLEvK)^;5qs9>O`0Q?sZfJ^IuW;Z|#ygiD3-qSEX zi*VZFR*m@IyH}4wxO&}V^=htr-_D({Q(^4fcYI_`Bq7dQZ1~sF`3fS+26fKL#&5av z@JMtF4y~?k@h zodV=5-G8g*W%mjh=8l2Lawt@&Uf%sH>$`sB&nWfED=*m_cVIE&2q^H$RB5h@I{0}} z@YD6{y7&3X@BaF`-}&qBzdw&?J`&lcQ8s}>5QFj4WK)I{Z|9nr>toENx?4NC1T1ew z?o}Tyfc-kNL)7>Bd4ns(^%^o6 zRTW^cgT`0#31bg*gfwKp2hoxEBAJj19N=d0{*Q64OBfV25ULnqfvB1A<3JGL`uR7n zpW$7eu3!T`DbAB54*2PIa75Oaw}vef^1{L;Ys}a4a$>qt^CQ}U;YaO4z33?hn}<3E zhK4-N6kzO5k%Id}jQu)1&BY)e+>GRd$wH9dlH%f4etG`G$(3PiXp#udhOLv6%hphj z<*vV2pD71foX3ZFIN=&@54zX#E=wZNpT6bvWfHf1?sjDVEY@_-if=q=?eGEVB<*WG zg9&>IjgYW2l?__se#_X{U>BmEvK7si>cMvO29cGv`O90mmTf`@#J@blxxZHo7sxG| zwO2L;MLl9mM&}We{qkD0v_*k~AVp8c9(UdRa>1Qsc;ethm5j2xi!|B2ULSK+Tb1LlvkuNo22|Af^s2{2N@ln~IfWC~;u^aIegwj~DyDOj4I zdsNc1%IT**-qKyDbu>E!hKSptH6KSfZ|6QU7gT3KG4d|G{Rdxe;zy|r3eu{ zf*u5znfkp#k6@P;VyE(!{wf`VEcSxKpiTJ!Vs2P9)V)T0=oh1HG!2SdM`;$NH zKk?noo4@njy?)w%>i^HrfB*gWeWNgW2pXzlv3{CR{$#p$gyLPhzDJIjW7lB!1iwyq zg&+|QCNGQtT&;=4A-+Nb0kgTMa8_5Z3PA9m8RH-MLFt!*KnT8Sc@+iH64Zxc0}}FP z-Onrx>3f)!VZcOQ8!9l|AHGWa21+kVMSL0M0j<6GeV~`8S@mI+5gX$dr%6gb^OO4l zDkNn>FQB~v3IaF`+;cyj%@NWBz03|-{ITbU0>`k@?=(w$*sAw}Su-+7T~JVkH;~0@jBnA^x^(-9|Oe74Ny2scoY59~|_d zhZS*H$9zRe0u5UnnPE%DRV^)BGybBr=(3DEES~V1C25;=I|u!1HmiTyQOFdmF2+ID zaM?e2%O4tVA9f{u<(_cdof?Kwn#ZWZUdq56Yj;?~E?9UeFI12bILud~v3QwzwQ6Ga zTV$fi-kfS^qqR*JEJTk^UL8PbGtW121cUkbnp}it2vWqXWE1Fx{-6!1?R{U64yFvWe2@n~c z)}P45NxY`#LW#i*#@(#C^?+*NeYk`q1V)5rAV?d+v;4+R-^_&w2$ZVYl?xYUV)TD9 z7v!U+r0_*v-o8Fq*tXfr^B@H88B#W3RnguOT&=+E5W2I;i*Rb z2;?8gz}J}{(r{znqw3tq_?h#)VFLE%+kCx3{!Qfp)5&vsb6|6j^WC^}11RK=-c^c` zsR%YeDhs%!u^pgs`DND@4*gxHScoaB{*~<$eEvXJ*!{KZ7m1ww!yLcCCk5#wT~B4<#@;u z@%gO8nadJzI>P8}ZGU~+6~t68Owlq+nprtW(e>=nDUJxNoN3A5yax;_|}ezhy!c$7gH)HfJy96jox zbJC~8F;C|q2MUch#@f{GDE$>WJjDKAE!@G&VE)0EP=BOQ!sW1WY#P@O2!<5>Na}BG!5!oS zk`j^rI`)1eihW{sL?WypggYVq?-Vr1&B_O=@!3@*_BFK!>0=nkf&WO~bh zr&?j;De`>q5=OLDMk&PIVHyOOMLJKlh$lRQk$&~l#Yv{JgM*sbIu^G8PKjzSnQ-Ls zSzzM*iZ1kAL~ylD|j3F%y<4H03|LHAn-{5$ZV6 z&$yE6pUVH&IreXUhe^ioaQa{U{;#jn1H5=Kdj6DzHQ44;v^O;Wn$8K7Ckkl^2ueBp zHi&#e_2z#2!Hh$Obu}Q6rQJGmfV*-1T+`mQFw6H{Rvw_SA2dR zQZbFb2M{19ziIJJ3n3 z)>J(lad}rel9(jVFG2e)UY~zyR#?D&UY-HD7M^RCBHlzba+$EzD(NLlL)xnE^FGA@pv3I9xd4s zY@S?e59DK^=H z=Kt~_A>zEHmTrvfD_VVKMj&TA8=xJE1IsKX7hM3*0AeOG)NKn1WVX(hz`n0x%lTBz!m>!nn? zB;t^uL(1o2cJ^5fDH;C7(N@HTE{|btrBl<`N3ZRP1)G=t{cnHwSAS*GZ?VWS0@-LM zqrdTHAPY|#y3hD`-%$cE_b2{;{nr`(efK*`0Q?04WCF+~0svJ2{5Zua!O4pxfQcF* ze^3HA!8v4kpI}A*mv;T2mT(c|p?DDFt}LP?!r5(tlaikbfRtgNL!wTg9bcwFfQEpi z1p$m^av+kSIKXC)SRrjDfO=P@f$peS-w%JlgubcYLu4d?2ZebN@EMywYK8C#)w+>v6;Cfg4Z0A1;H zoH;jiSlEA=FnlI}uGn2>VsgV-uSI9LlL^<_ZOr|FIKh*xL3|CCxwBEs(|WAJkzE{N zb^-yx*0B$)FL5cT&^KK)vd%Yc)uU!&yH>B>8j8ri19GI_V`* zDEw8B*UT(n4~}|-Iz)C7*8zQJ4)~s!vN6cNRAJkjjXaN$>D!-yg$t^!YbT= zTFi~TgQLe?>NG=4;V=)v4j(vdGYZj%!Dz&8vn5aq%b=7hXY&~Sh5ldGb6fd> zB{5|Y5&((U>8pEd-Wb0!g^0(Bs!-U?PdWn?>(S5dn(az^+W!3C|LBi?134#@%$Z{( zx_*(9UY_22pHzoroQoYzzU=f8jPJpx&@&rPR@P^iE{0XZqP zZX+Ck$mehss3&Rw(XW}ETzY91Kmw1U2qMX_CiM=-j*trW|GcF9>9h%VR(k~MK^HQ0 z_|G3aCIK<{!c^djS``cWBi4^-KV~>c%=4c71r21VwIH&95(&7U5)+a`8VDF6;zQhc zkc6Pw4Q7Q*wY^)7VOA) z!=lDRkG1$`W2E37=$z$@%N@;I9d1X`=Xds#h6mfdvu;|!aOu-Lz7Z#aA%8ykdXVau9(-=b9c;X^CoUthJ(RO$k7$B$AaS%cc<_K zaRw%O0@iHSnjXszrdAvb{?d;2iveGII&2Sy!qvP#9871%67BJsx-GNjwPe<&84}qG z1%C~T0?8H8BvvXY7sO{7+K~YGMS%0mgT z94Zu$CL_-H1J|cT;=0|S0PN#x9sjGxBe`moSs8#8Rwk3cC#(v}1(%8*6@KC%Ro^nh z$1w?@#^JLdMZ!5PKAtLzUYU8yd!(L2(9=+Hq@cTP(hVP*F&@l+K4#oV(Sgb}cc1_{ zOep{+pcy~a26W(tn-~V2LKa}{s_%$$(z6x!uF8LvDJypC@@?Jha&?+^F?N?zIcqG04^<8cI|2z4q!ES z>BE2i_a~pS6ILt)8DtIy#y!m^(z4CIcT!Wo-}&x$e&^<2UxobhJHK=9^Y2age+~|C z{`?D~qe<&>(*0L(D*k2hA-+AaMv+dqQMwa_hO`6Tr>Wh3?s<;0AMhUoeo}yngkuD> zvG5T4S9}2;3M>wu5D$-Z5WNDcf*jN=4M*~vgL1z#_mJ^?Q% z5UegbdU~A2NKIL=7-2}aZcv>}U;Ap*b36q_J_ z%m6mO`35X8j*1xqfg;1M8zLPfKrQ`kJzy8+}{h*Wj!<^@EfhUXBWW*z-KCizJ2v9yj*vnU?7$37coRMUNk*}Ne zV)Refr7c;vLe``pLV*eTGX+yibkSxTE3<|oG13`Iv|7yz`51Qxxju}=OKv^)5(Fr3 z-3P^jd=m*Roo&MtY#?1^03c)Yg>Ux#M0eDdY(9Im@6x3||K}&y@SzJ?+nverao7T_ zH>H7plPKrN=fWcM`|tnycly86yPtEB0=_@R1wBSlK>!L^GnRZhkfqM8-o1%vMx29K z6-*C^{zH=hF^D6c_?Kr86GM~$)B+InYTX6&1J_Brk0~JpJkf=j*ULLt1%tS_@D%$M%KXcb9>X3h8TWE_%t%lLSESJH`5grz;* zK0VRSbk*vd;>h-8+np{4H2zfAMW@Z`@L9T&smbMg9qA!k*gHKKXdkR!NoA+H!krZx z;$dUAhMz@kG>q~Pr>Acve2MnVFr9qYU^=t% zkX(^;_yS#-Q0l2S+3pVXM||z$>98#rpFu(nVS#ED;J@5+u^sIpHo(`)M3!x8B2y+L zywfX2BK*SR$5Q-aQ+_Kaciowh`--{l%@pd~s|!Vk*vuV@{SFV&Z)KLz5m^S8oXq>^ zh4OZGWogW}rIjk{ep9n%d=^9US!AWBtQMb&muu!3yccFvn5I!7AbR#GUi{h?l2QlK z9Yh@XmO1^o>LoJ@(_<3IV)C&D__I5u`9Z7$G8j!TbOH8Jd3?Qxz&BJux&YDf1r^Bg zqZBvNX0e!}n<0Zb1TFj1*=*U-U=D+o`XMiS$JG3w_;2X=8bRuNGr!DQoDNv~F=H$y z9Z@(s$dl4#TooWxy&wLzoF(8zo!TXO|7#g?)$j?xDT% zvXypAzP;#nUvwcKht>Srw{kW0XOLbE7v~3>+1?u%XeDD8mp}UST}TtNHkyD@8IJ$? zpD%6Pp`|1EPxPN$8bJSEzBpIUGxfVl?5p=b|2t{}u96l08WiyAd3Zpl2wwr^Dymgq zbJh}81TO=Boy%hAYoA{YxPnwH0jY%Yo?3CvSbepQVQx}=t++Or9Tq-F_b@0R`kQXd z@!pZ9>xVQ>;9&{IBfO(&onhumc~sWFjXgn##XoQO1dI{%lz1pHe-*Tc00a0E)%AQ= zS{8;8WSv854s`hcoFs^qg5)@$`->|i5g;-Kx#s72o(LXN_V9Yg=xNNBZ(@uxgm)7q zfE=7Wlul46+S_jhoWmC9L#xAik)_H^ENk^mC(@~C+%lNRSmJAmz^^%c&VElIZSlFo zxQ16SrL6?5&I0=|QIrCtus} z%R#rhJrP?AI=sHmobIvqo~Szz4ksU4+IZH(N3iwla{B@a zf1EM2ZQ9M0J4449XM8v8Lt3U{_GzN5sS_YIv*YZpiM)oQ09Qb$zkRr1Eku?)Ql||h zIsHA6P#_cx2KBy}P9!#=`?f4LThyb#bKP88TZ!XFkjn7OEno`+U54U%&nw)t} zSq4l!+}L64Mb{+(D5rsa+8k`*-|8;DI+6?a*DIRl)nE`X&;W;L&&L6{p6mc$cjXoA zR8YZGwK5`O`5CXazR02l^b_NmAbX%yvIgI>#cT1E9@N~KEM@;RtpM&rFq)&^uDyE7 z4pz(`uaPB1X{I_s$wB0O$a^+-9BzL*n(|Zyc)rmh3_Bv$4s1avTThVAv{_0=<_3!ZKd&U2G zs6b2th$Jo0!UYiUXEcB{bUR4!(=XHfH@tu8)6LN@xX?I+5XIbuuOo5rmEsQ)f@nWS%Js>^%d+isb@3t{lq zXM>!`R?uAF8mp&$s>|M6^E)0|YFFw$_ssK$)32h5=XamFEia$Hd>)ARIBj>Q6TXVK z>(<)c_Tic7ME0rO=4#0~+E?xdT(8!q+!taF-&1eUU2-AiIN|nn`7U_jrdiY0s^5Wo zT`I-7V4sO6T=2}3E1Zm_6G<5TLD9ALKrG7ugKCm|#6%lt2*>UPUvb4G?@0 zPPFkfsY(MBRRGqGf&8Hu9j6FU3!s%=giqxM&}GYbiGIGC0mu9MF2DcP{Tbw*5!E7F zZPq&BMMtat$sh)ej@8B0q7|kNwY4Wr<6r?AOooEDt6Nd#h3tu!-9841$TZpO%o+R% zr!}~+SOwA3GmP835F*h`@yb+=qCZ!H6$PX~#Pd&P9NRHpE>WnE16yJ%!^1EE%TNGT z#^e_1Oa=nHbb-Bw!`2QQMB7>=Ew^-=4W&JA++T5=@U&qkgq%3qu}kw9)srJ`4m98F zYrRPefKBFK6YAsu$^e@G&wmFNAUEieY#zZwgRF2O-X#Ra=?;pfo`4)63_$ZgLJ183 zVf|zQf>rZ0qEOU&HPuEDOyYlyrCpK<4L{*}>fE{On4BCt_s)%T+P^!buLIEIm2dp;!_ zY$ow=xDrbYN4M9WKOY=?KA3R~_Dm1A58wAX19$H_ZLh|h!}nta`}X~Cmn~tnJ$`0! zf7RlfTA2uK-nUOpTpaX68I1bVF25xm%G=t*Ed2S_;z8F+w%uYMTR|VH6mRVDjxe~G zrF@Ngqq9Vp__~#~j57g9utn618kZ_aeQI(8z25l`-@}ReJt&P(7_4Y^@t4=|n^Et9 zxL6dg-^bRYqmZ(1)gCb1^I>q}tsr_cqAuUh2-~+Z8-sHS0maH;1cPcP;2;o zz;MTa%MTzxNY-nelm$SKW~0A_tKssk>wx}>0HB$J2*3pUzW3iZ5a70G0sABi!nB`U zP|3~8AQ;{cHNzv~$2beKN<;6F(%M$uzX|lHK<@<<+F5LvVxlbsyOC*4HBx+wQDMX!q}5$kmv40H=+ogOl~GLZlhdXJI9UOvb^^wJW?? zqtUvL0Gl08yal?zb^}c!B@GR@_0`TCaWZ zt6QCWUhj=t`%j+ilZ(KOOV|r7@!vat^5oH@totMQFH?M8PD73X_fzE$2tfFomlu%d z&dE*_A;4pbT0&W1oPv%}lzN}B^W@+cO;B`U-qN@eh$a$=S|YaTpdkV^5Pt5%J7|fC zp^wYxrAvq$pSy7hw*zhGDZ=%bBnrNZImMe~K_LAR@{CYDGbPk1ju`My9#Ma<$pa}# zZ9B71W47uUS5NA)tHm3iD%|&DLAo4&B@ady49!RFi4g zED1ANr%U5E5-Hhg1a@wCwH-LUy#%T0{(U42+^jWQS^VqRxb_XSrwAe% zXgzMMa!@ZmxxENZ`bzinW=Z(g5FlVZI{R&znyt$jymq@OL--9o%J1>{^GC7F*6Dv*Mw#CW z3;dow;5>9WqW}E&r{+`vNC?aX1mV(TtUl@di63A%HTy&%p(-G5Ke<6`b@&C+K{H<0 z5-#@!J`_GD_b^$A$U;~lOUwTS!lC$?;5qyS&yG?bTX8{*Z_;jXx+_XmD9D1iF$4g2 zfPEk&LFymIB-FSP3VkFIfg8w*jOVHs8T{~90RKU$8VLF4`;(}QmKikw!axTh0pTJ} zIsh}Ewc*8N5guj7c^0C80Z5ah{G$yF-8?_qcJyTH&DQz0Ryjp0UB-WNE~A(V6H$y)Dy|@aLkw_Ea=G=zd(Wja5hd zmX}=*A0}4(p?Y`CHeB}IN_?9gw8X|9PE91HhLH|u|KQ=+{gsC=pL$tLeBR~S^uZ3u zcE)U$a@k_r^4i)n?IC}a{x<8j$+R}MhS*;j8Q2)$GYVL4UnXABY%B_pl)YOn!%p#8 zhRZELUbU^&!m`IRFwc>%-NOMKWtV}K5umaL9KnWgv1o}}Z(f)zpyCC<#|y4S)e^FQ z@cpxl@8mGb6abhxVdI$XWMY1Rh=g>YQ`=MkkHGucon@9y9sm){*ufy#3qpxsT6zR7 z7ek3^4FN4J_?X=oF^|{~7TaLwYp=icx`Ko2YgvKqf#VMWiCJa?H*Bo_D2u>8pfZ_; zD~I1Yn4OE|oSMKu1-oGubgO(&ji4bP0vZeop+%#O;$4Sva9EF^ePQTo!LE(bn?B%unq zFB};k%5^Vq75y#Hd;mgTy?R#B z!XggnEl;1_<#%ok*}7*>(WWYGVhm%w-M#x)^3@M^zkkK~zwE!t0g;~qktUJ*a8~g0m+P5#=J-4PN?v8c!Wd7mh^sDLT zQ?oM`Oa&kMqM1N@yFVAR_1KYxD{Z3uVR7bjC7TuKAJ1M?w)o9JoMfs;oj?q6@0I9` zY>w8lq?k2mXo&v#W#YY>THu#uV4!1Zz7+#n985p}B1J}50bzd5P$(4Pokovq4Fz36 zfGw~z6#Q!s(Atl&H0*X$*jgMe%7Y+xvm$+AJUAu6t>f0b((a$+uddXpTO~1p?LMq` zdLNmK+MrP*5Og90^8Doa(`znu0fOlXd#&`K|GSea;QjZN5gH(%kVpkG*Ks+8nXQIW z2{{A0Wr#@(E?A(&v5RCvbk0c2MTj31W}yc-{%@%t#U@n04c$YuAd_NYPH+QDSAd9Vwn6+CRl-M)%*S@3k!er!Je-xnZWox>U?ay zK-zAcxpAG~KhN2Jy5Wp>rtkENIdm}z`xj&b{&4V<0!C?uC7q{D0CT2lB2Vbc-hU%5SKOqR13)9(>V1J_cAh`>$`!|sXz+2!$-J> zfIZ+pWdS1hT+{UzWCrZWPO0Q)&ac>40wCad#nwM1$}xcoKPh-Zx}FCcRR1{*`L9#C zG;lp0?8Ujp&vNec9Jjro)ZuO<02G2{8b+a`%pg@Tx`!QebFxLBJ3mV6uSVeNRgeHr z^HM$qHZ~rNd#&l9wZA{O;tJXqth46MwD$17RRlaw zynLAjvF_@DR}@=o4{y5zgTLlLA$rXl9(2^bJyw?g;}chn@isQI6RBd^+h_y0h$Hb{z;&0|5L-mM|mQ zh=gFgDgp4DPmk}z8CVv059;^|F!57?jmsZIzx<+*d`?dp{Ii9}nmdC10iFiXj%azN`Y7`Do0Aw%BUJ$(2fBsj2MDhK2ADaLKztYN1miJ8Q zuO|KnWzYt2u+HeyOXcqnV-9r`NIqI9pqOs~2#KTGz=LAT)yV|`ktGCa46p=T(H5|L zLNu8q;5f#i3u{Y{NQ}9q|C#9OvH_S%VG!jsvJCc)^#>pq>gt)z*%9*cC1mGp#r7on ztm4f>s7yzK_F8WXs()@Z4aF%I8%SnJ|0v6fYAYvFriWu!7rCmSOpRU z6UQH;U#&9>1$aUH^D(d04`2@Tg6wA20{AQ4pgDe0vH%_iAOlJt+;hj=i&H!VsgPVr zfrK|BGHERShSD7t3?PEb9&31mth|a(-F+PINn~Gk zb@}6yj)}3dJ!ti3I6Az(bjUsR*{!V4Io0DEZntmOBLx4;35(@cVr*(Kf1zyY`SxEQ zj;#&8{C0SFW%&8am*0N-mv=w2xP$3@yxp<2*xn8~*_s%O#k;z^GZ}|J!<;YfiX>Aa z`?;a}LREdX=Jpa#l&vv-B`6PB44)p0p=V3CUcbE%Tn?Fw8s9^xdzP9#$v|WQ(-Ppm zB;meT+2S9}6$m>+yQ^n^jysi_1Fq9WueHr(7v%B}ulb8Y2&g z8mIL>SUzNp3?oG7fTaiYypsLmgzbC~m6N){9&rJ!;vL_67@&aIlaU{*zM|(Ce!t2I zhW{`C$tQ|;B35O5FBjo#guc3U^@<0_aS5Q8VxIxhQTGT$2T}AXp?PQ+Jzr^`V4Kk= z1^2q*?Q8gVawTRJs#Ejpo9SsE3^<+W|5i{)fc9f%f3QFNxot4T z7<0Mypu~2pNCU7yaQoA0xm;gdSUSs+*z)QY=)p|cUPzu`82IUz>sNT5?l3A5`P%v? zR~}s{Z6f_qPTALcm3l;47BYzuLQ8r0As)U7OFM8ZS)fG4P??Rpl0!ph&o(c+#-tm( zwsM!$pCP#f=}p?Zrm}GWpxWe}zx(t5_}dR2v1Dk^7w!z4J<6$nP9}NO`7c!A@)m{c z^KAa{9)E%TuChA&0!V;xnIkv|3dwpOA)b-^5$X2`2|#K<49x|fkpPYuD$srz^9sC! zBmfuyum99RJ}J^Zg@e#{@&g4r2m*72V>kLnzj*H*<`YE!4bJ~dH%_V85KO=@z!pFWLJp!b2;e)L0%ixFnlrJli*ZS7^FZI{Cr>0#^mjOrRv%9e zW@C$y=8&~NP|v5^dlF7deLL>*VZb&$J3TcK%d`&$UC96>0h^^gp7JzL7CMqCcc^(I zGx6E2377TOlt1eXKbuC|#d&KuV;fAoe4cRMp9$N)olXSZJ^#Aqp6KyTCoF4!+4KB2 zt#)gNZN(Q{+;Ro|wQ49Cbh|CKcr@$t)`O1WIEGa9mXT)&z)sJ$m~GK+Y`u{2h-dsb zPR-1!p2f_+F1}ikHlNlXwc>gC$Po3`2-il@wH!gMm{L9hbA&r>6v3^7%G86P-~$e9`2;Kc516GO-Pa(2k{RGa zgqJEL%2ts`M=a0c70|-_5(q*gOvM8Gkt3B_<$yK;4>T&40}TBRSH>o?**G2i6uY4I z+#^D`IMWzXq3w$!SSeA%jF0_W0xA(Pfb0+CLelM(TF00EdUsU6rjMhiHEp% zcEJQoV>UXON9G;7#cUq(e`_0nR1te+JLVPi00_b0#O!oZ2*jQuQkh>u5c{A1{f~a7 z`?xreD6%drzt`6H4rzc;=a(H{yry$}Kn6HXD4h9TR(Tlb@HsdDgb1NQENLlF8+&gP z@}lS}0T>Gpy+|sET|NMQf<`zSu?|PT;Amt4oz}+TgS{Nag4)(6I}qVt^o?G^)$E;1 zf2SekIm}MyIJukDh?7Y)0u&iQ6$=1?Z~!*<32lS}#OFDpTw{`rm;>7K;cFZF{XD=C z;Sc(Hekh(2W?(cle3q0o_1;*R>QgXx0TZp&B7i=8cYn*}-IDsmN33BXU6 z*W`DDc(=@p4SO$MCgP;Vk3H|k@if183*&Y-|wTjv|f)OLsO9QU{gYANyxGcbj)d24LHcVclvJ7IbO5rgO( zL>a=VQ^`Q%ex3f57RDcBo$W%Sa!_F|nMb@!kI~Q-2AhBdpgVT!dq~P3_^tE69z1_v z-+7?Z>&9MVqKo4`wIxzcuFg3R`_734I8+2=MZ9*jxfuemS5HE(bQ1nk2(~yDYqeo# zdbyCd1G#uHfP+Okf9HukW;~=V32_Fh>u?a5!$7tUMw-vQ_t9-}Cv)U2@?*j|m<|NPi!15r2rYxOxDb1UJC!FFEN_UYI&{w?H-GeoQaCoD|2;N) zKQ{5wyZO=lr7!;O?>_wJzxcgh`GCEQ5?$KlBwF-m`$iS-bNofP>#8_E#vS{-UY$V) zkaR#xpaftvfeC1FfT8L(=za=s>2)alfB=b07KAA7#nnOUPeCro?;taKGKE@tt^I3) z&tLk3gWzyThGJ|sAdAx**P#R8=KZ!j#|#AbX45(0iF5_f_0L@=NSnmmRoid&|HS?` z6G)8&xMP#ZKSxXjK`pOH9z_t`^!buA0_f9D1JNzmp9#Q=^Zfk!>B$P*4EdjoM;;^< zG(Leh&<+g&KmpI6(c9BUZ+<=meR`-rU~yO+7cW}8EbOn{btIi|0mm%A_L;-VQgBa~ zb7I973a29d&VQJ2xz?VBlReL(`9PB9d9u7>D=bo(`JQ>(yY9v#4p;P5IOu5(RkxNp z@@v!1v2UjzJ`Xrp47Qcg?+@6Y`mJ`m^;Vok{$iTmygB4bdl)-8Y@v`dl);85gAx>5 zb-sXYg^?Sayo`*>%ObkP!IpgRY_uWb^|DYF&!&pOg@ug>(S4Q#u*BPq0?>ZYo zEr%jv0l@{5Sz!db(hfEOaqnW`#AS2av92Zh3ycOmd>*9$mkU~5lpv!vm8TXO;+2>% zFbaNu-p3*$dR1B>GE+c~Ie7|-@kcT!sZ)`Ar?K<`dEX;(e&otDfa{hE=q><$2s%K2 zLI7WTjhz6gff{!(*nj$*T6YP8X|iwZKdu0h!7mm-C!K!iKS***T0rH2ut9HBDMF=# z5g-EJMxbX+L|)>ONJaU`=Zs1 zg>;d$=5rOV&5_?E+N<`k8J{de1&+-!pLe4A>c{o3o8}>>@*zk8hL8IF!q z&;iE;AFfW{pooH8$?-RS^e2Aejh}dpe*}#lCMJ3&EPVQB0^AG?0&p-O-jSt^@Z;tXC|2uWO zO=qO~qiO)ZY|R9ynTZonv+vIn{X)!$Jr&mm`lAUa*v#?Hs-DcjM5z<~k4iwNJbk#~ z{^>JDDU5z!0S$ae;Z8n~g%BM8;CvHh|2utc?|ec0&)qPbBL;G&j)zlZMvB+3A0!c- z!>kuFf`G_#*A1UoML(<|74zH^A^>-m^q2`}w8+)ppMK{C{k%?g&G@+V!!+^3qBnAg zu17hJA6E@Og@9TBsvzlut0K6HqWO*+qXhow+1AmkHyQuK2_70k{;xmcayk;@7kegH z(MdRe?Khui69J51J{$9e($43%(gExE35TVlt$#3(_W0b(o`5CQ6A1B35C}Q~=}E8O z>de>zgU@Xp7oB!*D0FX#8S+3gKw!G->?$mW3OJtR&X(TdjCEKGqW()Xk;aWF^$0#R8!nx zvXgi2+>v7Gdr*B~`ffk~psEkH2i5v77dPhTKRo*(NnmL{5=@H1@J2!2%tUlYT+cpPG0HHKUP z&BCvcggQHZ#_Owhi%YWy%`5Q;HN+pYDafJ?O};3AH4+lfmhyq;s_H=`!p?HG>$5>; zVX;)n$u77K=^pAOm(_tjpjP{_j%p_xSW{RmF%gZg#oaDirU)qiZFM;1Y|4SPLokn? z(0wWol4URy7(a1upd()`k+b~#I~V=lY7P+=#)9FkniPr-?0t>!C+BG^QhJQ6Td-%% zhGsv%E<;dKTAMqpd443h2fJuUt>Lk;9v}$ZKH~T9|HHrh%PqgRcosKt8tfZ4AOMWs zxPi^DKtSpA!MsJf^AsQI&?Lqc_KJfupk?X+gplhN#g3>rI?_3n%{*W2{+l%7H26Q< zLR253(Wk{{BEY^4kbu^7s2U*HawkamjS>PO0lr*Y0B`{MZj6eVz!cz%8)rcCHJEP_ z%?RcXTg1k@*Z7jr2dJh8)1%-ATQtiAA`}{yKe*r#1$aaB+mtpqd`kt4pNzxYsDd+$ zFw;x!$G}fL&l$;{Yh0)vfPVpzlIPA-0|12rhCl>@EPZo`qaVs->&gCc_VE3bbwNv4 zJNY)9KIuu0pBQ9s))Psm2Zt^FLy<(11)7s5LpE#MQRI3#MbQhTSre`9Xyt>JPafTB{I|^{ku89Ugo^o+~7V*5>)MV)}&S z%*n&!Arj)aZ>9rSn*WgVUqt}rfl&W2t24e~_-pLPswm(h0dN&X1S&CbECEYZsK!y` zi@6zF*XsH3>TQX7V;aXwY);n5k^^) zymNbT6>oN5$~TOgPaN2fM-_&TeE9k!S?65YvX8-?CNCjztSv$?2%b(XKGrkUm9WM6 z38HExIBmlw0TAx22P3;t0?hQcZ_5yQX(v7JeDPBl3-}%(eegbJ)R2N%h<}5jAw=qRA<~q@PEbObftkg#!&2m^Sf3XSFYURVx}zC-xWz4PIv55WQENK6a? ziD;6Gr2Kfr$HR9Wy%#pyc)xsPPS6i^>%v4|f?pdZ1l7G4Dn zNd&qvod8eZAQ!pijDSxbo$Cdeud($=wwP18IK@};{U?6S z>KGmzPdcD>r=96$O#hlsLaJhZw!AdoobF(|ijP~5LLYAJ#~2J}prd4wqiv(3tPukX>BGdt})~<34|9 zAW}$~(sHPoU|gmUUs_%cO8hJ7?8(Ft{`i*o$Dr8j#j}cp0OUWy6o85|$l(#uW1c0~ z5WkQ|0s4(GdC1|iCOz}-ojp6hG%&zV)8U~(@Hi4SC^Tl*RG5IsLCQ)Iz(KwzT1cpm z%GA&8g^y$-?3bH3vA4(vc0EKuIBDS0P zDAi!cwao`IP>I(4wjkKCOgNXX0R^%lNMap_MFg|dxA`CkK?u4)s$PYO$YaaYzXeg$ z_y6f9biKbl@+=x>Efl!Hiso$V(a&#sSVp5m0rtnmer3WDXty?}EwlHZ{p-K{+pjej zj{T#B&q4-|0EXiGcCf z*$Lt}r(!VLLGl%$=%8Rft>A^xqw<~|AvF@pJ&^a`G<(4U1jrkFcc}rBzQ`jC|Bunr z)59>_B*?{fXc9S~vDv7+M!yd%s4*c$fQkkx55PZsg2jUb4W&cc;i3i~UtU;hVNu5#x0eNL7T#rS;Rd}MxrN0>i*={-2y@3qa(zXut&Bf>GQRy;|^eFvm{ zzIC2_Kwqv(;VePCG!NsGL@|KbnA5U?A;(|dz3a~2U3<3nZETu)e|l}^=?oOMSrFq$ zQP_{MEAAlr2ZL7Jr%5e*Xl8-)D3*VCcHT1&ABgzx@K`bLv|1$!09M#Mu4e*CII?hi z5our6Y3rKl3+t0%QXH1NcedpKhCxKWVs|%#wVE9U>o6dMOs`Wwf82#I7TW$3ukEA^ z7t$|5K_vYvz+gaiuf4WM_TA_c%IZh99dP|I1LDZWkdP1d89#9T?t1NKUwcCWK~nS@ zkyx;RZaK3cHikgfwPh7ZWG%4zNK$;cuSd*FG*j%2#f6}I@eotQ!R}!M zhc9wA!%o0N#v7$Mw-d-#?fMl{i1W!asU+FZul8L&AV*ay2|f1%k2`&X=^)-;prB0M z#I2Uo?qcVG$7Bh60n;$9emtJT)k+11zikj<&i>!O*H4-c*DEbsDb&ZY6_E|8%!+bs z2S0wD@c-$b{;{8a3zG2c3=Bgb9)Hp+Nb^7EE0-Aoh*|hKUp@VY|NOUq z`?aAZdn(e__rV{K0Y!_Q4%A^C?R~s}av&%2 zd0XpIRtx!L>pOFd7Ea!j@jA)jBpIM>VE!ceV`-?BYoNKUO#|M6`A`zThq&bz?}z>T z_Xu{Tc4-emiT)rk-g^`d1n9h@XXWYr1NWb;P$Oo>?tTkL?Agk+0Rga#3*$tvv_x__ z>+z&j%MYdlK{K;1ZiJSR{ouTRPjV8i14|!Xdhf%?GUdN&gZT~2yCZs1&HX@DIQZwQ zQJAZFwf~#Mnj#5KFLpO<6mvXFr#7b{KGk@qm+`3i#&)WxS}F z1bzDCwbddMjb+ocXvi5v(CCwQl~g(jU~jI%un|shIV->9O3U%bk9#|f<{%H*QgR#7 zk_Q!OnPtMLv5b3QN{7Pet8X&YK)+YnYsC0?FzBFWKm7i_?rI^m*|~k-LdD&lq$s1p z+F0<%Dk!nk7bimibtj;aZCqRQLJiI`NI*^$H}3xaf=Co)G+3=1E4O8tgc(T9US*uQ z{ZBvkV?X^%KlKxDd<7Bl?h4%<9KdqiLNal(+H9xelO^S}tWL5@vUrznzxvDnrT`dk zUGn#I9G$y#;~bGj;A^E;BzzKpQTaC#nv(aB2nS4f5#{OW-{$j1uTL{SWBWz>uT!2t z=G+JFQwdIt6YNHBPg6df%z7*%+$$YFVwl$?2F1N-f_$XvoeXg9pbl}Wc7!*8{t59@ zMzTlWR$~K`Gid8+R_F>)7B$H82N zIxG`ii|9!t20_0(A@Z#Ui2xP^-|3_BKiPIu`&AVD+OX1je;$UlUbIhqURD3+oca_5 zeRHR|$5kqUt9{t8ULBo#=WO#)P=KMECy$=&2jCeJ+Th{j;`j()ZMk+c)7aE4lg zA&!tFT3^dBfm_=KnvcFCO=J3x5ARIc{lyM78nm`BicytmCV)b#_Np8b%h=Uph2gNV zVJESHA7FM#{#NTy@3Y8{-iP!8fvv|P^NLN#KO+5f%6-!?gVoFTf``q>Yyjks=mkY+ zDB{a|n1Ci2*~NqVt1`H^MKmC@m`^?M8rN>leG&P)rDfJBKQew|e7w-oqd>{~!l{B< zCT18RIsr0hh~fE{A8YX+%Mx6F`7XvE45>6G1U$H432_c_KV)L>_8N<0Apewwf30+y zfnLY5gXKj=bU<`wbC}ve!vfiw?L$-?1EHMINuWYk;NqYQ-9sr3iOA~;rIDA^L4R$t zruj@}$~pKMKAiFq#B2b5@@A?43L8$#jZ>cTXqfANmyutbZ)Bp6n-QpR6||Mhq^Ix~xL^I!h!fBxHV{$X=#v#aL>`>S6x5lEbU zV)MKSK-XB?7nHBVmcUd$Abkx5mU@9V2`Mvb6WNy!$O6WLkGGoX8}OZFK&f~$`BM?W z9rY;sB=5>a$Os6?+?Ub6j%Q+*G{g7CG*}sk>;fJ^P)d4S;G%9+Y?C3#9>hOI0IZ$; zh6$uhK$owG2Vc;$1%QMQz(cDIcoPkQL)<}2%f`R#oLr3zqmfaDF6JDX2+#;{i59?T zWP6~MVX}eBfft~k;y$TqF#Z8M(Cmz)Qlar7lJv<)k-F>ClD(%3IhIV9r5;X`0GPn` zpJ?uvyrM)y^jAtNpWhog`8j`R_xT9rHEz`_{U!Jn{(YZ+e)Qx?qrJp-p$3fdWxm&Z z0>MU18v;)3TLuSZ=ZY+`EMfuu>Dx^ydV;!L4o~>dY8yP+?(nUA`|AF2d^=X|K6~}* zY0J}@a^6M(pX|UY3dFIs86jAP1<=F*0|LBnMH;T77~0tA@C=-7Z4RZ`2VyRG2dhKY zUh{dm&f$R!sKqMq@*-yxr1ltXEWF=BaW!9eQ{QWyuc}~W4Y;1kaBQ+{$m;H^XPi>@ ztS?Lutn+!p?H8F3z>cF|nt}wo(f82CUru(QV*be1Uvl7*1V$C~GSc2~WJCwv`}+F? zCu_gD(Ej;>2f)-0pR?opwgpC_s>O2B79<@6dxV*oMhbotLo?8T+v}IHBZ;CntWrN3 zgPiCl?TA635v3U^sxs-610?K3wuspwh5*2SNF=g=ke4sa50n!Qp{k>lwP_L%rKb4` z41?zc_CXqiE8UJ}Q%##pOhkKb4F-OV;-7rIk&4Ne5NzQy342_;m<&@<$$Pz4PO0MB zu;(t^zaUMND+~+0om4kGueaWM14$qj1}`5sq|UXOSFczTqSj*rNE}fm0Y59+_}Adl z{OZ}i{^!5^+i(8LeKNNrRLAfydMSpGf)I=q2=N`WqxcBh1T-@I+H$ z@TGF(p|0m(CnM=`Xz>-|^bSU^U+er733GVf~nuAd>)e{)_U+nXg(w%>dxv4?P1n@;)hIM>=_;c_RYnM~>+Z z2k1++)YH{QR`75ol{{NxsTw|NQzcbEM^~KiRWKFd-dzx*9$flMPWUG$n)R!y< zJIRE=NgSgD<>EwfEISt|cqkDB1R)7&UUS{(jqwT97(csr`sDn(@ zGg0t6;|{PuMtLHuivLU3FXadhetHA?4Wxh5887iYHpcOurDvZ7wC9vdR*;;R+a;n` z&`~{+JVJHZu_Tx{@gO~4Gh>TExhJX_sChi}&g{(R^()N5FPg5_Z)AJ{Fo}INLqjTX z$hU}M;Mz?J;f6+gggOVj$H(VD|?^jRTxZvH?|l?On|R zJ`o_Lc9x{KFL|N=>{(`-wKd{2pFO)k3aDVmSr&Kbz)Bf25}XJIU>Y7!y+N40`R4CY z2518}-G^>N61?6y&&5AB{OvO9&Xrljdsjp^g1yX2a1=wf*&6^FFqQ=dGVwq-9yMWP zyOU+o(yk7-CCXD6ouJ&Tr}z*p(c#Ch2>vT5C#MllV>v-m=p_n(v8Ai4k%tr=Ytl;- zrAd~=h7$M&1c@>V!f)=}QFs0Mc$*zfK#HD7pDoUoDJ)NP%9sT@Km@(vO*H^N-<^Mo zij%Enu<`sC+hNHv22%kbeys7u6CO?=>c|xUpm;|{ldM2|?IREHv1GXg>Pwg3X!&u{ zg8UI`LIU))lK2oK-*^Kn{*7d>3Jl{Nz^htxqEqv^KUA*Szwftn_#fXV7ZHF!9p~y1 z2wXY-852-F9(5kgDQEyV{2?pj-mkg8`8o6>8{;sOvE)-^OvN;$l(#QAMV_BV*Xy91 z094*rw|;~G?gm%X`G4@x`NIbfb+(@{V**frZ3EE&7*en@0tvvMvL3tE1p%OOqBdn^ zxzn?ZF&hq10R+Ci7Qg@g{LT6MH}4btL@t01*H=%!`s%B1zWL^>XT(0AB%rzY+z3ug zT`wmPTq6OXG}cRAA@T?DllIR)y*W6d=eTP$-<-$^p-A6z1#$4L7Q9HtiSafGOBiQI zE@gn|m7Zvem85`3yf22-UvqJK5fexRNR}>KICfT4Kcf2xuzxN5a1^c`fLRNKKj={d zeLkPBg$elKsWWG~YpgCG_GfJlE0uqL|FNaelKOUZ@ETmD$5BB-!XiJ+-U#75~gQIyR%Nj zKUe|iaqEbO{0b*fn}MxFl;U5xfV`khU-5WbHL7pcC43N};5W$(t+MTpluAHx&;UBH z0I<)FH6Utm?P!fZuy7VqBQs4Vcont|L=rCFXr<~mh9gn~GPwoLU%uof?$PV;v^iR2 z^i7TrF0HN%7*`Uo4H&)B_8qm^pyoH^W;?(o_*vP{(a@0WNnVb|9ZB34=kS(II_T9K z8a_I9^|Nx3<&4SU=b2F?9pYZk6n~Am0m#|2qDnF|l&5)2>K=|+x)d1~@new$X_Huo z_-hR~PXGRIzxDR!^~YPz9z25F$UR~{M22!&MqzmH-2p4obq|JL{rtmyj-rkFdaI?j z@jM5p5OtlX`xO10a?-~TyT?@3qQ)z{h)hC^j}X%46?JZLcjUE0Q9!j#bPF{gVvRSq z>Gu`1dYp<0B2zg*93U+MLF(VmG*Fd4R0G0U35F3qX^3$?Sway@zTn|JDHqj+@dZb2 zSfR`dAizFm^MG@7@cW2tNsq~5wCWKqIbh*{B9e)cf>{cNA`0u2QAAJ<;QHuzrspWUhL>TT?;#id~!%}!()EP<=} zkgG4#7nedK@lDi{<%xf8f1?UODc~z(yvWDcFXO(><8I#MK?J?def0ueBl9Di;r{t~ z%0WIiuT*lkhw3oe(!$x_!rSr3kGCwNT;p4hNBc$}<3)m6Eu>I-%$}B4cdx|TnEN6k zO#ENe>B_Ddbe^1b0VC3r^^Ks0)c%;kTiTdjkO1OVIQ}(;?7z@-wEorNKX_DBabdI&b$9``s3D!SNOd2e(Y-i93l&^h54CD5eOZTOcSv7 z?{rXm9ME8r{k&-p7LLxI5od_aeN=mj*!BuV4kdX8nw{op55`iD;dy&JtRrr=?!f`A zGAq|al`X6w(QU<@@XY(O9kf(M$jR^1kkGTxrNEeAHo?vWwgFS$jkcgg1oMZa9h)4+ zekzdAnt&I!U?>a);D;ahg`4kMZ@gCz<4OPARMmPd({2mVA{A`*)f zMvNyB?~UI0dwRon6Hcf?L<+;q47dqEAnuVN4eTSIn6+SB4-Of=52rqi0O|zAy(j@@ zGk|+Z0FHUTZ|$>&!7h+?KY+j%vFXjYFokqj@LT2(CiIm9c;&rLZ?ym_6j;m;M{y7Fp^i>Tjv{Fo##`uy=@!k=sF zr&o8et7vidP$1mpW{Yz^;df*Ub#*Ma60f=bvmf>MtLN>nIZ}V_Xe~{!6DYw!{RSJrk_v__?sJRl{>RZ2+WcRvzs4?g1ZcTAVwXta$}{Hpj-3B zvHY_p;Q+{|7`aq7aXE2uH?X*(v||0oX8%DJi_$2UXpp2(jpuKz1KiV~YoelB3a0z8f7Q{u84dmHi7re4Wyw9^aF#PI=Z=Q0T3~r$P?T=1$ z&R~o$kA6+Si9Mh-e#=j^pFd0w%r83SE{7l?VOT#(2*jt^?~}y`ai)TvTp$v!`T#&a z#jaWe3m{WK1Gb4hi(q`TB<@Qm~b^bBTXp>1Q?dGV3D06;|{mP$IL0jdu{9o(nV z#YlyUVx$TmK04Y=%zy%|K-e3C?Cv9&qoZ>FkX4v5`+H3M+$bI|wDI}-C`2UP&vWBPy*g9yy~srR3afcf#z9w+zn z47}jo*u(oPudl7g<6GlgTb%r(kNf&~<^5y5stg#nY_*U!^hoI80k3;?b~c+_y71Ap zkFK%Ad$N{uuf_oRL8pV(FfNZzYd^1WW6Vr3m8yYxc(fr~nD!`MC=5sd*BS^CmQ3z^ zwpNZ55W$L-r{y!n305Q}h*#_p`LzK1lntXyp+lS>c?qouyOTRctF~uB*wG|iQ~0CF zAu9lZh|&TR#rKW3e&hGve7hCOon*>(J=*yu-u{Xtg>)U0Lc*VO9VDz7S|5b}>IUE& z%>CYY6Q(di0PoX@^H7XQZp%wo=Q>&C_up^bb5Yv4mS_Sc1Bi`Hlt78#2>%o;JS}Cj ztDL!G$8!DR^iavbl*|+PA_ya04&?{{{sx-HTnL99)G?(@w$`4=Qr8pqSgd(a#tp$xj z^`FV%LiVrFAx+dNvKiK`;y$1OR9+*o?)ZIx0Bb;$zmWq6;6$KWG?{tK0+}-fC46I} z2I>KQgw3C-?XtFgesSuTpMSW?l^9I)vc6(nAA-vdPWF!B$?PWvr3P}i^E^^uDuO}4 zPu%;O6Y<17RKX+-L?O&htjx3|lP%?JB9-?t@dn*DLw`lK3U(5L28L?Pxkom^^6eu% z7-0a-5(rf_3eCrM>7KsZoURF0OZ1@@p*}ff{;ShAxcI> zxiJc@z9=31j>-szh{{eRIB^u&8&+1Je;`-<4*CS-AnvKA5dLng_Wp?aAp<+(fyxxH z8viq51=AUj6OPoMKfi5jyw8?%g>2X;`JHX!GF6EV+eV(=9~?mda5NEYQT+R_-0z7q zS2G2GD*E}JsGt7;!22E({Xz=VAdnSE0OW_8Pv@b7;468Ts5e1RBHO&(%xgkEreY}GMWi35vf564N^gpU&ul1{ly49x z_7rd2UK`^CdxXLt$=$r5HDk(!$OZjD$)4$h@Gp%`Vm|{-pmbvoB)>O4c!TS01U=q2 z@V+X0S$miWf2|yl*Mh!udc`j6;H&tIpguzgs2TzP5n})jeq{Q23NnM>d#MLYBUmf{ zcCvGje0B{jlcP17r#8_BGzGFH{uY58|4!`8T*aRNB*T|Z^MY6?1q9)*8p_IlV77E) z1?{r@ojXZcxVtRY9$F|T^;;s5^D*ea7)h-QoQCf&BKGN)2wJm#B*u{iEkMT*UctEo zlRv|m@Z_fvWPK3EC%A-|8ulv9Tt#d{T@Egb7X7Drg1x8+LlKJ5`BxX5ibWCMZ_WWlOr!lMe^3{{#X+aq46=1>v-|zEcx4sb~>a zLcuxcz{I5mQpLPaoY;8w1i>H#!DSBmIT^_e>Wd96@I9lKCytM3zq}adUM71A zM{$UZ+pn4jas-Ls-PeYFL}S%aOe7b!{Q9s{IajvyQnGA~%pan#s0A}-u^5f5V949>rVdNLd3Q9t7AFuq}UgOSW`KQY0L~Bg zrrI(r#RTJbRlX~1Il`~d!23xN_vZoWZay3F6W#NJ-~ga>lm|)-*fGnJxr;7=_!r#Q z!;t?96+h3?(-XICy(09-IsLbOQUV|aY!UR;D>Et>02%nSrEOdYfhA7KNC9vZBwy4W z8F*ad9l;EchhQ}TbPXKdsKbIB9p%61yikas`8IZ+Tv(dr;BoNK1xno`WW%NJoDZmY&+i&I^ zN7^Us;g0ai0Y$ppyQ=V>vG3$2@E)Nr=?i#+poL|r1b~2XBg|GqxbA?d1SkT;_uJI* z3RhTy;X9BT77$=Gft4D72zj2?vUEAoVNVzy6EJ6u=*vtzKs}e%v}^sai_$q6EBVkUq@fhF}@&yM5Dw#Qi4)$w zScg(~SP4*nhX`xf(n2W0IxN_;wh-*+#R$-$Y=i7V7_*op3IEh~elie6ComWdRl?Fv z-rPKZJ1`BC7t>PIP^61iR8I8dsf&O_Zoh5x&mUcUcpbARH{XYu=R$(Rz6Q53ybGsZ zJbeDs&)?jizk&teM(uk0`H9}np@Y)kgVYNYXXtQHT_yRUB%m|z{Ih3I?yVE~w`3Ij z<+Eo`pS}E=51!q^Bee5`!OXzcw5LaeN(+RTRryWnt$YB>_Z@< z_g4i#-*2eNbozXa(hSo`%^S!NbUYF;-o9Ol0A?a(3JXeASMb9>Ld!w;h`6WwQ4v5j zKpxPQfrzKeo=o8M`1MpI1i+RxX<9Zg94k3Vlq)Yebb`eD(gfYhP^aZ^x-9Nc}D_>LM~kAA1q8jS)J%G$Pvdes>P zmge`Yg;SCK0NkIkUqiM8=)_8@EUT%Lt+2{K0buut*p>_hLJMb)aeZ_Q0=?)yz=HUJ z9ncB4IBZ`P^@o99Q2<~E&f8@!)@(JOdq4x*Y;ZtCvpNcuQgM%Tcu5gz>Ls>Ux5?S9 z6TNEd55vEiN`jcsahDG(Y2;DmfzCvUtW93S01#0Zw*H%}3TWzX2KU^r!ayj#nc|ZG zJX0O^P|mSn6po=v9wq0o_f$hWl>3J@e0n8uD#K4ygAp`D*_3+wFaPq!ouyo^AHL2= zlLOc;UrFW~*!O~ntrgq9E#sHWaKS6W)!rR!C!j4LpD>{Q>B2DUw{_AnUg0l5ZGfdBQ9qTMasfKbViT&j4ol03ury?JZkzD+0C+v6Dnr{OP)J<_sFSaosM880K#QG;h zssOYQRDk%0z_0^i5F!Dnl=qFd#l7wb>PxIXM4k39q65=x0m1>`_bkCL;9%`U`-#r_ z6P<^(|3@?tiW2t_&aDza^XS7m@4kNdk`myhE)eW*zxtLWz(pR|z&#YtK#og61KI^7 zuHfNQd{e`-h9ja7VNdWfDlllE;SU(iJ_&zt)bEzKn0E5^9Y4+rpho&g0n8k}bV<2D zd%rw0&Ajx!1u}||#PjmS00)u*u7>VARl>XA`IlKRN z0|Io-Uzv|OTkZxMClXG7ZE9qrec~o3_FdD&Zy5|vd4MJ$rGuTj=Y{_P)kEeHK%khv zdsEDS{~i#k_%|&-IY1Y>FC+kEfGvW3T=?HFWCF{$&5J@!8=W7T^<)DJ<$=WlF~PZT z@?5v}^UQiare$>dPC*1to*3{i>fpmT4J`5&oPF1{IdJ4;Q_8{N)}Y#z3I^5p+fbc5@5SGRw~~BJg-3X1;A~hs(<#G-N zgg=-H%re}NrO%DivfRCK=V~r!7#4$S6j=OYyIR~|xO*c#4m<*OJ$iJzb>JaMsctWe zO7Ibg{!P|^CMPi&?}H0z2!?aa_B0Iz@`FlMVM~gA2_Q$Rho}qgv`oY>Mkrm)Srf3b zg8>A?SLQGslZEl*vg~BV(!*5}&^IIia}91Zo|wVmi<5Ty!F`4UAUB@F3;@J}f4MG-s;F|x z0K~bRf}ed!;Pd+Q*|*>F_9=+~zaY7VqsL=p|5d09YLlI}i~)&QE&7rU1j&h9OgAos zfN=fdkO&A2;Sb;IL|+S8QuK!qC7o#ph?s$*sCl181y%Af83fON(fyScB>np?FNXRj zAp(Qx%NvluSH@5S!Z+GiT~LM(aPSb%D@9Laf4C0_K+lCQpazGAY#?*c-DU_P+E0}v zq+6r|fuTkhk!oOKBQ?^sEgBuQd0Q9)G~OHR8L5qK`BObxTaJl(@C zc#eB6c0T~zym4y&_!U{hw^*X%LIdM1@t#z7;}HlSeky)cyrYCXNgy77%*L_< zhayUVRJp}4%`*iqU%->OxzGq>$S+0E9g+lMzd$bfJER88&<&FT%z^MD{?z1!&5NA< zfC>j|~;ism4VHJ?pze(FtELfi8VxtiO-qkU9leqSHU8FcjjjPSA@coeP0qF%6 zIeRQOE1|L^OSRg8I!V*yk6Jua156rd#{ou6=|Zv2(a&F+|08nqYPoKXT>_~ZmcT}` zB|Gj(9+gCEocoRd|JwZ_as*aH9wR7#_5uvenuV)tOidn{G9nUVcT0K%#<6uy1`2iv z0v)V2%dNE3fj^=C8Q=JW-~8=A`0cm8|9R2>7|JRc02+i4ko%Y`>e?+%M0H<~^+2={NeB1D;O`xT z0k+Kn`{p^z+jsKO2lKz_pTEmdE%uHQgy9RB0)XOQIpPZO&w^*1*l*dQwg&?g)DPT8!`~(z0L$KE zAxA&k`Ocn(1&MV-Hc@UBd)e7!GHpWK=evW57~sTc}#iWa^)ER@#KChtuMdP z4gFU^!2=BljIg0cUw|l8gCpISL|8gI#u?(n6ZqJy-2-38H%5wpK8Vv{c8x)`IQys! z6mS4zXG3O#X%h&0vASuaVz9xsD= zug?RB^0xs<-3ey)3|qhi@+<{J4Tm!rXg~$1ybELH1H0-#`sL}NZAYcZjxa-lESuPI z2h3w}EXu28YJt<}82r(jJKy5^gWvvxop0RUoU$8tBH$(q|C@=_k0Lh8GwOR*Uw@?MfA);x-yHd3vV8mP({FEnrEH+0 zfCv3}m@h`PSN`2p`HEU1QxPTsFf*I`d%A7i%VV@uEbu_XgJeK=n_i2)FT@38LvMX{%G5eB0%=zW|A*vBA=4>m#7I;{eS=W?~rM#$G;Mea)E5W zr8u_#lg>t#Z^gMkbp8wFtE#@I=s-ial^mx80^(%;VIrSD%t9oH%Io>w4e|F! zh))tgoFaN~q;~=>5N9HlP9yZ-=;EZ{)NMR}qVfDh*F3r_9NnVpZKX-umwn>pju9+67QTrz!<9#9a}3oot=A@Ji@&@N4`}$FC7UTih1sav^OU2*Kf@ zXKxH! z&!*GE_7YLO=0ObnB8@q4-R9J?`05WI%rWj-TjBUO6al+k0)erD2Jamlri>58>8+A{ zQ02_8R;UZa%?I#9D1edw-d3^wW#(fDyL<_7z#AXDwevTG2(Wg%*PsJ?6_o7buW+3% zYU@58O%ea9{aKf;uRq#w)<_s|Rze1x%^ObU7A5}kPhTH}jw{MVJOhSPQJBX0Q5!sTpa~P$@iYzzvN7=T>_042z_+{9Q|aBeN}$~)%t=5Bu;vKW{92EM(y65 zkj^QR{xgR;>_r2FS*U@5B*QLg=*J{fbI`*g1e@qT#9fW7*!br+U|3M9k8E4KZewGn zA(85uceWt8H=MG1e8B6$+QHs)mxdbgS>{mZ*cOJz1vAgzlhD_)ZPPOX+lLJwuE7!B z-4x-6IRI~?I0{1zvRFs}Eq*_>!>cUAiE6! zKQZ~6=XPP#gPaa(*EiOXtLJG@nuN5`-D z2JXZ0pZNbHX>-(>VOw?Ich0Ef5c+%pp|4A~TGm|1>LWt8xlUNF}glFzCo86R7|?DvWw-v?d0Y0F=Nlgf;?e z@m*S~bnGGC@fy%(Z-!bT7NJ<;;J0U&LM~yuoY92*H?Ro5`Q|=f%B^om0fhgTU%n&*{J8xfQ#dUV7?V(9 zUm?Xi`2Z-nFpT3q?cNh$avFVNyoPyV;varI`9(`J6ZmUjv3Fc>od$J0$Dir=2ci=d zQ0Ka8eEZ34l{5XHPna^8XLW$<-a5NJYy!hLVDSLX1x1A}lbM0% zMI5@@0tnDzaDX0a2pW8)0>T>LPiQ|afDQHz8$3DcRFJ?n2UmZv|0J_MrJ6tMEn!<8V{zdfv6P_Ky&YquW#5#`UoZ@CQ>W)?}f26dYx;taAFX@WGhEy zsD^k{$1Wr_atnc^JG)w3*)s>ZrMQ|61@tXkKDO4IfD>v`iBM@p*dCi~5g2_%NB||e zR;>QmwT-y#l@5IpFaT3ti%sr4ZtN{F*dmm*^sf=W%J^Ms|0~5^#Qp~#kPaaKzwvu4 z0`BP`OjnG9E+kwt3ovbfQjJ2Nhl(jD1VN%fWGOcNSLv{Q*{it*Fg)~)P&cANI*PMN z)53+E%Nx^l5yCx;8>*c?OK7!3{N(d)n@fV`+1ZL@%jIIq4@H^?&wc;>2e&U4-RUF< z9us~W+Rx<8I~@!G(`k4|>@M#uhkEo^UK?_c;K2>>2B0|-RGbo6)0KkzDc zVi##yUObFMaQ>r4^Rs3q=1EAWH8 zN|tm%dx~yGhM2Q7B%*~+-=4cp1@QA-_-EgkwG?&n_YNJ}U*+Q?#H&r%^Hj*tUrtQC zRILmL;MOzE{GO3BIP|NLN_~3&8x{(_`i76c1rvPv>?Mb~EI=jmCw8zTL1gm7v8sVQ zrM0o;7oH}MVH$@g&%P9PaQ@|&U;RKT`QeA3e;@^P4Q^nxHFUZ2GIpQ}Z*awZe+H~T z^*|G#K_KNk`M_)@5ZdZ3ROSmGkfsQ?NeQ}T_K|ciH)tAg{DtH^gO3kG{Cg*~n5#rL zA@5VeBo>5Wl)_1ZKSBi-7@#k?d`Kca>j|5$#R7K69$w2$`gSk?jHN7n(GmVnx)KSm zW5nqUZXn9ogiYY_{he$A%a(bZ;$NIS!{hgN@hy$WIwc5HBMFn4j6(wW;fK2>0SF2Z zhqyXCRON6NB$3mf1R(am5WaB$fah(FeyLe4guHBiLaD*!GSuJy(OI@!nH|QrT7tmb zh=g3*ak%8Ynih;QeeYS(wb}ua4NaRu(zfh&dMqr&^*UH`yU|wgp$NnzKrB_1;cSP ze?aNO?74~1i-LB>M)A_-9mN2@qX^puIFRwyMO;IOW<)r|!$poy8};D(Pyo>hWQ4$U zQ5(JnS3n7f=oip82UVsT`{EPOS}=ISO@xz}eOc)fCB2H{TtgGbF4^%2RDhGJ02U~o zT)suheEv`E5n2Y~FT&8SBf@mgfxSm1B6mQMkEJgPuZPEwtDqULQNb)p-V+TUKYr}1 zatc$ygs*{*=WEf1w)Xu?Gw#OK&YTUARx;pPTKt2#o!krJV_qBmt6aZG| zKE3_$J1{_goqiw%Pyr|be6?|?>X9w>&2i1R_*FkIpo`e=V#>#1Rqgw=%3{KwGJpZq zt8cy`V!20|R|JYRIg7|)qXK(ZeK*9_zbukhB!vzyN-9lpKw<8FBO zFTedp;m@VcVsN7u(VQW+lrQb3hev3LJ^(fk-u%?|SPj7Op&1~=O-*6e5)9Lj_gXRL z(;xtp2E3{YpjyzZa34wlviU+ol=g;WcfEImzvw!v33_fKe~2H1RAA!&d@V=*4@CSA3Vc;)e9UBkFI3aU#T&wuC&Dj1XGY)`mpDNrl4i z-MkV2P?i{gJgT?5DJT`&;dG!O=z;>zX&s9o7=~A=!g26Rgo=r83INvT2bZ$nbDqpG1f7@?RP*qo zDYWn1`idv};isSVeC);#_SS#cc?doL^F3I)WGC2zg7(4x$O8P)sL#w5#PgMe8IfQ0 z7UUuNLxKQzB>D|8&y+Zs+&C|XljBl2B?oBDr<0D8w=50uu5@xVNbZ7VuV7!k0(8fhnSZ`jCEr?R%`Lky>;@ z=o@UVVC@l&v+3D`xg;_Z%?UDvMj#4-d}@}#czGHUFtxm~*MvWq z=#dU05;=rjnXU7N8cN)n!4aKR78~AM_*HU5qlkkI5R#mX^NUk;MPerJe!T?*cb4Ga zyO+ftRsTkLS4+4$$;lggi1z7@>1mE&B2a8zac6j?U5^+#pxG7#{{Iay03H3zS+0e<`(%i%gv7Xc9q_@zMV8Yca2-sakT(F~O+9rxF0Njn%S zg*yhI8k(^QdB+WKQek3B0;r?kkYyoji7+Q{Q3XMHG&dC)9-qJ2gQh*u)&Q>mhA8+CFPj211_3Y_Qz8dV11VD6s{p4Gv6^7qP&!-DYjUZ2!RnZ~Xf5@M1dCXLY$!8?_rXfu`QxcAP@UqZ?0kax(!SmhZL@+WxM4 z@>i49+E9IMSE{9^w_R@Qhb9JjYT5vihqXlrF+c<$r_e)<0(q&axuV@aae&PX(3Akz zXt?2`8S&rTM;{?=aqYu%N175X5hcOojYTJrfvz2 zlR{h$8LQS^H;sneurmKy~0>IQVNKrVRpDwlqDk z2^yWZw>Yi+O|=M#1ydvdzpX;u%~Pu1FV7}t*UClB%%^Cu+h|#Q0Oi#&phUnvHoN>t zR>_X@HfOqoxCF4}Ia2X1@)NDXl@O zRtr)_%pB(Mst-+~IHem@V89Rx5Bee0^IQ@{QW5K!}E zxBxn6G?1E~7&^3{<{t(g3&DqIcBzU7!Kt3TnUI&7rg`!08pTcf?MB`0=NvY z2-`qPcrb({2OtuU$i%Ah0TsmrCcHWG34IZMY4Y^}lq1ML5CQctaR$W%gaAn0ph>?3 z$rb(#`BaBU^85Vy%ZU?2`j9Gce7|LYg5uYSbyYy%bD(ooO#n)i`G)SU5l-VPPA&7r z5#(XGzb)YOw_Oj%;?ch4(YC&{rQhdB@jFn{v_8=|M53XST7O2^JGTg5)$)*l*up*m z#}L84^NkHw2tkR|x=|=^?_%$t1n`PSvO&aFf7I~)!~z0D;7y#xoyS`+|IF$iFO&dL zlK^Uh^a6nS3m<*-(Y3SJ&K*5cWB9{uajU-xjH$utyfWVd#)pwNsx|g3@cfPZ)uwus zz>YLoTSO29f?JIdC7v*{mOe3Im_jhFsIBkqZIW57+%TH|VU;oi4!a0l zPBk}Abxd;_xm-Oh;1IKwH4o04-l!v80pd%_>Z=H8ofF)+`Lj=y=8&88&{m5zsOk2{ z>SV#z#q8JBTJ7@TuM_uj?vz(o-3lju*(~Iv#1CUX^ryz#_B9TQ2>==hP7Pv^@uIwL3i!iLTGj6V>K8e6YXN2$YvqP zA*=VQhooN!F_2RhSVUSVhQ36nU&*=vR?uODy3;VW8h$W6>_@x>;2o2eY^8bbz-BXe zK-O!+E0AhR=`?4Lu&B?q7+bER$p4f%XLFcXA5Nwb^O+;lE7oamrv36E6zQ1Y*Zr#hd9OVLEh_RT zeiWz)2#s%_k={S4>abxt!(QO@Cj-284a5(meF6;RYt;y%{)-DltuV-`hO(B;;=j+y ztYV!sx-9^3Hk@hkM>C^6Ex>`UtTlyz*LuCy2;~+AXZYn!C)d2VfI8Y41+5S938MyD zU)#`Co9cc2$PNBKKmiE#arOtJQB-01X9saq0fD@a1_1{CYcnJOS`KOe2Rm(G{#x^6 zxozRvug;#lcJ0Ft&vgf35Q^gqWi1hnw!k%>PtcQBDh#%vx(Z?E}RP)HNrU0 z#MUHe3-DFU&n`GyY?(Hs>p-SZ0?X#1!1o6++X9ZUERr@b2SsE-<`-VCCnk|w`|$N;A^zZLoaSQd%JG^7+5>L` z*vl9!#?n!NsldhCKzK~>NDqMe@Ply$L+%?Q0CFER{UG1OzT^S~(9u0Ga+ubeIvIq# zxf9lL9TPMl$!7Thtq*i|Lq0me z*sp#aX=rh)d~T>w7_|=&{2x35wCl*nvR;}ltc5?l4fQX~clFX>IRQw0vG5E2?xIVC zSrNK3t2LxtZY=4NIKZ0dQxPm9pJUu3KAy4EboUK|_j#VVl=8w_^th;Q24V zJbr~Bkiwro97t3FgX@Q6|AQy~p`kPFGypF*sQMNAyh8mU&hM(-7fP?_R~`^%rz${+ zK%y250>3^X>nwbFwYwveFxCIE=O@}4HBS_K_$gn{CB)zK0AGJi6+mKuOkgktB>>S1 z*3TP#UW#{dc;1!9SGBI!3rWCm{k7*K0RjX58CO6ByB`0J@BpO@(-UYSAuix3C~g0EG+=A--`Cd&{zo_x0Fd<`GJxp;etAARZh8E;MJ+#0 zeL-UXSLA|g^#2%lY0F)jA#p1jG;|Ze1pR3+x3pSVMr9_6ZCprwa4$@SRH7vgt`wPG zhI2bL{qVumi-8IYWPxK){uhF_OpK8j$M@>r9&0$;5R{U2sYsb#1ok4T(6^AKUm9$2 zK9Le({%*@Cq#m2QvZiAy1fIaWj;UHjje{G#kWqIS@1Nonb3ti^LFAF#XX{_Iwo(Sb z?>hiK;E7EtkJRyNE3I#)+L{4SD-is@|9fx%h55<$r~0PVTvdS(Z6p_qvJHTdkA|2W zLpsDTG*F;q!N&ptN~R+O{W-j+8qcA5hBSyIK~|eL`5v;~$8Z?r0hK?Bp(q%~SaNj@ z-tAg86awWJO#u<0JAVtKKrI-Xgn0&ky9D9I+?K>(YHSu7@9EQ|&DpVBKxIGbtBuIQ zH+Iv@!?Cd0J#}-pZ$sA+Goq9nqvdijen9dQ;y{+UD8Y|JduVlf^P-lE*+xViP<6 zM~Bt_(U`zVa@q~>j*7j!0ysj6rDCT-r2_#1&!An<1GInanY#Vyqq*tte);96$FD#D zsr(sasR5V=YZ@8a4@Q3cGQXMaz0#s!AmhVq(T-3 zMVh~K+`#_CzK(7Y0^Z%ONgvI`mtQ^u>l2Vbr2q7a3_$0=j^UFZpWJ$)65uO6ol?xV zw^&1vGQoRbfZ%xOjhlX+FgLpZW*DfV9}0kEeh7X(lA@^DIWjh*Bmp6X3=I$;;RZ@z z8bna$py zf)R|qr)OK+-Jape==k&JlWhUZcWus$qe)gkO^xuF(DXl{bAAKO$Y?4>0-*AuruV0M zPt0ftKvSf(A`uLB^H0Ph=kJqV2>5XW`p=I@+0U#3aJA40NCW7X=i}i2?EKRQfCsd( z?#J2B&Q9R$uVmH_{@;BLWE-?kszKxuzf=PItpOXyej!HB9<_LBSNti_b&#_SrDFxq zBt}@v4<6!M=T28Dp@oHpV;}v;1$aPxi+JR~KRI?%(*GyV9$T_wXNj@a7@$C*SmY3w zFqjK~!7+yHM3hp)Y4M^$UnYP&c6$UxK%8%_>_IZ^mkU6jSSIix+Z4WC&0EUrX_|gwP*!k(( zJD}k6Y1R#Z3kWPb${R@&wOD7LI@frn^GqXdD`&))M&ukcQnsL=h=d!N(iPBNMAO3G z0)K#7q8Fe+2#;cs>FVDeTWFAD+O=b=q?bn$<}cWjkcXTjHQ52F3Ci#+1msdf^fPt9 z5TX&tMZYAyK}r8Sdc;vrK}JTlas{$6yV;N!b2zlJ=}!>;&Cm&;JZR8hpQW$j2e9JO z{L$=Zv_u0kH)qDW>Z1u501lx=_-NNkF^sAZmDda*GquFgM5}wW&h0i#)Wg7+~Qrv%40P9_;lt0QCkBSTyrc>KI+x%C? zpZEC_QA=ABl(FE5vv#7smM56#?8Gy?6Y3CO+bg~Ik?K?*DqxSFF#wUTK0x9^e}>MO z(HkZKXjkB8mF`!#A%1WV*nzZt#3trXOI$%O2;jK@fVT0zwiYWrz*z}fQ^Kkiz%EFg zeJvn>z(Wf{p#fK08pa;l&{=0Q)48C zM8g6Z@iX?9HHIstZQ8d8*fJSQ7RjEqJJ9xIm>8^ui_wd@8)k14HG}hs-p20i)POjU zWC)wh83daJqYd;QJJt`T=S=y@+YpM9c4;h}rfa!sU&?aylerQIG=RvgkT5SQbw)gk zlf&LoMA$G45g`CCk~O1SR)|2Kyip9F)|%yQc$iba{&yt+S^$c6A|FO734o)g;bUNKNIKF`mcT=YIW$FFgCaJVsQJXw_XAsoS3OPg$n!w z2|!Co*S^!LcM#%zRew9J_a0Q3jPW*Em88?uoRE;%`KHydx z@_mb_LdOcR$+S1lz|Pgy7EjTl&bKUHFMvXM%3w~nUqo0R_x9P;#|J{$%G3iuH@N+- zoX?YO>fRXiM?-FcL#T9?VVaa^!V#qX7bh1p-pNHOd+rX|vXExO4EZ^r={?8Qm29I8L&NAjx2hnivxgTr~gL3$D5lMiCv-k(#~eSUK}8Bjk*tEO)%5sE24LB zfyrq;sOU!1Zd%Be3P=^9cCeIOS~|ACJf)%L|-5QXF{rj({Ed*%i{LE3B z!V!(2xtl%#o&a&HkmazaY}e7R>oslXHFt+UH=k|X`&zU%h6x9&ZsBAB7(54|7JmIT zgMY%C=r+mZbq@HMQlHxYTY{fI7oy|MSlF-U-9f zl@h=~V-F&84oti_*ZFUuBDZ+6-WDzbbK4+Pzf@Q61oA*w1I;kGI0RvUC&ku-F~D)g z0DL`H{W^os|1Rc7&MW-2V02KtB)-cGD9KFcJ=OuLNusTVI+_o}9om9OrsH1&0nPq~ z`{)6N```hLK8Gt3>Sqc^smPH5$j{m!-M&-X{fs0vIGB9+Fsxsb42qLmsvZ`vFJ>%O zoZ^f>n4LC~u^w}Qc#nU4aXFLbXkUEzP<~R7;;_q{j4V$=fQmcP&{U-T&2O&2@VfG9 zmO6h?n|=0G&zgO8wo>84W%w~LQo!vykkZ20lA+1C&5a5+6}suG^U&rgYr)0};nN(? zs)EbKJ#q_`x)0*ot;Uxh{omJOUNZRvlHW^-UxfkQ7xPW7AE5nHW|we)YmXWPt;4)c z^kY8qVsqC4ihu(HP;E4}b64;om}3_|Id-<8Mg{qqdX8m5f%H&BTo%bxT)U5r9Xm@= zbre4Ig{#ahQ9ScP_huN@_Tx_4OlEPEuQq;9+(Rc zfBo-VWB~QwKZ_6y<&+Q?q&?b=2WR6@{YVP^c@8UcFI_$%Db$X7{ikZ+a|yl|5TJg* z0E%DTyiW$Wb?2q87zlMNSk1GhhJi_qY_?)c-9l3h$AL!`lz-2lmsd5$>jR z8hn3-5`#Ap0AX=}9-t~GEdCFXuR#E{|9E`dtN@Oy1W*D11!(hySW1nPjv)R*yJ1q` zaH0vI_2D>0Lm&2k%Y{taF9 zFKRhQ^g=riO4~*u$wy=>fG##FfcSc^pkiTTv!B7Uczx2t4oltk^XuPTU!D>@uQ@-K zO0bCP^de({;rD@y2Snc!*Pm8@Qz*A5{9=J($6~x8nq8=_n;ERC{dF0>( z(2dvf8M=Qg8gV_r6&3n)u!bs%G3vEn={-(|C78q!58Hi!umpxxK7P&lY_PWR+`nD@ zj32ZvQYZoOt1ow=#J^0^d4RmPH{f7d@<`){Cyx;PdO3P_%pQ68-~kvb7zHXpX$Sv0 zD%KzbDpO#py4EpWPErfxlISVT!9S?W+u6hRxhM$a(teB?WAi2l`LYL}@9H`T_|qc) zx}r*AIz&PgflAWF!a_P2>mZ)eyhocv%b)-HJK}$sjKM&f@VkE>DDE!&^P7--9=qs> zCO`mB# zbra8?5&23aDga0wdKSL-450zeu=S}Lq=76~i3Rn-`hp1(p_flYX;>o+uD0-SsF zqGR6Zg#!x8mq>ri-3C3N3ox6)-1QEt%9;bdV=M*U6H(Yu7WVJ|Vm}-paR51s8yhEB zd*o)mo2!%}cVGt-EkUkn4 zG4{X=kb4Xocz%$>9qRzWd-GkIGQb3)z5r%~5Wpy)ldX%vn;HVJI3yH6Yur>EPD2KY zI>zbzkpvU}w+-6AoB%Ka8TE2vd)rtMX2DN5AuFO5vH{V*b+<+4wWH&GF;AuM`Samn zUtxNAx*W!-x5PvrNC`ddMzQPB&|VH*(%#0_;`(BGbaXL12PD@VN%}Fm?y+$A7s1R>j?I?V)CBd7$3ZpXIH=05K$1o{>Nc^mlMlniVb6z!%mnbA9kHI} z!gX*IFGJ5##^P_{>3eDauwBX}V;utk61W3k&K{uv0BvC}6kxEoSC;E1mrCMC6^oS% zS|qy2E(8iA5E%I<4T-=N2qErG^s`2Fogz?lu(rM||A7xTaUC#Y6e_8_2mL~s25_A| zO)U^Ek+H)o{5xX}^!%6SssNr3KUWc;5L6l<9G?z8z1xxkro`Z$&0&@Q^C|+KGW;iK z$q`i);C1LKkX01uQch5oplC-glriADM&5t7qF;F6@lP0VKf6iP8?Zo6RDBA))fDi_ z6QW-U;PvruZt!fP2x^sgpWKC2+Cv32F>cJp)eD$ufR26QouPm<0jiq<`hW+k@R#;4 zpOpxKln$z{M;HpqdKi+jmWKATrYMgA?mKjF=G2k)A&vx+fcgPqc)hz86fb2Kw^7tW zm~YE$_S18z0_g4Cz_pKcf^}p9;7w0-0U|J5ui4L0HNv{+?e2^ zT@huBvsAxW71kA^2^%;_uq1uT9Ns_($Nk(dbbF2m^c$1+XvEMm3+|6z3dI zx0&@Ir431qL`fNzHgtm!3-nRnvn`qrSuK6TnHbZzvaG9Dh?cp!GDWe+9#Ua>Va(?? z=g2^DZE2wi;(C#b7-J=XBbze|?mxxB$_mh)XDR0hC7A_?nZLQ0<&#HqFxD3Pw%WWI z%Fxpvqn;hMbHXKhY`)4W&j3^4K^;dQi+ZR4E_ytu0n|N|@7LUpH{RO0w*#>sP(fA( zSa>f33Q{cOr7w;{2m%0)MD^9_FKZLQj)iiOjIx*eushTyFj@nAT9P{twV<=9!~c60 z_3#Unm(Wg)7FlEFu!P8W!-q_Qe4Eed{BMK{T;JLG$+-`y2|hW~4d0gHo^PHnF^adu z@m$JQ%9`g-k>MsMu`Pm2RVdh5a^jpvXo;ggHuVtOwZ%~!n#S$T)Zg}=!TO2r2{yeG z{phDa>7Uz_7Z9H8!f*KXL99t0t|$>+SXwOudhTit$6{I++ClIm8i<+-#ahOq8N!0S zJcZ}ygQ-QLpK8xy%h*eDAb)gQxCq*cE9_)pw&?0-NU0qtB+Hd_09!z$zZtxisFIX% zm%n@d3n@TJKv1Br>KQJP@OjRqn$-MFD1U>4lmuoiUG!gr_Y=N);rLeUR-<3{R}%o^ zJS0Js0Dh#krIyBjpZZz>#~Kas|5PbN53PQQU_^uXku<)3X+{8wdES`%UmuwSa1gN{ zj&V+SE}i7O$fi)|JSDv%-Kc-@j=Q+U_1YCo#z1_AkM)K8;jD+~NLO<3_B zCz}1FiQW;vlz@nNE+&5_@sFwxNGrU--ba-SDEyfRXh+OTxAi0eex(;IIK;4b)9vANG1#=U*%gP`ay1?^wlwpHV?2%&+pU9gp^` z&cZQ<3jj_4{RM42(fLIT#MTb)e-yIA^18dh!mzq-DToa(7Q*u67+>a@$p~$U{k}< zGnYSU7Y4u(puO{xPfndXcjn9|^+!$y7sh5i`GOtAo$+`}Te?(kg%Ld099bNOwi}~O zVg~4A#n&1DzRTHJab(AF`8un+ZSk$)$N=B2WQFm^yx-AANA!Zc6wYhV8jG)PM`#+ z?;}&M3&3t31OT?6x^`ZIZ^|5T!*NFk}YFhV5<_a8)#v~x&? zK%QwA))0+03;`02=Jc-BDT&H)ZfFJ#V%@WXn>+D+(v=l>0LI z@y~1W5@o4J=~aV1Ef&fcs7E~)G~@Ysp3ivx2nO&&9HRw@a+VYFTx|c63TXSp17;VP z*R;rhQ8XgL-UjO zlqT{r<;vy)Aup`~F2AcwxCrE+@OlVYn=Rv^1}K&x3ob1np&jr=0GK&d7Kayy-99dk zxw<`4-C}*W7_l=AD6N(AF(wTN`O|S2_IxbgG4+s)AkAJ``AH1aXn9C1KL)zE3gKRM zL3Y7c#b%B3cMaeV_Mo|rOVWVy>5>p2fFPH+119HiNr(@kzy~`O{E`H`E&CxE4;x=j z_6ezbR?0VSoUV)okJcP*|AZMJ>%as)w@&@bujGMvs(T@q^J(o_@`;MhJ)GT}b!-## zrN`TRo+2egYgz2y#6oUq)}Ei5Vw;a(&A_FWT^i^7RKb=QiQ0?J^!zVgJY0@w6G_HD z-n0Ou9xH~ePG>4OwiY+;2|LMJ8-)kxXnE&!P>KYy%~-z0(8 zWdL3NQ)z?ztaWqnC*wVd;)hZGF_pVH^i8Z+0YP515Xhywb%#;&d5LT&Ab|X|gsIC> z49pEForz~86f;BkiRkB2#lRN8JqClsJ!D{Bh;An%$FHJORwX^^UGX=D<5s2fS#9_A4&=aZ%eay3MPq5G5 z)YjBUkMky6a6B1h_sOy`3jx$ve6^UpME*^heJ)`wypjua0WRE~ClMIlpW*zF5PEb= zD!%{#g8j_#-zOg+i;ei#Z6yH9sG$$~{jvsi22c*+xG%-L(U#|vqhbR)Y-n@EJlC&B z${m$7XBYeF2u_bBCD0McMXd86Z3CZy3dm6u`)1_9!05wkw28R z9F!I63FeDgHyLI*Oz$v9FAhdTBLO~#I$b7DaXtWRxLF$C z(DaOhznHs%*$JPDhvS`_gnum0qOW;03#pWz-Y8DsiO>%OwG8fu`kg9Cu|;VFiJocfBy$6m2%@##>D}6kZj0$Apbk5?iI5q7x~@ruN9S z%!j&*mLdzovB|#i$0iFrHjDtK8w>*H@4uqFR07b5P{%xZK?A^Q<4>5IH;Q(Je>Dl{ zrJ59;q6~J6YX07jRq1?<)r6|?yZ_WYkZ)COI?S^{0MrH$QLlhU7l`Z2ufE|$rGa<> zPrlgwuE>5m!ZpgIy#8Ee+5ItTRe@I)C3U9d0pt^jIRDXgA`l$|1ym z&lSOcH2fra>3)muV_bo_3E@uyARu{e1j2%c{s+=bM}RKvG?h2t6L$=Yi5a+?A1FI@Uu(D`oY93mdVLU`P>b#jL%ubR2ulMSY)H0<7^gv(vZbV zsMa!P+g=O+ixPYq& zG5p`=Ky_*W)B@~$>kt0m5755l?1wG;AjYedfiCCKMy zIT22T$4y=!F=)k68a{=|%yZMKA>p*hs2;pAopeYdEa9ix@+bJiJ~lnZ)W9a115_~P z6c77cB5c;AnBP(pGQ`DZsbkI1BWF~VbR@};p$^dkOufg zOle)XR*MKI|9>+t7~wgBny zfBgCh?Y>bHlQ`fl2!I9wFqps-8VTHlt200G`YV93bZP(g z!{@tu8~TG?+VG^R54hqeWkwy<1kmH4-D=tPn1KrMDHlG00R+9|e7T9Q7Ylt<_(;A? zpsYg4zg2Yz5f{Kne6`36#E>!chF3mm?0&X*W#{kt^YNB>o`QRE8&TvN4RDKjwQLdh z<9y5~cmq_$*#hWZlLD$_~f@e5})QmSJQ64`_N6 zq}l6Y1Z`tKuki3iT|S#b0C*COu!W|8ui&8^rklU>87>)P{qWxc{4039Eb_;i3D~`I zzActw6;D1)uU=d((vR8#vb(Z-B6YhM*%u3x#^B>_+JI?h(%1kDkS^3)WHQXTkhUUJ z;rD&~@n8QDOuy!7`1FWi!zrNhM>$BbZ!!a&0yQ=Tuw?JhzJ5#6zu|nerfvXBs{!+{(g3VM`n+T zGYbDUYHGWp{~~!u_+cMj+;H1 z$JDL&X;-WA;e3pFir6zvby;lbKO4xw7!Mv~}hW+m13x1)Zbto_@dhIh7vMdXf0P=~_vZt(1F(h(YAOL5ePepG{qp7g`>0U%A3uKl zJwSgjK@tOf087Wh2_#s){n96hgTT@|erSCH6bEATIv{JX zd~7Z)9f&x@6o81A6rgU!xtKt3>`Nk8d4LQ6B*9RrrVC5Igj1;WDj?dV7nQ3qw6x)Z zMCbz%uCk#|=Jnx?BdGpk5OnZ6FH8SkpFMW{*ViEpz*4Eu7p!L0vD#Ri_aUL>!n>}6 z&i4ZCe@%Z9R&VA z;XRxU=K3#$mMUwNtgS6wfEx4=y}|fM;-=7R##O!dCIA*z(Ych~7Zj%KhF#e1IK9t{kpn5F$fd%}?^+ZyFKWJ}=3}6xfuU|bwI{@b`bMUJJ zPy)DzG9Vd%;3okv1^BareMXzTd; z+A@+C0wEMDAfNY?_(fLibMj6b)ZbLXNca{|ZtWuBEQ2!roIJN+%dIYx+^!D;)758#`weTa> z$<~zg5e7!&M?77g?1N$91y%!$LuiyuJVIw23I_?@KOglL9&TQIFl|q_z=YuFNC_OS zISS%`!CtQ0#2fhGLllIWXn1^xhz0$ADg;jAo~SLBuapO*mWHPg9H4mA8tq98TC=6{ zXRGW8QFu}7Nh5;r=LIFjB!xdRYfNgx%NP*jQE&CD=;+sCQ<0&G(leM&a%qzA#CsU^NZq@OjCEG8d@w0fYiR0i>t& zXOeGve~x&1d^78pgUA08gCqo0EfT8JK(i&t3m*y?1PGLGF`U9n$M+vRe(<ag^Sv?~a+P=*HYD)bMry}%kkJ&vCMd0OP^tqBtIsS)kY)4>Z3gq;`MHcm)0 zv@?g)l0hB$tq9hu=kk%V)m{%TT;i5KRgfn>G_>^ zgU?H=@8R_Fq}P?c{`m`8Xl}N)YMDRoN6gQHnwdMM3?RaMCT?w7=+Dm1y6FNa!buHM z<6ptXi~ZmvlXL(-7{N^aY&P5OvV)h{tWz7R@>!OU7@Uw8H40@SkEDAm<$p{||P) zwUZR^)^Gpzo6^43_)jFDJ%qp9!88^ywt{*Z=zP(Z{pp(h^0Ct134>colrzzDn@6^B!a0*V^Aq8 zUT10^g8-Y?fx>!VECAb3L*5=Pa3mcG7B8seKpoh_l(fUsZ7v6E%RHZPe;s2!Q4CAM zJq9J)I}3ONz==RxDL*w}ES2XTE^giBH*kUifaTcU!IsRhK?fD4pn(K#wPgPv86YJ9 zF4L=I3!jM(%}4&=^nR4 z3+Blc|KbN=7YK__oIj)R!7Fco^;PimmRBf3+&1h$vm2xb@&MkD7HANZ4IulF1vHd6 zKC>OY{~63iWI#v}u>JTxsLGNs!!(#ef{7wz;q9#OLCwW2?Q(BdjgwZc$4UZdauD-f z>~M{wgtR-YaO0gd+njLko!SwR-1i7ax_`u6^QL7;QPVZ8pIGmbh7k3?7oxPxK8g>UJyv}r|v&#PtN^9xN- z7J9o#070wT6jsBEYxdAFdR=DrQ&VMr3v94*rk5jm?Bv5bl9Mh}?k$VgpSRrf_g(*- z6LR`NxU|&J)fEhRm>DiUA7+R>6}kTT!>LFfn66UtEECVr*t`f}S;=n>a#KdepIV?R zQdh8e4>KN1@!?U+IA@JiY+66A`u?nXZPY6^TZjqe8ub8uztRGmJp`^;J{(?j%lVjJqwF7umc1XAYh8#AVil%HJMJ$pQiGHz`2b4f~cBYIP z2MF^gfZ4A7^buYh6EXLEw5hr*JsxEz$Q467Ip#pdp57stU~@gk%?YP-HGnnE`Mfml=#`nn0FdZ6 zxquhFa(`71h_?!Uqyzq*JRxZy`GPS1qx>fqKmp*mC*~#aeVJO|_$8r!@6aJI{O7`W zgs&(1-`ls{D0#LI9o&7neh|zS`YT* zz?-!Fl;NEJDgNJ~@!|b(zMT;ZzfZ&i<@=VR{|eGjIvCc$2hOu+Y*+>|2?YeGn*UiK z_YwZI4$f$Rn6|Qd>SXvJ(;lba%D0!LA61A*C4Rt(paz!6aJp>1MTUKV`gY38sQON; z1xz@ZM)JE-ya+8mPasRrb2#J}ixJSicodn_3{AO^?_D>p_B%%54U6g0GTs-7Xp07y zPy8rbVcA9ipBYa8I3_?7vuVrd;&iw$Jp}>dvv-Ph+!i(LS?SYQ%R0Abv_gK$^m;|OLJ z1&B#Vumgm-CW~4GG^C*i^yKgs34hV|c*~Yh)g}B5qcwCIJ~RGD4M#pa_hA!4V7X9X z*|Qj@WD0`><~w{Yp17$Ky+qkZ;DStdZcfbfLI`CKHzL~vngmF0W} ze;mg!^(CgHlR{=|S$)AHGSh7s0kO=e#bJ%6ZDi(J%!wH^qu54Tw3nLaHo2xAMuyWA zA91`wr4w4$T(TuL)=?3tQ8(d7b|vKxBxg$T! z0Xa~IW-(T3ObgvMeBRGqJGPW%Q&>|J3Y!BLUqnWc@=ysdu}&i}Vi^`wsM-Y(tfpi) zVN00nQnZV=`EL1lr#|bNM)HINDd1QYgp2jKygZkR97Ctl$zXeinc`XSQEHf>%Q@ zKgEY}X zyUo4`GF4_bt|M=X(OGQ<;M4u~uC=Q!8HiKo63m&Zr3az9=l$Q`yC*DUpm9cyy-SVoNqRG75guIMi^TAyYT_{LipUi|i(N*9Lr&v8$wJ-`K8&YqB#iS#Ya zxo-eSBc#)Mk>Hmw(3|uM++->tg};!b`Dm2`{d7y$smcM`$PSNM%f3W{@YfE#{B}ez zniIZJ$w0c6pW!afPlkgY9Ktb3W9_e@e@P&mMvsi_D7CzR2hjxT>t~nlu#Sgcnu~s>o-xa3?M!|s{k7y5M2S8he->7f3*`Of#k}sa%fNres zP0^8>W#ZX2iKW-^GuShjD$p28dYE(n*s-$!f@};xgSaizfC40d2>;d|mO%r-W5=#u zz4{M(4t(4y7e~q)V1Sl+4x*P;-s+(B`J^ESaj`7;vw3g0ey=aZ{OjxWMv$Ukw`6F~ zYt`CbO@OX@v;&4aXlOuHBvH3v4&O6MJJkNb0Nmug;-8@)@y{y>0GWOU^vHd__5ji% zu%toN?Gx(8>x&j&w0~%S^aM}@G zo6CV>49V9IC}>S^j8pPa_e&WIQAnYDgXj(;3dYm`Kv@ivD^(qaCI&w16dU#T*d%Ia zrI7q9E~c3Tm;!*gLJ#4uebEL6!~xm>E}o(v>z_bFg7BBmS5QSHdu0%S`dIvfE(T7R zj&*QeCEIMiVR}PA01nUsr2JCJ16@VlvNt|kb(;!TUbACY@fh1t3A*VVd&3+0kEti&3ytMAQEN}c|@Ik^As=~hJT#? zECm^7apN3d%t!~Y0n)S&7*O{43Vk4Z0sy1~Fpa3znBWb>!FUMuf|LVWVe+~V)w1$W z%ES}K{ixS7HHZB`(SuqD%87aY0z??kjy*tggs5&qsh1J~t6rV_F}oL*1cs~t2F``b z9>EXO+VYXRrxUm+1<_)*yt0eOZ_XjKB&wkHw|vtB0}qGkDks=L=xQ2-4T$!_!#VW2 zHXmr!0Cd!qbNR{=Jwd;)goXpGQqo0ZZ!OF+F|f+un2h`ENu%2A=f$4_vkkCKxY7X@ z3U@%-iD7y+w_E)gNtZQHTd;p@_9}D+EmTX`T-q#=(JTDUa{N5&>U!3dYWwbgcLdBd z;Adjq4DSv32X?Rqf9B*DpI4qBx?$p;H%2~4n!hA~jaz_$^87L^;#UF!c`Ua~ z8UR`F33KE68uVZ2-xJY*3=i-NWcvP41|I~qV#weR0`|y+1Q6lwcpvuhkv!-Krr zm>1(_JdB*povTYp%zC*Z)9mDM(rM7hw^WDm!{K>O{?+yxT(|+2>7r zmZ$27f4ePPDh_C#2R)7$1!MHY0QKiMM#bMlw1ZmPL#uCAR3kuvtO!ZfZq+9TlVU5t zKP>}whhfkWZA3dNe&2u4s(F2kOhROT@FqR}t{rc_3B!jXT7*6@KD2=Mn3H}0ByOiL zf1+O(V@8sLZ1rR&p!Q-XUr2Rev^Q>p0MFGOr~9su_-O^^_KC8 zmUNg9!X(n}7$b$kgBJr4Ux49oqmX5nORj4i*dTuoIsE&G(-4pxyHB)v`aXpJt{ni*YprRo!0|0I~+v96&yR7=FA5Er0|7_@~bj`<(0BkpYyw z@OD%;7y%OWAb>nMXaG!LLVo{tBV9x$z!Bk{?c0aWb=TB)pCVgaIwSEmP#30uIPZ;* zQ%g&u4Pj7%KW=%v)pdTG7&2qIg`i8Pel3Q85>#a%GM`ET;XwFCI}p*3`&T4~_V3jFMp*WhB^tr=u+0et6JuA#x& zBt4o2rKhegY~^6|JGJ`*AvEn}A`eIh3YY9(uyV#_E0F?RvD{hEz?LW^o-JF5fiU!v z#YI{Jn~Z{`MVlo%c)>Kr0O@8|(|zB4w;W!h;+K&JULb3BIckLhuJqz$#z#wGmjoT4 zy{BUU4vc0^h=}25%fubRDNV=}UBe3AJZK0&8{pUTgE!wd^S+`7KP7gLD=`UwE<*Q< zKu|U(_XmIQ2Rq-~p`%yZKaBZ4fG>QY^#Er+*xD}R6iWOvEQBfu>bL9Br<(OMNniwC zL=TVCZ--9fI&Sl5_JLI!9H5w&r3WSb0pXNou_CptfRou2jrlSlqxrjolG0N9sx;tf z+=(ObX0xyYg&=b5(k>Bs4wr0J6HHq`+iO#Pw9tHMmqU)gPE4219=lWCb6edztJjN} zKJhfK;5lv=*YN?`4AEe3Z+&;u7<&eg`veU~VlzVlfkPSe%Ij)gRkj*FIYMY^9Ea{}HiYSErs%Y=Nvd&GawJNl=QE3lwg4 zm({U!r{v34N~DA^;m>A8aq=+=;ViJ2b(gODkJ%C8GF^aBd-GyQf7R(<)dM87=3Aq45dGaOzpo+3&FhAw0V0pbC2`%4*#YX(4NuI`^(7~_1+>CcA1EyDw3 z1n{mDg*ERtn?T$!TmMG!SMJ{;`sl^j0_bCr0GJC%0+fCMT96O|>5^c0vQHe~!}~8^ z+7AkNiG80x+jo5bzI_mcNC;Y4zV(kLWv?C97vip;n-N%O-j|!ko1?8UUY8z*A zMNEnV$A2{unz}~n3lo;wMMb|sO9dXMkpO^giaP-TdXJJ8FpR|CB3xr_ zwa75Huqer);peeJu>;oJ4FLdXQpwQ|=uT7l=2(2*IX*neh(Qawn(~DQFz+L_-}-H~ zcy{eV4~#&4M4c~OkIT4o5YVQNrvs-1!3~4}hx@k!Z2!Oj=|k#4>end^Bqs`B$T)DX z8il#j;P`aH9a1BpIm+LFR8wMxR+Oy3b@w9e{FoIw9YD!y(m|JvH^*e5jwm-UjX) zk0x+%W!y{84>^ev2S#yTDqt0Rh+_#7x66j-JM#uNQw^zL2~|9O zx~?!zQ_3Vdb@QoyX6XL9|8d212VxA>VI-Tbu&QwgukN{VAMXS z@zn_c?o$M?x}!-TNx+Q#Xb_AB@H?^)R_MQ{37|XyMGa8-4+^;bI0ygbnNKL}Ap;%U zw|^fTpnaGjORj|PF1`_bB5eoFG}g;0YG(WHnL~|eIbx#euwuPsNqaTyg`*v!_;YeZ z&tvej4L@xzS0LwbP>B!9=37Q%_jH&u*c~CBdum7kOz_&$Nv|yqi73kW06!8EzxvaWLiQ3e{CWEZae z7mn1^vjO@K=p>=6n;ICGuvK&gF!aczCu89v5I(2A9D)ilZ6rfgoulcb37EXTSWrwi zgDZ%OYU00@OMX35JcN;TTZ_$&DiK#!H2OVr)cO+e-NB#?U{6F|+EfNrJD~P*jAPDm zK)(N|0v-_K00jV$3EKKk33)CmZ4|JD3-V*hDOBS@qaDn*eeIdYnHMgT8$cy*)U_&{ z!57OA<;j|*mB09#66Fopjzd#jXDHLx;tXiN*5Pg@X^}4k_%Vw2iv;GU-Wwt4p+v5B zpp)iS_y?QN2kh8GjvMG$LoNv`Pxi$a-CDAR@Pkd50To<41ld@TB`uk;C4mWB8 z0PKRq{1DZDMSmd5U{Jg=7Eu8A@l#?b8nT2?{AZL(4hjmtSh|z%pk7dIM8_YdI^m27 z5&cGo71}y3uAz)OvB$5FqLu=PmRWk*`cwlb z2UHJ#O#}6!QeMWLgt+O|33D=mJcGH*7>Mx7R&bTaqXh7^;rFXwP;0<*mw z;0+Jd;NRp9&UJLjkpF|(`w8?PhJX720fuI11>RF#@E$Z^h6FMh<#E7*@9rO>0zfF@ zFw7y$18OFEYh<`1!&jRF$_%hO=r=cm??FV1a-eJGMsK^(Ie zu;X_x#*#}pI53VBc7ALM7>~M|?h(I(QV|9F@bdNPO~mrg;B{W&AGM%Zn(ahw6W~q3jxNG6S z<#mz{1%Cjce2M}{K#Eny?5&!%sdb*C=>PtMPZ^zQTZY0x(M}rBVz6+3RRgpK2h5{e zC9bPcJq0#AA+Z67LTwevhGd0+C-yaD#7_uDlHmt+=ts-&id+WXnt$|9e_i%P8Qg%} zI_aA7k;yoN9a5u%y@2waS*=e8z}#pE=Xp&?KhR#5>j#vBu&*c8&~IN;=K1(ztqPJy zL>$xlSQ>-Xxy=WWiV{pyZ5O{DHKABxI;<0)N{l8bQk>S6S%5$j#0mILvMkrb&|`|8UlDF|cmQOezBy=Dx`Ryb8NFVXPsbp#ZAF7h(6A zxeW5p`7I*QOT8!lHOj;C&xm}yUIyY%bq@fGU_$&b`N5EY*&jZ^S`Zf*fV;Sl7JwE3 zQy+kSKmcOd(0_=17Jc3&#sTfs`|}|Qfv#WOf%f_IDG3EhIE4H`WE1mr7?&<>-@Tvy zpQ4|Qz~dyCOMrpLdENIOsReZa<^uSmUaoJKF)=9$sXpYrYB6kYpw^}jV9A!5f85I{ zZm~tL^bD#3AR=62p)nS{P9AA&m(UlQ;N*bbk!k|K-`6>~0l+Vo4?rMFE^OK<0mvB~ z&e0GcC|asg2*ivGaZxr3{acXxs3_qF_gLxnagx=%NQ=o$sV06J`hf(~K<$lkf@}38 zIp0moB_Slk{_lKaIW7=!Uj5@Un(@UOeJaY2nN5?J%P?2Fd z1-3mJpB21jN?BLG4LsW0=hbPV?H`{v9+mA^7SCFuAIr?htm+x%@h&BiJ*f65h>%wX%oOK10PM$qqI8#yhPd2D`Fnn-V@39kp}kK z=HsZ}3FlXg7aD&Z{j>x!3)yRQLgWw7EHIPAR7YmeXhFunMq5lefZ;Ov2N3}A_?1qy z&sQK`l4+<8zWfTNL?1)6FInJljKBf((8@5rXq!)vsX1puS$zK}@qpW%E`ZkrKr9MA z{2-4s9-NNga!5J8louVhh;L6a5M==X!+-2QrP%eNj-tmvb~VUHAvV|BAn{}{Iy6r- zdj;~C9+v-#+eeLY06`(t(QCK*|UFp2Z&We&{pzWevP6* zpk&n1ps7%bw}ubulG)1?f{Bs~ldWk!m>Kks-;4M6-p{pKd&ue*>2z;6RFV7|hj098?8 zE4t@gwa#JQiivFrGk?6j=XNFh+0{c_*zg?;T3~;-w_HJ@p%!NfqVqQOTA^HSL1Bu>yhMbN1x6CE)-On3 z+dg1^sDo+aPv-vQ01C7D4a7pF3{GuACNv7u}cPxBge_}E1%p`i7|fGp*%3A#rZV4maY+iI~9F&%U8V<;&q#Ck%> zlSm>1Qpj03(21|WSgE|iUM_V8iNrpMwQpM+wRY}m2{n^!n$ZhU#P3W~ay86)-t;m$ zU2%sDnh+A_bZD<y^&NT2 zt?R?CnsX-?0v9Sb6#eW4l5H4VctgOpMi^Q%A#TXOq9^1dk%6S#meuLU#wxsZCAR1W ztj%H&6AG+`_Z;|3#MB4&G%rsA{A&q7yJuv8NgRa#UjleN22}7@Mf*$z0A7!N=a(;U z0s1rW=T1?ANeAlUSp|5ag@9X9_!I=lYxU{a{zEkY;;-o>$Rrf}JXRC{2?X-2U+;#x z2Qx?B!Zi2{_L$}o=F$X|5->tRhU*IwXdr*J2r>b9Pkld+dPhS&wF^=w{hi+dZP-0? z88bkzKn#a3T{^zcm;mby2q4seKm8MFg3uD|-j7Ml1p07?x6d$kLC1YWoF4W#fqY#p zaglIMxg8yMcHtem%{F#nY|Ifnd9oH^DPsgAPjJNkCZGTy0>;dru77ZY1R%2ylK~u2 z0@WPOTD_SRs&{ zeR n#GK*AFXR^C>;v$X+T0lK(Dyfs82p02Bkv~5Jl{TntJ4FKe{kmDR@?ifi;TvmAp|&hPDa z#v+wM2W`P=DQAZ13yKkU%nn?~*mIy&iDgRIrY|_6E1}6tL&7S9^g%2=XIHT-5Z9j{ zl3j|7mn}{5$^BucB-b0DOepcP2IrGH; zVqpcd8}p(582NBRzzkRsgvq4@8GR0+1XqsGcf%t@Vlpq|)EWg7O(D1H;RDi=fUkY5Pp zb33*j`!5YO3V0*vpxzRM&K5ees8oP@@b^S#qo?cq7hh~kGX!Kh-*i;U@)L;v&EO+2 zba?-v_6@cpX##KsvqeX`dQ;N+GY2{>A8Qw`g!j-2#2SRxOE`lF6F@<>i6#+20U!WC z-h>v9qNsc!U)e6EP#1A8UwM|1f}H!T2gFeXa(h09wm&?H`G<#Cq&D~EoY8(9WTG~A z9vTkac4m08@IX;8kk6jQJqXuIA%tspu)hc(`kQtIN4mI1tPUJJPy#6d@)x{;Aa27; zSM5R#6#s+KG*jvuC1wH{k@T}RMr2tjS45z`FI~k4$CeENKiRw%E?>vqKl-?%@R_=}b*Iu3SN+W{! z5FhZi18vy zsSHOUcLdZbi8-;HkkEI73 ztGa2x0DsEpMfS3J%%77OiIi#QeGU;t<1x_4;zR0@(XIIu;6ujanU1`etGhuXG5fb; zTx1j&pwPz?_^SCl50C~UIfgy5BM&TC0}D-LXjzS^GlZw+Bz!Zm<%ckV0aY1Y@K7qV zu!z>Dad1Q?it?3hgb98s0Fy&_=6e(X z+jlE~0r4OJ64a3XIrE8A34ijj5d<^JAnfpJ699OCSE_#5fOD^cpXg`54-jDY(5X{H zu#GT3n;Dux6F`msKAHd|%fJA+1QhH)4ryqn5jzjL_cziBa8Y<$jqa^`oN1q|Km~1 zj<2F|?)kR&J zayl$&@cU&reLQ$Gyt;a)Ec84!MrYZRB0)64oC9U9RFW<&AXt$r7XnaP4bdHq;az33 zIar`bK}#LWdRW0(DVIU#*h&k_&tGRSbU_CHSa%oaj3iJQY%)U{3peutWV&U|^8o_h z2IV6G)Zq;b=?|rUHM3Iwf!g!-0V3L%hnKZ)oQHA8nvb4L`K}VHUex{CH+QAewkA+(NljRLq1{*+@SIUW6 zhLVDc=(WM%EXDx2nqDwktOkyDHy-Ur@MD0&A{|c*cG31z9zrhzZW&tza=@Wb>les~ zf4;m-<@O!e=y*Jk_S9{P!^dE9b!n_0Jx0}irfF2@LQW!H{9 zk;(7AgQu*ZH(LOYpI7sN+28#i9dIE4g8EF?{ViI5)&H+Gesmv8 z{=EFp@&FGvPbi&%ssfOzfNBMxn<@Ylx4gk&7#=Y8Vv_xk7MKE`xaXYbg@axjz{&#h z0ezPoAcXK8-V)Ko_IBVs63M&!4qrNT>dfUY4(_M=XCIJVK;{722-;2wfF=MBhYqM) z`#z~}oT2-lsh5{yik^-28aZAkf)06VigZn-MxRDm|04;2LF=)zxX2(U2d}SPd>*-h zX}y6JfM+-7(GNBX|MT+ukl;_7mI!#}b!LU<>HJ9<$R};zPsZ37tO4$;nK!-+EgCgY zFVJ*AG1{nKU*8}(j7VYxkO%}I6=b#t9O;#AEUbYxR(Hc;+S=j1xXTus)^vWHQbL;2 ziDR>@<f-`t;D3e2Zbk`v9U!8sJ&!!p>9R=Pk9zm<8kN9_-Vqm#akk6 zrLaZEu9woGKL9{r&(%<^I5iHjQ9t|I7%;D5lC$O${Ot#!aAeDZww9$r+8lOgGzugb z=bHJIhl6^$8iM6Dl@g1S?AH%_3Vd&J4-O(I`^gC8fa&O|$_Zeq0tCcS_tpvk6hmzo zsK!^HUljZA5!_ZeWl_CL04;{+&<@rHR7DI3U!Vn6&!G8$5(U><1uRlZgkjhLAQCz) zIsDr!PC$4k!y9^jODLJEMZ@C`d(N`rVrK}f#fDIT(-o@ZiyibaBnCF61Mo-btne7F z@9tmCm*|7WdJIXc5`$d%BkM04!O&nA7Y zfD{C3e-ec*@G@YuWT#$C?eN7!;DilcT@<`>#KL`0(@Z{vGgNNdWoD*Mq>< z4#5At`cqxk5$A_pKWse!0JsO@FM&@ClNY4%OBzrDpg=HMpT-l^JO!(SfC6dGog8aYlPlfzR{ZoPeMHfhYrnLx_tPcM86me$bnc(2fKObJMZp0hbzD* zGoAIdO!fUhZb5&7oot=8fqn-lpe+t>vPZt%sr4^N069*7-1yllKQ)Anz==K9phdF*|%vt$E~@j?uVpUGuP+cw0`EOy~}MWP>~6NEJ3 z0=Wyc0=a=P@|juJD{{1tJD&lL<^8pn7DhSN-Xaob_I+MaFNqf zK0W{gOs-0NhV0Xp9v+swaeCNIbuP7UT>YdtPz3R38BZL0b4_h(FrbcBuHy9gMkk<5 zg#c2Z95GEVP3UV`0hy6Uum+SyG-a3rEHs%zxr90+0uxT=zn-$FfrN{3-d^>EB&T7e zg||Zf!UdToXLa?eJx`migC8lL(KeZ_l6UPLC=Vb+N-p9nAyU>h;xgP#AAl~)!q}Ks zj3J6RTD8R4kFLVMy|Drbq3q$m$Avpu&_JtzDrH3F!HsU+yd1@g#I75|7#A?3#MYd5r;Sy?$S}89?3t&vy}`uOfa`}Yr4Az~s06?*-4>`|(uwkfDgI4E z@S~as%*9j!5c;A4aKuZqlTfD|5ZRA0fSLl?dSDMs7x1nmL3j+P06W28hV=950p61^ z>CCzMb3>PpZwKN(^{an5^9hmwGY26Bh!A{fpNb91guc6vj^NI;C#URr1TXrn*sn~;EZMP9)bnig4l85Cv&9z%nS&lq{MZZn*QGUvq5Tjm^M*A z2e-8Tr61Ntc=427>-Q$sfeaBTz<)sgqbvqVe@FsNDgo{?-s2(&4s1G`5I z7JXJJAZWGNIfOjkmMiFkME(4TwqzF0`6#1hCT5YzQ5ufPsfQ@g6j%F4Mvk1jcFYyZ zjitT(nf(0Wby%;l0v5jX%c~%H*}h*-t*y?cZN6CK;S>^g85hyep)-Z=XQz>@QZj_{MW3zEG zVYwrdH57JW#lQdNAOH4SZ@o>OP*{W`ECrzJsBc{dxLXcSSGWA2;}%LWbJ6P~u4nXr zb?y1MwGL{sqW}H3wfbue3aI#T803~%1g{$bCTpYwN)*x!`T)dm1;QhfIt@)xLlM0$ zEEv9-)mgd+7!VBTMdL!DCl0?Bgk4R?$JE%LEsf>IT%o1@+Rpl>sKr`SOWkO7d8HF) zv9yImby|VoK;jCQd_GpGRX|4L&lj&pn9?;5(A|(jnAnEspZZB49R2N`x-NGf>8`Q5 z!z<0?J#c~SSVO}RSj4m%(A98(ra)yr&3A5s(^iF!T$dd8(6u7v;<09BRXr%?aF z?}&CG_0#|aKOd=bmni@$-=Kr)3Z4lAP;;O$pD;jOuhj)f0LlPWNI~}t`8OzlKF|`N zwt!>{%m`1i;Pe4v^~u>oNFNdZ&OU%Y-Ke$%ycO}6$p2Gf9_s&Z@kQI2>HijG1V#Np zIsUgF;+}oS&wTi+f4zL^@|jb|cOSfbX8-m>Gwd4)C^*iZfEI)H*LPzQBO(pPjSJ$* z5W0jM%@wm#n}q|lhXml5bq8L`xojI2vsg=YR;dFAsR_(+epo#{N}x{>32=%CK2HMp z3HKg|w{3m#D`Ez~0bDcSMG|?BXxEemZhE!Ob(=Ek}Ttb1s z<~l>0??5?l*g~w))8{jmi$qE#?HjgY6pT-WPbbkN{(>u{47V>n zqBek(TvvwC3$rIqZ6IN2n;oPo$T|RdbtREq7AeLmnYQeTu$kA$9 z)CRyxsdl90>C?i0sVhC4jy-%a2eg6KPz=$|W4W9>p4t6kN365G^YYN;&NHVv0isg! zR$7&faKF(pt?xY5EzDWJstrL$(#?91k|5ohSW?YJGGIk8ZI;^2+h!Xd&1CUdAceAV zuqU4Z{l9Ue6bn-UC;=p0q0*kckEpsJB|rQ%Pyl_v5A)-rrUH2FAg0*8FZ}OY?fdJ{ ze)jDx%6>(z>H(^Erv3_dy-4HpyiR|?`v%-IpkI|t^g12zAvXp7RdE6?KIAJ!0wA(f z{xb;xXncs$53fgEJ!Jq#`=!qIZpP@H zSf4BOPyGubu>W#-|KXuCGhZCue-P}SuRtB>GL9hc?muz_LSIdA;aHAwDzPu$o)Koi z48xONBz>3y_)%X^CKKyFssf-OUzc?-+W*Gmo0sz^enb{s!eSFo1^aUn+$90rmD(Sx z{k_L~xNMW`smfeC$uu+t1{umSNr0@w zkhy$k3E_mOH|e7y(B?@D2M{^tvkXwC2nXN$0E_W^j@w{3-q&fw8M-mb1SP>PS}rkFeN5 zd+&e%CQpb&Vxd3Dc!ZOdx&XYGv9q(WyPj&aeZpvk z2Cb}s9ytoaSbs)X!_lUiYYW*Jo1COTYJMyb1Z>dSkWL_cz)yn}jA?9J8#93-d2>@p zEW~WD*N$OJrKCEbv|6g9la&L1@SDH;o8N!%=z&0h???czZg%yISAB!3ivTx`(#N;# z^|16StY2q82fs3aXurHMv0IIQRq^Z8^*YW;06fGr00u(Pi$VU(+y8a};zL>sH3O&v z$V^E6znB1j!ZaV4o`|k`fYAfc6iCWjl3Yt806P261M72B z2|!L?((U62pT7dl!g&|s4t2dBga=e_1H6##|3`U;9X@|=1Hw?Rk!G6c8o5vSi%3Yp zm>N`PKo3yQr+T3T!`V*Fg8;z!6zjtwwT}>jL!qR=X>i15PV96op1B2j%Ns` zCsn^Z0BQE$*uhr=S;%NqvH(z&1*u<@RS=O+i6G}dvV^QiT3NC;>vHIVi9qXON18WD z9LY|DIBXJ5-~@z+hf|bu*HCL}pxETDYJ2at3{B}7X0w<02NPpiY=X7;KPMqXp&D#$R%Wp*WC;Ok8p06_W0}Ur z!mufT52Y|nd&Hi1G@6_J?2aAshd_|RaQ5`~fBxrxP6qgNK*nK;`}Cjhzs z8jTO}{aFFzwn+fDH1{Kbr2uA*eO(#@>ZZnk)b`Z`pe;Y0`g*BqidJt|>j54A`iRa# zGQna28*8!MPvx^$}Z3}qHQz&msRmrh+~f`4$P`|Q~o4qgNy1S#V-d z_8*X|kQC@0#1}wXz*t5|JhX`l06*a$dHfLV0&e2$10l@R3-O;ywkz{>Vw~Q5fw)ZVS(CcRJ7@ zjY24E*_hj!l9B{!pl+%_FD1P_ya()PZ8j7Xw%{6LsjaGU^Kkm%=ZkKzapVRlB2l*J zizlEaoE+B_#Pl~jZJVfoBuqF)18UHfD!+e*a!C+e?UT> zq;Ol|PyCB8_$iySGTfEms6>RN3BXf}|HJ7AF&bebWc!VRYACtd(aI;yHv(((af;f> z4Z`%qy=X$)r-FikF(+VfNkN`-IT|2m*TSZ}V08Woeddm02>8-=W_sum^S$yFFVK<2 zDSdamVPIy!C7@ET^amB92@J_q(Bo2i`O@)26#kv`|8#`yl$A{Z)`Kgt+>!com{U_8 zNW&->^3qvJ4vc>vlQgIEl`P`Gp&XZTw-v-M*wbS14qpc$VUrreRfrdh{BNKz8f@yn zN|$2KgQQA4y0Z5-fA@Du0Kffq-Q?)2`**PlySdTX*>!~k@OphmtG<%<5!e5#|B>z| zA?WZ&2aLmBIe<4p{e%&`RP1x{8R7e{D||)1DFk%mwXDc%fB-B&GJxh%6+rzjHDJWP zwGJS-U!b2fL3wMY04(uB^(8yd6cF$N|0ojx3?K}F1ETkX3cx3q&t1E4=kL~*PIe7; zcV0gAZy$d0FCU(fHpt~){oAkpMf5=04Xgk@kzmN~6T#d<%|@;8b##Eh;^DrE{l_1P zd9kK%n_cRsA8^jUJfYTMTUWg}{o?)+^asD7@E59o0@H6%{JS*HqXZc589@_(5&yevDSm?-Q!Ve9WYQbivaX0FoC+n5@r17yyEXMRKSELN$d5!BmvP#2YYE1PbQ)q z$?>+)algao!3@IT$ER}Dv)I>X>?5-XL=*`MC}nG`p|BiAsT$}L%)dN6=_G|{;1Hzm zMNT5n6UX$@p5-71SLW=O@%@8=hjRYIcz`(JHxV%Lb5;eg0c8#-g>8FQ4)^TAX)^(h zFgOJ0U8X(LQlo$)gIIYSK041uC9>?6&w!7O0hZ~hO&u2L0=rpdlP+H6o1^b@ARP1%r{rym6Zp9s)VU@^Adj9zLs90r0}@GiyHq7iu3IyZkwN?Qhq# zd2^dDEOW-fzoR6#HUY*peRQWRi(a-E`8hfYF*cpto{l|IBoZ%@zb^>Z2d6xCjLSC z#RT9EqFY2CK?LvVZjeDW0){e-z@L1;-~b>d%3hC*K+YV!_8(XOZsi~D2p$P7KcRR#@Pd+)c{k=cCL_^1@%o?zA{(=YhSlRq{3IB0! z(IkT5$CJsZ5`e$!#BKzHwhi_&_viGdKkq!DA02-KJ|Y<$rl{ZOq48%C*cQEy|N1NH z|8Zp{X zuPxC<1A=n_<`!t*c9h+e;w_>)rPzMto^bPn=>q&gIUd-^4f!IJ{|N4MEXhNqhqzZ* z3f7G10t-ZHb{J%i1{6sh5#(GBftOB46274*zud*)pJ9D259u9$Rf&&eF~UB5fbe|8 zfSYG64jAG;V7Z9=!U19u1mrN6=Lcv10d)zm2cZoU>w8%Ts=AAcNKE)^8i=SS!3gjt z&9K{h-==%`qd$88eZrpyG75bEeX4~H;xwJ+Z|=2c?OJl?)0)q(Ef)|35@bHc)&zhY zegQ{R4=&}A(G_yACZARKzWorfG>Z%NXUna(flb7@E0^e~NtN!%z?^>6Fc5)*n1h=u zg;-a*FmU@*{SEOw&$4P!f3%kWZvp;nkqlivQ_rEV;c)vzea%94cC|n$vs|h80yVXb zLqq5KuZHWuJ2wFzS*nIRkpWmyYs!*xF3=$6Py_#eBqp2BU3mC_e+oR3D8)SdJ5G|1 zo%6Dd!~f``3uh_wuyiPEpt9$CqW{geAMGW5P4cg4KGrJ`V1Uxvj|K0b_Bq;ZQRs&8L9PDer{|BOh zg$;wR4Fet^J;Z=pa-rzpIs!vX0R|^X0D}G2Yww*92B>wQ6OAVf42UDBWPa5@^q>QF z#W;&YBH=igPhrONk}rmjNC^N2*v?$Aw;LjmxB&2m3==7PSuiJd^cV ztbR`alru9~@vx&80pcr!SU@G31CHEPnqxxT-Nw+vmX%8eyDNO9CV>IL}(AZazhABMP-772)h-|NnIFm=20k+^sS zInqaLFLnF_YdK3xg5tG?(Li6|x8e;V9HjCOr&n&wN*gLbWs@${A?FpbCy{crI<49s z97|Rrv?I6wi>Y&uY5VTZc(&@*?ptR~wKcn%Z4qQ=mf@9Ajb&b67^F6#i&_JQ2r>)? zVGvh|um~#&QNk^hq=XQfw10$T=`gDeGD--MUp%*Y2Y3 z?e=-TKeFw|&49Ut_?+)K&w0-Ci0XsnA>!u%|E=W|b>#G_5neOmkv4xRNB`87O9PG6 zF?9C6QGZKiNvezWvWX?YNBIo1^Fd=(XQ$1#YyZH`RVi^uEd$Wv`LP`3&t|*{ig3i^ ztY8B{z$_OjxwI8hB8=t3)yNLYD}G=%u*zGcMY$b~B432FVVxOZ4$97RTMI}-05i2W z>%LkA@PGWJ5JJ=BTeComTByq~gSU0S4SSepT}Xrr1YiJY zc7Azf)&eY_f8ty!LD2r_{H5W+T#z&YGk~Umtbqa;jA2xODd{h~d+N;Px8Hf^y_+{* zIeB`FLVN}^@YJc-_Z>TO;*BbdVVf?z1X_q#=&363z?}_j+M5$0tydlruL>*4w4?zSI z_Y7;#Z70^++r}ul!q|YKVOqTb#kT|@TY z3wegv_4h^#k>ws6ghzWvt#^!-S{Y2G-DIg)rjOFt*lrW`0Ui`{<5bUMwMVsh!m&ov zU!=|KjQG%@MzN1$57~^cgHm5>fb0MQuS508`63+Ah;X1#$~16nKs^6l0CF$V#{%B5 z5Ubys)*)g%^jYlPEjA$GD?p!f9~#hA*#s96BhWlgodBdFaP-D;KtTY450DHb{^=#) z!T%WbUTL{~w*2-Wp z+}{WBkpaiaGD$Mm@ayVmcV-tyMyjd^eINj7gsyd!reIJ{Hl9{*Uxd3A_XnYXi32`0 zTRVQWv(XtKN7cn(U}IuJF~8VxWpS!xieG2~FaqgHl$th$i@pyVuu~zOA**3Yq4h+f zzm%$mt@ZNO%Tpz;vBr}(p+oNhRsq_fy4&`i9=5gdsEs)W??DTG;s&ZY00C?%0RaEY z1XNo;7K@L!ieN>*-U|KGx}QQ?)L`yltpwtDi6{0H%B3&gMxcDel5x>}>o1 z_OY;w+0S!Oe=PY=ng5aop0h+hAo@`i$lHJ4K3KmD2=@`wFM;#zyKv^8PQ3B`M>jvb z`PwVj|KR#b-yrteICV7~Q3Y_~)IZS+yuJ^(;B~M<76h;YYAs1}L8K40#Q?$~0D5ek z3gG|4X=c|pm^^jxK*FgNAV4TYJCaa()yLDtBkl=+sl7e}1M=bHU#>m8$Qgdy4JQ<@ zF>JEZh>a&t8JDgQWiS^rM9yriBb1D;z$=<3ZcOy!kc^m%#IC&x!tC z3Q*GF7+t-4_wIP{F1Xt!^MW!_%TUwIH%5Tr*^&qThv(yEoX0oL(}%*+MOF;vEyX@3 zGgAUdQ!?Y1v2G;FkZ{0=iJylKC~+^dz2lR;RTN-BsR34i2!L~XRWSZTdBDT67j^D4 z7y!qE3;WHFf9acVJ~$6r2<-=O9^nn57+4?vb0CxBJE)^)q(IplH6067EeKEH$uT*d ztB@(9Ca7%*L$(ORrVF=pbaXHW5Uu)fHNYTnO$qMZxzV_nyy&l;zjW!!)YN6F@Y$nT zz!zl4J1VD2(fqf&qGlyml?t`{xJgM!1qR0QL)A2P*5S{YJXLM?VBu88TyK+ICdki=`TuvhKs;z0=fji zP&}yt)Jr1=gqlB<|1U}m0Im-!yK)I+9-vHMH2-K40OBO(#dH9t-`@fJ`%N+e4B&lC z{ZG8{PoMq!N3ZFmo0*rG|{W?LRV$TJs?yuMFl;Fmc6kqSUBrGxT0iFR*>^oMmB^TQb9B!4GxTmqHvOmeNEQlLSv9= zVhTV3Apces3|u2Xgh4-zwsUp-E;bxI27U150O#P7qt+dV-YHkH1X7>QJj9pcg6Qka z;pZTEJXrl=E@N}I%r8@6Qv%YcrU3_>=|H}dJ$Ok2N8+BgU}-fB_@Q(l9sw4^NN>nF z2JxmJ@Jl{csY)10Et#ILR~OW)L*E|jA6>Slng(%}*VK zK}p|U;c*xTiWsLQ-UwQ|oZ2W0VE{xjP*;Cms@0tO1+DZ)w5Ky3q4pIg2*z^;4IMau zjX(u540`8fviOW-$FC$+G50y*+UfFVMkMvS$ zfz&B{vAWtr6UXd!2K>Puq!?tW2u+y@$`=U<2NUUntVSnK@81&{{ccN?A5Q#rtpl3p z^-Rxy{nT;;L;Kf~{nr@(;bUmPhPun8Zop#ytBP0O|0OSM26CXA_Fw$~{eteTDZuvN z-^v5r)N6f?&k8zF_?tDNLu>+o^hvlEU|%91#JjveWC+GIklV@w^Z<~5NC4yl>H4xI zESrBO0M8TB|8(Z^mCrx?yFcOjgFkusumAexKe^r)nai!u^j4jCTSewZ-6G2n@t_OGuy&P}m~4^zp7M)^(QN9Z~WfZM_H|Xrb>oZX(S8 zNdTb#)B+F!$Ljb1u7luFs(8LR7q)e99z~^89FoWdI zxCb*d)YL%RLktd@(HI{t^qmw#KI-G3p!;StM`z;qaTdlMGhFf-Cff2Gf01}*Sg3sw z%Zaahw$?8qcsGV#uM9I6`Uez*ks`XdcX=N(S!at&#|5LJWnUKlxZj}}d3I@_k!{)n zqePG^c9wW#+6czcf=`+=iafM^C_Tm-i(>q$jRXR8Y8C(LW-*Rf5V~a$hX)`I?u|s` zHNsD(>6DEogS`bpIzJBmj_|kq{7zFS*J#wokp}!f9`E5|!jTSe<`YQM=R4U}WSan| z{!Z!%X7_mV0sImB00d~~F08UwD1k7>|GR6(GDwurex!Xh)}FC+g!&ovN#+GFFBbEJpA#7I{SGf!Gx32M;Wj{5C9%Y6dbQhqCW*301vLQb|oN2_{>~VSH{0l>sHT zJo6Nsn$VIuO4SsA;~&k-;s78Orv;`Am=aci5P;8gb=2~&D3U-Q;p{z5C7#>L84FoJ znMyV)q4{;z)Z88S)rR1m>!Ql2o?=a#44@BGPx0H)u!ek?l|g$X{J;2m3y*p6O&BEs zW=k~=vB@?%Lhfi!hLpSbkIw|MLmls3zaAOgV&>1;F9?v>=fae7aLXhBgVss?n=~NR zPkjH>wd=`ncunA&j(}SVe>}eh18O0Vcg+og-img`zHV~S`|GWhO86vSZq)$1pwxNb zfNK2bSk==M5R_kRKSEx`KjE);sRN_~YWP1t+4%zg9>4R_F*bl`3U*=v$|-;O`>)>o z^FO#w2Kej0{@a)T_HUm>J^G8DZ|_u3{L`sZRSh8j`+i~^LSBEJ1aN^2FjY}Gaiyh$ zzrS7ds~DXCj5UWGd1cHhbMBBU)&>NyZzme|yn==b^^=Yt5Jhfd`s7zzSVMQ`5mi-DeMp2|yCrSV7KbO$?!jWDW_6ac9{J z!AFD&T&440{^R~AqvS$5gZ?YBJoRZJE~XKG82+M^B(;K;O3aN%`o}bOvjyS=TaPrK z7(@ESutT*cpLH-QW{8Ak2z@bwZ{(umj-Wh7Tmi5@Usj9IA>w#hbA7}$!T?Q&50is% z1q0_>_xn`W%AJdvlNnk8$IxLt^7M`SA*#Ii)^uN?XAmZ{+p`s$CL-nIWUeSR%k-x+6gFBJ$si#=q~6aacb=xTVE z9^uv{ae_m3ajRLV#-f-!)0pj`;}JB&;A94FsGHSChz--=2dB##eV#7#mni-3gEL4G zQ1V}Hnnw&jBAuOd*U>(@5LNz|HF|+aT&Z?cpey*}lB36KXu7nZ#5}(M|C#+Qg+Bu* z$}j0GC|Q9fETB(EFQhQSV<+4?2?v%BYt=K(KHJ^Q2DmD z*5x{{HIiVEZ4zP7cZMM$a~Rni={Uy$cmyJ$p6cdHC-?Nh|Cb{$T-9L=JUIXVYn>c0 z%n)$f{znAB$2?I6NC(s`@97c{xIORJr3s+UaD>0Nhk#}_pd4YoMw0-g4lDRo1LzWS z01!c$0x}RR00=dV0yxsC06+i?uMa#xIDnME#Rk^epO8R=frJpU%ulv}(Wm|&+COVP zrzXnJ{Oji*z4_;V4g&BeQ~-a=rGe1i=+=68acY9*>#RUP62?9d12zMX96xoqfiIB!BZWpD$gU+4!uP1&IF;0x$mE*{XQF`oX3pSsnWucK@X(t1y8$b|%;vQIl zap1!nq+#5^VgC}z5_qP&52~~$Mu~KgLyZ9e4$oGdJpvU#vw6N69bxSO;sc4lP^t_6 z;AU0@qzb}A&<*r7uCrp4@)+TMFD3DU&zGTFaTEPw4Y8KKIE1_Aaz7aYMo+|W*AbT9;^Ktj=qhufVX^l}1GUYZ#PKbh_N@Z>Vph9WlLm*t=2!~#&% z0Sov?aP})zF!h7V#}p4^Aw%8=+eaZ#a6ln9IG6!g6xj#DO&LsF2@QcuWU)49p3T$3 zIM-dgF6bLN=>?u4_J91%GXQ#f6+^In$ag)^6zFrz2t7-e593KX;bTsL_(-AwucYYw z(qnnEXM~$)K8%b%l5E63+%OjNjd0MkesNKqoQYKRQPct&+4s4|4|3Xoi9(JsZRU!m zx4Fsi1LQC7TT)*@_1!u$($rn$+4}myG;BqH0l0#46)~JiLey=acLJ19SD6JOw1g8u zxPov%S|^O+tHu)*bpE_ab)n#t1dc2e=2jm(c(B0f|J@JkgLOQ3+T3mIlHLJMmN7L$ zXxm_pe?A(ek3fQo5gszhwT+H^O@#PY2cSy}fLdJ_9>5+T#y^??a2QGT_l9Qnqyfb+ z;Y{Evehmre{}KStCic0M8T8B(GJ=#06ai-S*UBHH0lh*ep?dGnQ5{`}8h zdBv&%{^aGC$pi=>t&;jwP(iyyC6E??ih*^&*AE@6Du-}iQirB!Gq$%^z#nEAR8(;xHA=yHG+E|1x4~$k|iEpy^Ar9pMUmvkL;B^ew&2Yk0 zNNK*{OEVu$!Yl~Y`#p@D980&xma-7_S}>SLk5*-|w-zC!i&Vc9zey^22*0_^Dw{Yp z#t#HQ!jEid@J<^&68mqN0m=R~=A8i0bU$tzV)U?1mM15NT>5)D(6AFfs74NTSc1Lv z0pq~^H3I_RB-tsmFak7Q@G*1*+K2O79L;6CMa}HQ5Yi$oG6X%4+9MJip=K22tS#?6Edn@2*9GDNc5h^ zsgDI2B$NWNAZ=N564CTp7J;--aDDG-I-N9YjOZu1!ndxSq}C}L0ClGzbD)(rmXEM! ztg$AbCiW;midjU%lGUyFGc=$b(@<{DjgC+6k-aFT5be5cow9??uS9MkN;9)-|Mk~S z-sH2jHoF}y7I6icJyfCoA$lU?s%D{k6>w82M*s<~KKO1Fm}itter)APx)3q$aNP!X zf`2k7A}3TQN+g^JV2dK!mQ7GtmSzj0089Nt!7mJe3P4T3y>IXQJ&{l64;lCqqW__> z29*S;E>qV_8knA68Ni4HD%=(LGWXQ^&y%$pWLf^{-d{dZ|Fz_RtPkpa-Trc$3}E74 ztUCFJXvq(9FDE@jV1)jD(X##l8XyB8<^u!(`ft{HDCoriX65(9<*zs9|10WxBJL3z7SNfy9ql0j6o1^2x7W&xdpCk zI7Up#;6a8R5CLK&0E~;7KfbWzMo@!5&Hw%rdH>^wXal6~x3M94Fjj)Qp#SlVf>;;e z-3`h8skWCBi$YqsflvcDU_b#z69CRXw4xR5ATYZ@gc`4Cw1Be3!f@he0Ubo zB9L zCjz6JrQ42LC_`ctY83&6ZQR)f=O&N^;b-z|7J35gG!LVV2);_dBQ+q&G5}Hrr=0zO zh;npMFVP@vha_Su{e>}-4{aQ)hF}iNX0uDoJI8BqF?#R4`}Z%sJv58ZUkN1f@>ZC@ z&C(l_SzJpv4D&MQWnOCB5AjgBdUtCO9S@ey*T21UaUJX!H3^>F5U+hvGzaL&&R=EW zXQ79gki_lKb=Ae#U0$P|E8 zAL5SS*J_{<0|54e`p1O?*Z~HBYUsIMKlbiFUH<;lH~;Pr2>(C%+rR#6g};FU2!Hax z`RPcUrZVB4U7R>^=8cIH2hj!LYndM4AKWB9CLW>HW`^#fC1m^`gDWR)UIKV)jKy3Z+})`p^}D~ z1%Lto1Fn!05RN7RaQ^cw0npx3$RJ`4&0y9|?;Nn9Vnq#$w1^waA*6sd$DN@dqy5N$ zR(Wd229}pl>qBv)g{rf{mAJQ_7>vi!{s1-z)CQ3IQHt;mg?Q}aPd66Lds}2{UJ_ao z38sHC98Qn=yr9(;5Pju_;vfBD4!dAR=p6lh{P_Yc_~&?i^d25j0+JzCizA!GtaBk6 z2um!ejp&EFdV2qUzG0a06%l}o+0Ymsc8ta3?PofDBmyHaRxqw$$|uVJ<{F9~`2kIv z=&yMM!~h7XHrWg8>7A3=h>`Z<^V)m#j%Io!I*fY=v<1T$8kYtjAZU>+hQT^OuHNCIoe!xI=e3}p~;05~f6wnb?H zA3V4_4uDGVLzf_#B889}zNeamGlomUCmMcm+A`)lD5**yjKYa~#PAh?9cFll5KrvR z=<296focEmAOMaR$VKUZ;1xjKZ>jzp_g{3uKT#L2z_mJhngNUTqeb3rGOtww&;=NW zPi|QkL&L}Vr1?U7P>rsQdrxza{o*0(h-t@a(SLWnTkT zw~_e?k&dbI?&CW}?ID%mB1T7Wp}V_*y41$T`PA7LUVwtgD#DJL z8_8yE|H|4x8^H_*2z&Ygc^>}f*|U!s{0(yfjLkHsgy;dsh;s)90B9ic&`#q2h$z7j zf{r(+ERbb*bFxh}0QW3OrrOtq@e*>dDLjyL(5|Znw?B>?!EB zhr)~@34t0C0y%;w4neyoz=t3!7gAj z-zP&pauhX1+_^Ny_z%X=xuc7wNcW75*YuBNZJ`b(Cv5W4!%HxVk56W|OjIb9^pP>q72R^HvO`Q4?fYfX8BaRV$Lf&?%u!kaDil#)~7(o+P`= z(g`Jh=CFyN`j@~zOw#%uCc~lyp^bp{ca$Y1e;w!~`^%&gNQiLDaF8J36<6g6EEk~L z;^?Ucpb@|y{EixbUeNO5@FxM7bpUgAKZFm=8$wuop#KjA66c20BV!Q7fawSSs@4xzwH@Q>-DDn*mXubXu4Fq0>7=-cGT8SIKPCM8cV&D*Y$Rj(c zBMg!7R6`XUE%9iRcA$Ui+4}kx7XbcQey5*+H~b1MK(dR&o+$tdK-vN1CYwE9!e%ni z-edBxgh2Rv-jG+A)O~_T{1MXa8%>%3LLHD};gOHg1tIBZ9@xa|wbV=sefi`JRyAq> z9yV8(qEF;s$}joAdvbo|b0#mpL8oCeWd=$w8%gfqx%hV^R*Ml~6D;*FCArrgR zxU+mS*2s1$=Q`&x{85)9H>l9#q^BPSrX%#Jkip{E{;iNmFrH$z06PW6xttgWO!@K4 zgvvK82OTh2@?K{yw>2!AK>z`n&{4vR{?mgoZd)%cLdf_FIQ;!=+LuZD z>z97~DkoPlnt^{_pM$|p42$wlon6CH9Rtp&A{UJd6i!3~81=CWqvnA-!CfW&jLKg~ z7Q`|E6S0VizfUUR$c1zQ(~H)FxHuks#+xicP{Sqw7rC}HZ#;HzUZ*l)w1&zO^2@}3 zU40-5P%3C;E*(fmsZ*}+KG~O-Xpjq6P>Rf7BdlapfRwZrRy zLhSJi^h;K>PSINM0iFL-q51hsq6+kT9rn`l%T!=fQW7n1tqeSXJmu?2+cdN1cqU&c z`eA>uE&vaF;?n)oOh`y^D4dAv$9NObBiWo7cSal;w^-(5P=!8JAP(HiG60u^(pgLD z$=MhJ^d{Sz;Q}@>1FM3Agb!9phz4?j1fcUD0l+(V{_WeJY7}^zOz>~U_51((|4S2u zl)x(j-z)@ePXyHD^J0KNYWU+i$n~;edH^F6sx)CW`x*mm+kol|EsY@*Mz%pi?vJaW zLDz)_JSX3N@%yO-RR0SGL?Q_BKlndff(IZ5?t_^_mKdUJKNdQ1V&Ar~h1p1b)te$oZd=A*1FX+V( zxO8nz?gNKl5a3#XRiR*nBTWa1|8B$uVFfmG_!Biok7P#j;w_hM-Otw;SkH-NJWL#K z@h)|4)az#_aE|Jckw5LE4GN=3-$`-}!d+{dWe1yZMcD)ErIv9yKS4YoqnM z9M9y^9KG<2EyqE6{5a&@N7F|53U?2HKLV74A4Zd0v0>|T2m*={`&uF_!1Vp*_rEcb z@)k!RKtNPVV&su6;4egS=UK$p293zQ+M`1Pj#;Hg%RJ+%aG(ood>=7#3V0o`XAee? z93U=M*O#2iXEhqN<_OC;p@Kf#+h=oJLxhOFTHc^ zwUa0JbS`)uv+W~Q7v4Q_X6g)g9-2p#kvwkvr{x9NdiYFLQ-f4S!GqQ$|I=PsD;CwW zk~)JJ@k_X(K1E_vze9Bd)f0}yJ?2dS{qcw@6AK3s(|RH91g1%;nj0aR@C!CE^5&bu zL9FiZAWzWw05wn!e3UwA9xhV;YbWp@Sq!G_zhgG`)D7^A7r15s7=UwD)76s)#NsnI z#0}J9;1fc>H3!(v0v~ge%NiR9|9b)>erbt{NK(WA2&xqHB}yNn1hW<>vfp#8_%QfK zCY;x(|55zo?LqBNaX=($sedO$JjFW{pm+cE`%mBb@WVI%oDA^chkyLxAM^a-o3Bs_ zPy{IYl?Q(9+1&$K_r+RfWioIcw5{Kf1<&y#2eIoA^7sO^z)tu<-R$Tx7c7BS?@m@$ z%-mQz^gK?)U=t953!#Yl<)$Ci;#jH74-kFw!REn912PK~%EbHDa52qhW)Ph2Y;tiJFV#&f<%;N*P^ zK9uZ(vI&+>@ED;0$rt+I7Yn3^PKMq-G^Kr!fv7Xwq0mWn)QQcb zPe*(a4fkM>2ZpbKt}x&sNGlxVFK6}<%8&mtx6{}A_CC)XMX7yEMpA(hpQdm%u}G^f|8Hs0*v7?|N7wk-hsHh_yuaTv1C6O zhx`M+UuMvU@fua%2!p+Sg+4<6t*rLKF=1Xu6i;Yw4_BAX&%;xTZ(n-nCUXJON!HWb zyV7tLp-R?L%Nt5vMb;q~dSpA3g0RcU&&G-sp^(3Bq`FE04p{x}8Nd3}FZ}W^y~=p= zo2Oo8Qvy^^G>RTE_&H|=>ocr+bfQE+gR~n?000UM$PC#W9FbMB!&DT=jh#Xn>%^JE z6Tpf{9F-x)a`!vU0VVxM{{V<64B^H<{uJNvzt=>qTQHJSrt6S@&Ul6zQ2DHIB;pG-eB>-0tws1Lme3Wo(Ym_!0|9Bt! zNiV(hn^1w7)~^8q$l&WZLp{9(*dr6r1aO^QW1uf#Fgp4*f?AzF2(bQ_DJ?J%*m>m0 zF_jO8kSE(|!~|#q;1!O1^01f1UsD6X{b{SG%!L=LB9^~3ugjK_WAyGEHl zc{E`LWkVs>8|}Nhx!@?}AHr(ctgZHv2>Jm#jCaTfxDVboXtft%aH(oD9H8{nmLA;@ z>SL%GmsHeB5kM;0?Wc=a(%3Ce<8EW5}_tzsn6q%+eWHVwCJ$Wy2T^ZkkYg$rhoCHhe&&BH>nnh3FdLFrCbA`A5AutG=xPxC zh$xFN?Eq@*>*T=>SXhwJ2`OQD0Id^gcv!|j6bZr)@!3Q^P#5V0c@MS`u}0~ailF@R zX?KAB8Oq0dNS$aQhUmX5?2C5h^e1D1`If~Jzyo5R3(c+*#s+`{(8|8qnKy{D33-JqH(pyt^xK26Kg(Sk zO~)Imx(~jNpGFAlOb3nu={GZ4-C?H3;H3uMO zt-!VNfNm)GpXUv3z*g{Efp3ift=m!$<~@;#^`glk6nnt{gqEztkp|^i>foaPu<>(j zXLkb`lIht2>TwBxy&xNam!R~V`0U?5|LUEOIQMn>-z50o;ri&Ek9Zee069RofXacV zpS^xQzD|uvF{Vs)+7PC!jFZ90Lf5E03p&Nub*ll zwSg$GQ9hm?1W1aaJSP_+$p%B3i{zJTI~Zt*mF@`?8RJiv6-F69YdpXq9YJ$kd}LDI zMVx}m)Ef*C$@pFhMe#-1#9>SC@JM(gujG+kZ zf@oB9qH{ciPKpShAy%ep_;)aKq>>ru$K+XHU`T1&kI_Ll;qMO7DES=@?>G{l&=($w zBm_%nAR8j{wzgMsxWXZ6ZYA0$N~TInnAw#|#;287Yb}|uOLrt6&jGq13k-w&Y}1+Q z_Kkz@vgBRjPWx6VI^3m3>a*c>#DGWEARfZ-t4q}TN27Nock<|q;q`RcQ@`;u8d1tu zk$GfnRo|zW7c=pxf5bFG=b<|Hul!j5>fj9Q-5zy*5Aq=f5QlAWuLg04)MrC%L5y zuuTHevy}k26w2n=3VnkHYL2*#9_Y3iC|FyBT)^lD&sy=X#XydKZ~(oP?AL)~N8Ww6 z>hR*>Q&l7iEd6K!(DkPfK-=dH`u|Vg`RI?$#ko)TfA!T@N&&pDD&UpZ-h2&A;JO-u zm#^P^@4aR0W2s&TP^yn-Sly>=f6P8#(?I}87K?UDX>cEjfNlUK_QR+ZXjNIe2s_q@ zQqFoa%WsgpKexGf%jr(WV;@L?JR*kQ3THpBHb!v!rUYmxZ({u4-3@3!<$pwEAQ_nP zk*cQdrj@nF%mT%{xgj0^i9i&;q>HBA)`N!wj+>izIh*0xx_fD27jc~H@vU-V79#OU z`ISwSw}(*Ftm!Mz8w^z%J`olN((cLlic;Y_&yEfg<$yOGvpnyUY{NTLT~q%?eF0V? zb5X!>&^|P<(3=u7kYjVUl7p(fbgH8T*%>JNWY1Mq37en=UO)b6;-I*pFVf_FuBu);ZN`j%??IGG}xBv$nIhw$$ur0Pr=T! z)SB8m?3hT@)>csGRUPL1hrC-|-7)W#PV*?`I$YmWs=4~oQnrF^K*Ov|vWILBCC#9K zLISp5vuMY*p08Qo+?3?Ep#Jf#^+aU<(?5E4|8>@>Ku;Pu{sX?#*G}%Ya8YRwYoEFY zR;V;grExApQq~wMXG=TyeX1z^PrNb3@lz_@xw?CUQKXaktEmJq93XN4m%Hu84TwM1 z3cx?>6*q6(G1~yTK0z2n(Jy&G5%`Q9sNn-xZU6rj@q)+9P>+vWALH(pn1DusR`74j z30VRH+ZD>hdw={M$NyKlC<(av#~*#jb@L{;;A|Ng>x#MB3f~nwrJ3Qn-tw%t}gA! z0gwg|2PA{x4DTbd@f1L=T>hlk6zt%z)+i5ge|7)7)?UE#Y6s53?FS0v1wUAYJ?nyc zW~pa{35D@NN_RS|XJ2FucFC z=*GMPdBpC#PZnW>(f;$h-v9CY=f^WJx{3dckHR0*(zo`@d`O-Faw5Z!RNQ8Mf+*lw z$ii|V-cN>*JPE2JME?i$*qHW)t6R0v3!Fxj%Sb9^Z-Gi(D2vQDxAG~XpC%)`R6w+G zU443;+@3msnV{HqXuyv7?vK4&;lK@D*H`H#QcP%WSsH#i_Gg1m5PpddI<#lyqL`bON73q1Tk zpNA2&j|{N$1%m=I>qGeOrPog}HDPdbWj54Nx>9wVjUIXa68)e3>y>~1{L@cAy?_5J zD+8$aSM)0Zy!WIE01$jl6T&~&hQQ0uUf+8X^XQX$P@b$=#xjSdDgk%}lRDP6wsG(P zFg?z~kPTTfU_(K*|4$f7Kug@g^1o=nO^2xYDd-Pq7eG0Hq2QNa9zSc@|L~U1dxIrG z5&&R~u(^f?Vx5%`EtXdy1H(tE+F1f|(~U)H#u?)0je%C& zSxZ4*^m(d5!)qhV%gSJgvP14CCL2^0NZmw%A~qvY5tMKs<+HAOe!ehodf(lg{Sp3Y z0{ejM$s7(3>p!@FQ(R0-MaXgw>J+UY8c<*Z2Ot?L(;fH$q#>q>qzMLRL{=6I4%TUNABao=%PZ}4?TP5LG5pwj*-7Uhk z(E*F4kT*)2*(6iN)Akg0!H3Ba90)v>^9)_qQ(*|DPiRM)Ch49e^5u&p-X@ z(@#wku#E-^9e7h^z$<_9?BBk;=k&5~8LdYr3~&^qSbiz?dox?#p#dhYQ1i;l!Gq6Z zGyvHEn89@aT1JpP`3^SvVE`cRx54bb>THt;`wh(Y;Ph^MNesj6m0}>bMmEk;4A2iy z|2LO(Q3RB4Y&203@JZM|<)jGS0Q-_CnmAIpHwhqhql>1WvfYu+urW^0i*;8IOYBmb zr9qe6+h}@_7XF4S9LRVF-IxUEN@qQxYG6S2ibhIHC*D9$a&B(CxVZrJG@un&n@5jU zWA&9?>Z8g8&H_qwaPsHSM8?brqQ1o;mv`@r_g?#hjV z^Sjdw0>#{kX6@DDx?HQtJLFm)njF|myZ;Jh>$PPqD55)q8TS_q_kB?HA^>&$fpnOF z_E0>8+^7}9R+eUPU@5|&J)UMGGwg)&fY_)cJ?j1NmX#uEIG(SoT6*mJd3d#&)t z4G1vLgf#|13SPDT*0Z^+cb}{W@dewv#N-0q*AH8#Q&{J^YcmoNCE2nCm#_0Bmjy4 zkU^yZK0yi~0lfJN451hI`uyxl)1wAB0_doCoJu>yR=)+oyoai`3*7I}PUBfBmy(|t zw_^E|?*LRE*&xh0w*hSw@sHW?TVZy~)hzn{+6Fmct*f0o>eC~TFWZSq(13Em{SA}_ zy4pu*74&U3s1i7@l{QoY0K?4KFtEt`crb&DLz{t#7v(&d%k_YKjgqPmoo2vSD1#gZ zU(V-qc9bM5jt&iBT69*n2p!>goB`a|;ABAMlam1*@X21(EoO0-PI`O@NP^?%SY*LS z5?(Pt_*}dQbhq3OoDi}nr6yBR8}~Y(_XV6$?vDtcurSTz3nJ@v^1CSo6pOg!aBPn; zUsxWit)Kzz7~4yOssev(X;KF~;jMyxFb8+IkI=#dpTiubl?uD@z<`@z{53=)}f&EmlXYQr|}8+2s~5vlJgxTCT}5qW@;ol1#wpByDnU#R$bWHg2H0w01Q z`XpNdmC#rT?Gj5)IAs}1Bm&k%p{DZ3>WiPx-n488Njf4Yvs}zKn^x_+@PT%=z)vQP}z4&CLyhk0MR*NVUhb(HzK(qLYYeTrjEL2##iD z`9|w(IK4mz29&}=qkPQy_xk;CkHo9Skq}*)J8Wvl^G0l~;1f$>+1a8Mt(aXN(y&JH zC~j5ZaTwn58$gN(ik~r*AAytLen*F#(s;berFsZ3)zS|W2(VoO{F}rpwEKgG-3>v1 zH2&a!GV+qu=-dK0y6-*$ee6k26^5FVB4+OCD*on)FSN{DR*W2i!&0aZr<+IC|&-~LHmlyx_ zC=1f(E^++N&%>FyPo(SoR{~J1ussQNDCBVjUwKRrgZiyC^M_)E zlnVf`mjRq60L}atu~1K>ueC$^?>>T)C{4l!U4cU1&;!pB{S6}xz=zFL|73t*h7I=9 zec)-@?g94eiHj9WR~)MY79b1Ejk22ah^S;H;6vYo?lns%T<~7VIdBh0CnF#y0E}KGXWY60xFAZ1$8ZLNa@rfTxeG1#mBdK2SQ7zQ(|Xmfd^(Cs;~ zeTe+#dU70;;?@VqKGr~wSUnFaUIPo(JId+H#;vvpNE?8ULd^_$5cP5P?wLrOyhe4B zr3s;RP*u%XQUlgBKmw8j`**2OCRc2Va1|JoSTkTJXlD#6Y1j`d6EuPG-2Us=8LUGb zhMC7`WH1+?o)?Qkf41lV#SXd>c+8;nAhLJZUx3=>ZRxW4R?VL096Tnb9XU1xr9Ey> zLRE&W#_Z7T))IC)?$hZ{Eh9WM>a8e&B-GM^{7^G!BO2xtEW($+L9~+qCggMnu&`DF z=Y8nXSRS5(F-z$*0_fhcMr~h{*U0%M0o>LGu)+SiQ~_|%BL`@a0j%oZaD&b3ZA}pB z04V@{X^estxq|#8@&BX(IIvv;NFU@$pj!=q)jn7}0`qZhs}4}`t6(q);DMcT@D~{f z_n&>QpZM%wkDfa>KmYdIq=0j?i&waC=l$$*2g?)xdga{wtxqp;-MSA9@Rbn2Pj7un z0q~VxDGBJ6`P3iZya^uo8U#UVfU+p_rGRQsC(e>5Pu3J^PT{?;MvM4je1S#63=6rq zh`u=2C+`WyXKm=On(+nSAl^&i0wM?k zTc`m%pj>T$@B)m1&=ad7_|G<-Jx(5Bh0tZYx5{HD8i&B5kr?g-!|=Z9>tm1*llDYr zfnRY!nTiK10A?rz=MQd>bifd`O4Du+!w{EYDC@TBp~q%R;FupigYtX}yJj<6bD6|# zMu3UIoF5E{S~9`I*#~_j#k3DVy86m|e}1lBTcv1tI1A8f25LA}eaPu(`K`DR&q(nr z!ThktCu#x6BlT!7h#8?%>%sw}sweTT*1{bvL|>$g z18PBdHnJy0vvBjM&x>w>ixfaw!V*bLMkaqk>U*e>b^5EVhqo`(+&bf=I|K&+DIl(% zfT)Nh6C*pu!jSrY!y3j_8=1oWHF+g~+DYr|U(i}2%}3Zj#!wK+QR$bednjOjkYL?b zhB0n!=0oTUvJg9q)bc2jX?N1irgt!?@J~46<26^W zo^14m+iYYS*w!puNPs%#))rcI2^A5RfvKb{Y7wUZ@nL9z4UASF(OgE!J677zR6a3P zQhC^1WB{W63~zt|stA4{ib|J9;%fT0KBz4J#Mfc027#Y!0hvh2dI5uQGoFXQG7##{n8@xA2M~olODkA2W(3S za+$%OwMV2BU_eAGJy_F2E@;4DZaW$OA^DF?I9T7Q&xrfC3H|v?dVTch(JNOLKO+eo zoozXKg-;RuW(shhP{02U4Z!ER2>)9=^BJXq`}c1t50C?(2Py@;{NnW8yPZ^atkj9x z7AdYqr9T_xV3k?o`b;wsj#9vpgBK1NXL9(+G9uog?%s6qD*hky!fUkz4DcD^YlcVU zGsiDU!qDq_3>laW>~^(jBmiImVf~~EN&=K2`2xTM&_QdXm^{HcFcHiduVGg`H6Tki zI5#5z#sePJ2@pYk#bSZ*v(>vL9zZqP0t)9q&CHHQZ~}SZAa=&XJxmS8H;Yk`_fv5A zur;6cjH=4a(V~uy4#Fc7Ww)L#VXFtF5w-#BsgjAKn2Xa0KPvXzySFt+lo9d#VT!Q& zz~-P4iiskf>b9U==30e94{0bUYm*kZHSqJ0vcwBSk)BFbAYw7wG4C8hdC*aZJe4Ms zDXh-8JAM|aiV(zps*97I(1@AvWzgzk0lE6_ zp`^Sf0%sbTvEhqtV*Am7VG$Mzz;2R`F|*J-sx0gcaQ+lfPfP%!rtDNu;-3=vXu<^1 zW+I6DA^roz0Hiv5{|DqudS-zkW1rQIwjQjOM6@VSEa)9W6=Ga$05EVdRfHHtAf)&t; zXj2G?!?U4q7Z;X^7zyB@rU4oQnt>s(zhYiX!IqYY`LM!%V|$J836&Vc9a;^ndp2*L z7ug4Y@5@Key?yI#B7dGH;HaKJ0)PZ*1v+@|bK;%jpX*Z-=am0%zpW3w{Wdw^(%YZk z`t%kBz^9*53fxjBKpk-NH8KF48XhqbfE{}Z(NEOwzF?nB+)^Nb+rgD?4eT3E@CU|T z{20Rtm;h7)J2u+E?nZhEea?E~URi*5KGD8E=RXf@G#QCXi0149ezIn4z3}&AT-;0- zK(!!gAblwi0%e1sLMDXJ0VV&DWO37DyC;u;!7U8HV@P*YBnAg#ZH@wyK(1B(Bl!eB zA|(sapu3)kK^U-->4KfIco07_2(0M9Kp5xnsf$syk|HL3sBAuX#9;vM9RW#XC|&lF z6b3;VmWzuuNOvt|?X&X;-N3#p@^da;+?wkL5i}>fZvniGaO!8(R>S&QPIv^c&;#@% z1kBl(?5JIi0K2W`!0X`9aS^l?v}m{R^aUv>)CNh0QmU?$&`2<-^`Rv+LC_iQlY&Qv zS|q^PI&S!^1Rli+1p`oxEPOFFO^+@22S5ba(nhb%(cgF)HE+}It3>vBM^yxg29QqE z(KzE~V~~+%l#BR=V#sHGG9J;jpH^ZC(J+q(%P&+-hLJatUfF&sWLgPeKhY9M!gX#W zPG(Z{{0?uF(!}rwK=!QF9J-3ev7y;n4)3Amy}S1^V#Y9pmdxX!W}%#yQaij2n}4hb z`C0QsT#;ZK2hu|wQxl8x%hD5svz0>ggVb2f<0#duA#FdJY3L9^lNhI`;rRB^5{_Z@ z$G}<;UrDK++If->1dx_U9pS7ufA{Vqx%G`h&rF3SHc_f+;v5v{c7Y+UCk!(TD z98mbb;D4^QW^#bF4~(G?&LG?(?4=E2kO9E}yiYi5kH>-l6uk!x1CS9o902R|hZ?Vx z0L=$g080hfN&u<{tQ=sQkI^34Lj%C)Q2~%VASNLDs-`&PFA3zs@9!M-{yEhE zi=YC!&i(tj&u`tjM6FN!^M*^O|M$FHnpXyRdtT*%iUV4JckWwQ09?V>uD|^99;sW@ zrvaaAxwMBRe0PEigRqChX2bFBeeCOZA3D-~;UK2%OjU3ag)O{;*cJ3A+8$3tGr9@Q zYoBn-Q~_M<03omoj>e&=G-1?z_@2#NBMoQUd1plR12?Dxm?~%&@VLnUYes>GVv&h_ zA_m(}CbT`s_@;BK<28}-2l0#z@JDvV+O$by6RalK5k>-_IyAUSWM^{F<5vYN75wgG z2@egIgJltz0C%H3ufd1M6|dGyjltgDIeHbUN= z9v_poU)(tsm#h}0zYDxh-XJvEs&2X5E{MdP6yPiia?ER+QHC`w;adaL!hJOn5HvqZ z=Umk(?O>7Vm^s!kND$GV_#DlVRWkgZD30e1Itc_eoWLLjn1D@(tjKR{|K=HJ;4zHq zVSW$X`12Vyx001I;`g!S-%7U~8mYfy+fehRH+DqECpK}^>002ht z2hI+;1=xWfz*FWQGC=uZFc)Sd@EJWG_9~-rm1#{R`2~U~XY*a)2#w3x77pWb z^gQRs2Kpsi3i}txvYNUDNTh@#8J%C$ly<*GBZ5yD@@6nEkXycd24T^mu!|0d^Fq`y z^Z1!@F_w`C8LJWuQJ_DOdjo#4p8`3D7Qfo585>qgaUWxU1QG$&Lc)fb8*I@{nF&q9 zyXZuEh@2{aEB=&tGPBQ&Alo$EKV88l@h>Y-)BsG~&#PvXX`AV4&5@5JkW_)zKDl$p zk_aIaDE_V0Acgc}tF>1Y6Y<+cfM`6tcibR>1hz%=we$e=YFkgtxPr1<_TLor&Tn>r+0f#6bPO4G_Nf-aGGoL<)HA`rp31JK~s;cS2XlTkws!nibIC^&?-B zzYaE)cOQfTv=fexXg&v7(LU0BYTwQsgl;o6Kclu@@&L+F1_;olOwexnfKr6p$OW8u ze9vS`vw>zV;(nuiq_t8vv?pG|a6mMnD&$0(&UQl-;G1^I9~=`)_1BsNe!E^}L+d0(2nA)XcVmMDow8+EU5iu?B|K|=Idfu>qe0&Fg>t^vkCwTS}(*+e1DnWNayw=j|YT2T>+G*q~ABDb&he;^ZdeFxJUq8&Gh>!^l9c;K2D}o0JNJgsM_FGHE}qNWY=l)%?e-u-!dVtZxwcWNvS5_ZV55H6c0g{<_-MVyZOi#+Vh4tJi~+BK8nq3t zZf-`v&ZDQXeZj~pK8D-}R50Yi273yHapMB@9njg}*1fu!Pr70~UyKv6bi_4UB|}3O z1<$*9yvs;uz~38iid*48_+zww<`ZF@?G^K~s>g_*{wq$Q517G28>}lX{R&Km znCJuh!EgaX2%>4ism(s{w2}pu`|yJl<<7`Bk8&@^w2S^NK^-6VFb^P9Ii?{A@xF_l3Bjj=2`^3t@>Y}l59P$5w4gv43!9zt4`w40 z5)K)5G?$*6i;nN!_2XB6^eUACLZwI~h9^MXszVyErf~n{Dj~be*a3r&uyeeV*k{Y& zwBhjuJdPGr*O`t~PC?dMX;%r~rAm_Ne$bXiHJvg83HT^*D4tTpuAPFAG)Sq%iSG%7 zI8`>*4mVUmIHEoY_=@UE&{Bf2Ty~e4anxwXn-Q-E1l5kQMObphSNU%S+dyyi>Z1q5 zzlgxV{;KV}9zG`G^+^6?#cFEJDzIYTN(Vp!7E@3-z$e?oK&*nr;!&r6mOzFFtHb=@ zlM`JSfFj?x0BV8Q2m^~gU{F6kWo{^Abi=MYMDQQ_m{$7*SiU=-WB%WL_{x>HFTH(f zQVHOlPbmX#O_Bwu>dz77$^tt6&k5(_GQ&TU5Z*pV+;j7LKL5FH8ln(G0(t;(1&ROn z-n(`aLcsOY%S+UO9{6_H;@w7pPil~VbSLuWx)c#{wg@+!E(V?R@RL7sv~VjIB{F9UXE zS&AR`_0%IIh{k<7#5vLi%h&5;RX00i2mYYaNqVy64~k-JMSF6j4Si{wZEVv}@5J3D zk0rnju>{>y6PTek!U$`r*vWZLhb`lPo47hg66 zB3bYvJ}luwsuimE(iC+LyZzXIboEMGLEX4y3~?yaE+QVI9ka8%Fp-@&Q*{a;7_Oyi zqtqQ(%8g~*8N=~toP`7_{F6|?OMr#}l~wSQ5tHnh&`_4Pcnf*VL(TLPof10!z9Q5I zdL1u-B+M^LrGy7y(UWDSsoe)7nke{!1i&&dV39fe5um1gZ8q8;Dgjmqe~MYl7vOKq zY(Vn>HinE?ptOOA;!l7D%}W1-6NU4C^3vOrw{G=M-ny?z-!1aMJkhU?fYIRRv<2h^Lk2V* z07w7ZJkcBIPF7DKo}j+AfBBvFSQg--4Y+mn~gb z4^r{N_Sdcs6~Vse4{&dBpc80|*8HBGjcme~oUA(5JQ| z{>2JXO`-H*F#%x`D3j=INjba?J9S6^CsD2GCDLrwX_sY>vNeONTJS!1F5aKzM1=Lm zsIAt|LsTpA>Yi;ePHdFHe4z6E=jaMXv}MZnS2Dpiyg$=Z@8Xe(^Pd+1FGr6^MGpvz zRoJI;>lv@fU+M_XJnVhQVZcMq?rbar3-D4Ou=gKP-|q;B&l>mJ*(2bLAax~c^PR8> zAFz%#l4{#QdTI-B zxY%q~0l{S)(5%~$vhyKO@5y*ymais)X^d8 ze=2^?d@_LMe8BlU6YpSt0R5x_eOfdCU;^$&hd>4(?$sZ_9Uu#QdW&zu{rOsbgJlwc zaKLMS{2m$L`u-a4FhyHvC>&hFZ-LTvbnA9ovYm;w3P-2Uhb_Ho9b5h97$zfmtiAf4%ug8z9C@yAP zGr_i&`Rtf%Df7VP*mJUR(nzDrXuEGIT#@QhcH_CSc46}_{yyFwjUCXG7*05Rjh$Da zFL?dHT0sdL%|KVoza{(LXPz-j!#;XxAL82e4kl~v;0#mofwHTFI75iOC3{JAG6^k6 zY`K^*8YlfRwty8+l~Ydv6CY54&maT#t^~;gO9-LsSi33(0!9fDSd*ZW+HhDkR4Jz2 zt3LF=up@h`#!uqRX3+#V${|c}xZLvcwdHyWK#8b(t6~m&^D;e#5C|VID^aBcHCNE% zWyEoM@9vr``ZVJcRjYF<%VXkA^yiD2$@wDl*cPj^P|glD9}z0|n>@ z4U2B5%{Px%_~%44mymzW1J)h_{_7^bo_C}V{`cw!W~c(VEYlDj@YW)avG+W#L7)o% zv)iVjkGHLaO#jcb5`Z!Sp8^0d2;o_aLkx2e0w5#+^Z?oNMecj&sn%H%z@HC&@r@#_}zBM5_E;orvRY!IcuVy z@RzP9X1}5Z@%QWk;$PFW&kzI@G8-%oOZtn$zd=&~Y5?v{0)QthC=q60Z`+I^0;Pyi z&cp{y>mUQg)`!#BeDQD$1e}E^G}SRG$x13rqB=#5F~j^FQW~bAmCfzFDSob0Oj-i$ z$4vG+YZGeLZ?FntuP$wNdvLb|3^W>26xNM39J~&4NE`p^8QcZo;$tlU+$Nu$KEeVv zjjzl+j0yiFZD?~B!l|Bgd=>OAckhRLVv-Xbi4oYn{LF_pf+<^|kA7U9 zKC}dq{r6#8#*~#YCvt0au<@EssCg&%BJkJeMT;BQTt~7k>&%x)r~m4B6iNgDc$_v~ z=lrxGw6Vn3FT?|WI~OD_K87k7R2MzQMaTj6lQH6hO&;!htXI9LZfOmgi!0# z0pr%td3QC})?3K?Vw93Qbj0KI|*%tLmf91u7X$Sh9j+A2Ff8a zSVsr~Rs~+|^OJe06o2r+i&rU#76i6J{HiTrt^UQ9=Ku1adB~Vbgmn&oKx{PuT$*uP z$RHnv6AV)bi4au+w=W6^FhfJRfb*mTpm84;vwu|r8^D5=ceoY*dPC7*iU7+LNXVdc z0OT*G9AJ5a8XdsrUts@EI^X|v<=oIYV!xJGK!7HEx%8Ub+m*c1fC+8Y1w3B@4%prV zVEoUe&ss}@^!*k9Xi9)MvRzhe!Y`ocBb$d7&EEps7F3C8IAA0LVVP0Mq9;lSBw{4FTF4j%$68T?f9H z`-n}fuE1&*ln)M_0ZO%qll%}w7-s~zKY7O0)nP$lZ_%nu5ic3eN2KVH^x|xc7Fgti zuz;9(JqIB%hRH1DwS#h)mwJDiT!E(;a5>Xy_eG4u(rEJ;M=SZt$^zlu)@ludwSlDo z$Bz_4C4$W0+~Wnl6Ps?lsL0QOOEIyt!HK;HTPZONqS!xxxt`QQwXf?=Rq)7#)g&s8 zq4cBFwUq-3Q+!%iB=eU zQstf~l_Du?mSpc>N+y5UviVRk(nvNDGrN7@#jFbfB3g72vH-Y(1U@b;_cfNW0Rk?> zf&z>yEkV`O0eTo`A~%ZUySUc~pW2yQZa4nBiGH7uO=x8?S4S-)mDRy-soT0Z{B_pz zNF?@20MLMSLt)=+(E@Mus_(ypWB83bc!sh!2vLY^AZ4&GEt6lf&ZkQWz)}LVF75nS z3BVKp#xM8@D{$KmWW~$_)HfVp3-|>Pz?WXw`4d3E3!h!#92c8Mhqz)`F@N*gHP!Fe zwDd3O58fyXXaiVXJ#Ta=A@IzT?yN85p5`(c!|DM@0I$eGbWhnATRpyhu1U0Y{B+mo zU$7H?Z#c33!*}0}=4^GrP=(7iJL?b3cK982Vgjyk>J#H)^%2u#frI~fys1fce`58J z8Tgb|d(;KM^d$wL^+bkfA0b63+_{JD$34|D$QZ12cJ^omLVz8Ces4W2Ud%sm_zTmg zhCF_B=h4Tc8VJ*~!YP(xFwRxgE=0pn*yjugvw0xvnAvlaV_6q$UD$59%$%9b$)kWx zyTDxW?j|3Ii)n{fgb&V;mW%N7NO54t>b(&VsJ-yv3q8XzjIisW2y!^mP)7o8`Us|4 znB--O(j}Oqg1<=cQ=K5wxB6hS=44-gI7CCul>)0`pP5M|{^5;%3ljHbb&V(QTyd_x zEy&rgQ6?5$*N_QgI!-4$-4{nVD+H7wEbqJkD@`e&$s%zALI4|v37Y)0(l8`@K)wri*8N^aDXa!d zrgR|SNzw_9q1`(s8wC&Pfn3%LoL>>3mDl_s!_Q6BsooE|K^_=4I#~m*fd_c^?gFHm z?_gGLrAOn8ZE!L|b4|K}5PY!mB=#x%B^1ESkJjE}`6|p@yr}7=E!kxkb5fWe%n`{2 zv|40MMn?6~#R-ywWH@Vn7eA@t`#(UPwi zWF&BhSC3^FC`?}oKsEh}AONEVj7ydw|36s)%Iy8o4|HF-@M5?j=g#Yu8+)(T{aPTYsk9SKP)RYUnt37B*0Z`I!C;`yS>1x34S7pCIK!v|}!J-3I zw_tfS;BzE$?1iLbSbBLGyGKqpUV~)<9vuhm&V#%lx6gR#9(Fejxgp4cLfH7LL@yhgB1Kq0OQM_M;#DXqUEhw3Qf;!chol zl9?FIlmL$Nm~7ykA(y}oyPUlpY%pL6trcYw8AbclR7VyLq|Z-k8HPM{2fMdabxXF; zh*ki69^kZbI_vM)+x@S5vTM0JT^46INzcFsc1r)&sVQUvTZQ{!>dz`*>|)HpVNCrY z?=Ap~xnT-F=sjdu4tp)~pD8E&(Tq@UE^A{xlJahZ>q+ZLMZ1w&ngM5dz$+ye~1tG%4>guZQwdHmU1xqk!JoldfZYs!z%r5i>wZ1 ze(USeujjUg2e<0lpoB{iNU<+JFo_s8@%|y*+yfdvY=64v(dFMRmNr@D}K>k9B;XxVDf>4JIapwJ9 z_;7n2iCA5*OP9{9=1`WuB0(I@IEJySCocVCjUWq=IW6eKYbWp1_3vgXy`NEKU2?ao)`5q2}gQxyVAcmhzEn3_j-@!U2&gW zMi>&3m`E;0kyrY_OCTctFhE>O%TZ@uni%xxSNE3jX`yExg?njhi91GVz^>oLppetb z4iCNl>ZtxG!}nwpoiNMOJ}vs4MC1^VWPt@hE@sXph?cvr%EcSp`SKLVH5!$c>;i72EP&pCfco}nE0i{}`OFJ3lFR{#K1VKh=%XB_sZN8W1 zwk(vH#I-Ozr2v2`y}hBE;sOipg8^ik0M35inpd;}KUEh%fuNj0{3~g&4zTu#j(d84 zae|Cd$cEYYqpHx+558!|fYwNW&lo>3-dg%$ngLS$KQBWd_WzDu_>6y%8UKj{Xo2?E zboP@1Zk{Im^}Ky?rZ1agT^&Aetg7E=00HyqE0rUb`+*3yfxzYEWs?h*xtCzT-8J?6 zt4B(OKv&Fq{G1=*1aXr?%;{ns6j4++JpMYsxfu=lcffciiqQl=MAxq;kk3ORGVN2` zOCs!JsebB}aSlc>7{Nc%0QWWhf>wc701pHkic8qb>UYc$mhm0K9F7jhC_GT&WiP_H z{oeFoW^^=>5>mTDF9Hgjb7$q@7q zxzykqWK(S{RQ$9v5BgYSBw74gc6UGYz=s3wFDNO=m9H+w6kfW+bxb4!HU%U#w>k^H zt3-JQPES+s>H@|9gu@^>vMgtEqdVjP{A(G?vdRGQi85M_K?3r#VNXnqT@+SJL<&Up zogRfg;at{9uz{D1=5Tte8y?K~Oi9Eq#UGTV2dx&#xr9DR3}IM=GA=uLdaQ-rD-gaC zw(~jqIr+Cu{_QqsSc5s~ol2zWJbU@jlqBmnkfr>XINaUc-K0%^!w)1UbkHQ>GOjXA z{$vSek-#m*2V_KMlaeN#UF5u9ynJPGadCFBVwToFDLqA|Spk9;Da0Ng^=u&uc4HSp31`Y5{m+3_uI; zZuz-MFnwx(q4`T3+LZr1QU0I4c^W})Z~&rOO@KN6Env`uzVJToZCzG=;LgkM^pgOD z2dYd!9fW*vpGz2lrUBRYL=)OgF#u~8q9rh#E0JUOu5JeSPU?0-InF@D63ofsc)e=)Qyae~==`80r`339?jMQ5gH!0HSXvEA4E|eOn$INzIBp{!IP7!NE}EXTi@Aml;03n8iHud-B)5QP!yO z0wD4c3Tx~|sv*H_7EN+y70*iO{QBem>gO(p8o9SzcImU!oRe-lK#J<&bCg? zkF+x~L+?NJ>Qhfiodo=Y8A;UMymIEuDU7MwWf0Vi+88U+jnETO@F~N(DbBf&0-_G) z;Q9b}`iDwLEQ?e81RkM`9#S!+1d<&t2RjRR1np(=-x(j|H`jO|M0KDtu5|)1vXsIh zA!2f@DswE5O zXL9~r%Mks)`uPv3)?fSZX5(oRfDizyuGfnRYJ&z?{XOqnMS#Mb_~#ZGVELV;Tlaa5 zLMWf)zDp(paETCjy(Z2-MqVsz!gf@)w^s%LUO=J0<}yr@T%~6%AJ6XAi4)xot&5oQ z{V+%cK;Tl{KYzd|fHwUn9>0_ThmI5Xyy5UyIv^QvA^R`>pc;d2bp_mqtRa=45vl5jTLi5a#0Kf!{vO^#o(k|FU zrMvD2YEa-x+i+%-ExKWjFu&|BBfhMQGng}tav)KUAiOj<+2=7_u~>}VJHC$~FOI&h zBxNrRyxp%3y054ale4A55(TVYoSi41AQWD~LEl=%{oM7rtsgW7xFfbxJ1GDX%xvvg zq_NmjPxldo$%<$aQ)ig?FHTgoOInGaf(TO*2A zdw>h=9Rzc$xZb675ABFNx-j|)Ui zkc6NFP_W<74iaYe#6H{XDgk&V0YLE3g?A8R1MW^PAP>y&X=(-Pe?|FYuD=p3VB-@B z1#wfZpw^6F+fmpO5->wU84PWpk45~e1Td?EwEr(q1E2&<{l9o=a_N5mtxqQ}9li4T zt#>T^j}6{y*9L?F3JO%Uuh{2?a)IW2?-9`4AQPw{u$q4JSp)sw-<}9sEMN@{m*@$W zd53KB+TKXG>+#A#f*onzrX$D8TiCftT%74`_}JP{CZ2b4syDUrhud)$;lB+=oy0rw zFHaBIeUU530Zp_5&+jAwm}X!nV&S~cOp}J;b)bSHG7)3(_izbQL1BfI1MN@+D_nLs zSvZOV*t1H%Vw_fD$*aRr3H1A*2z% zD_M`x_1kyQ&IrLn$@!_@>u&Ri!NYD8M;idUVQH_~=(TySaJw>bA4N0;4dApRFe3U$l;P_Ca5?NOaQ;1N3e?v{A`#J$OhrD0}V?&UQvpR zWfBvR8&5smK&(E7qgbGmG3DOfuk1d3`n~0A`DKnQjITG+jd4TYDV(pZwCy=ENNB-zgCo?*0$2s4o!&Mr4X-P4pK5{GU8Ig$S$ zj&HhzA+Oid3!j6I6}oXVokHs@0&X2}{pFqvZ<8(H{Sb=47Em2pZwrN)Z-Wzbpr;K8 zX`ZAFAr5rRt!cM5*hv5>O}`2MJ3PzI-jx0r2Cg$erhFvdYSTjwf`9-*1iCa8;4Bw= z;IUx^Zajt|XjT26ST4X6|FRQM^jr3zVgQ?6AWDX9HxapNK)u(x4cc2fPh6Q-tP-%7 zp!wZ`w!7k`-ED1scdx!jyQqc)j@0xRkNhN$Uu5=R!nSE^pYpI&bTA1==iR{{P>fpf}Uj^t+XVSGpo(dkOQnM}BMiWPbjE|FceD3;HFfa~$Bs(!*qRlRet+Me# zy~=j(#Y3wNRx$)vZd|O>Q%rKY%XIFr_4i1V8!DRzo-$`RO$c3=U~xE2sg%S+VxB;O zc$D|uU0}?6F?hn9l9K-vosq4>D4~zP2WxxNvJ8c)$9JU6NG~;Za=&LJ2X6>I4p#>i zaf=<8zkLC@d8F^lc&M;d^A%0Nrqc0U?>!x#Ewe;T89D zR*8cO3^zMr%88Oi_$0bu_$Dhu=PoVfYf$kvz5@%x!#?PQT=Xdj6)aPaG1+90i9r}; z)Z)=gsH;&7iQho~gHi~ZiNR*#9OnQkvD%D3*bI}byp$!jYI|s?_7)CMU@T=wDkqDZvxZS7`}JIF)nhAeDJ1UG{638ds# z_>-|gT%~DZ<7kq%!MEo6sqrFjQyvfoxIH}3YJg^g+vdLnzhwq!!k;HTzr74B0)aLD zHwb{D{{UOSX6yFYyO(Px4dY*Xf0NYzmYy$h&odC9Y3?mjfCce=sN3e47X)Y-gKWcn z)_s)#G%XXF=AZHRE#$wu4eLF;5m@E_UEKa9B5NWBX6S6^;0J8?(!4u>b%m zU2fD&`zKpELP`1Vm8MFKO9G^VaJ)tyY%UIU%y!HYT#Fr*lH?;uXDdpicmSt?90&z@ zH=Mvf`IGBXBaGJH8T1nMR3RV}@-cyF>~JLz90}sg=)`AHc8L5nqu?XT)GQWGw2oAr zIZ@6vQXT5ByP5RQhW#ipY6D^~d4=4*8#aP2Yy&mpHKHgPXPUf%kV*#ufAr|k(M6HN zsZ5Y~?^tAyRJ$5l$52k5xzp90f>;K|%(-KZBE*94fw0(Y3|jATQ@k&6KV&69H^9pc zT_O+MAOUdJTed-l_bXW;dVXEHO=&<8U@-!u1Fb{Q9IqTU-F+5Xfsw6BrhN5&*-+ceweL zID}db(n#UvYs+@P5Ny7r(u+K{R)FQ^Lyv-~|jxl>p9?09M%L z)0-yB_>qRHQylZ%O#%V9nvT;796EA@_jvR1p$lXKg9|_xs+yS3`?B@*u6hLhV<0hf z`8LQb%xTkwah-+{?_dVi?<8d(>j7wOs6?-}4+Sy+0Dfw~RF`&fx?CAAXs{XrXJMc? z2hfeo`BEIpT}Mf2iiAFMgHyWB*|=wqF_g|bd&HFC6G@MY%&+0I3kAQzg3oJ1axaW7 z2nPFbj?y8#>M^T_T;N|AkB3E1OA(4Nd;p#$pOitEj|AfPMRDcSEPR!a$+Nt~a7?-& zz4^7lowf_;mWt%Na=2r`#1X*^c&=eFsLdh{L--k%1KGJy_K^^xiU~9NCG{=3qT>_;0W3y@KE_=K*H;1$3kLr3*ui=#G>@0->yNz-+HuQ4`(!0ht}-L?0Z z=J7B}D*j?%1vkpvS8B*c;oZwP*5PO19=1IX~; zHgo`14ZxFdRt7R?A}Rja_xdl%#QfJfxBv;@cH2cc3b0E22?4HE0iF2UF)z^1TK+*; zSZ|CssHFv~_>SpqWs|A&(CRBdlf{fe%zA)T0q7H^G@#ixY++s${Ah>iW$D%}edmGqPG58MZVvfwn=d2&WAZHA0SiEKz~Eh#?F{%C10v_wpyuJ!yV(Eh?x=}@1S${U z|Hp`<`+`_PWCScgD~B+M3S--ws;25|R}NW8O65D0iLwobXVX>6VXrW%7N{h3kPkc* zvcnE=D>vt-y`q=p0^neHdYLeUHEDlxpb*Fef;WydZ-r4OH8w+C!Jct8#;%S-s$*!r z4lt(`on(+akEFd%g4sT2E!)Kf*a86aLYqPUD%HDc4=orDt4-)t_y=vn%&+Ci$D!Gm zhgbkyFc6iQ=OEqq-WNf&(VB!o;AP#1vAIK#^Z@e;bTh7@l>@iFL~V-{e07=^CD17$ zFg<1|NyC$o1+NQGdqTZ4WSDY(svR!!<}_p>M8zcQ!T7)D?pxFikEqs}YMPUJ6M0UY zdjR-++L~h9o+f{(=EY}!{G%T|^UTw`UOX9DqNfSN0d!SVPDoG_&x5i2LVPmq0%=Xz z9rYLz*@g#g6g6ot`X)>iYoenGD*hm2LIitCbX8A*i3}l}6dkS8rfzAu&ixVX^^4Fs z3k?bXflhf7oHs(G5Q2;15$2#Vpj{BG4$aS_-~d`cr$!etTbmuD>PL*=Ee>;355`Gx z3HQY|BhRYLQYH7W%FByGbWEB>07!}!8Rt<1VQJ0KzdPLvJ= z9l|{3H3Xp&3V&$AgueV_lR?lG4hXuM_4V7x%iUo&5R{MpzKy;gh~?Y=0zJBSj}(Bv zQH~Dqru{TDAAkW3`K`yt#$anNP>?~+kd%`U&Yjy=I96eigz3xF zG5=vJ_yDRPJ2br`wnEu#^~wsNf1DDZJAenC0YU6OZmRvWeC!CdgTB_(|7!kaC;}P` zST6UDl?Dg}Bcc+l*BCOq(N{_H*FSOvkAp zq>F4$=)L)+Fno~;j(+$nDmmK$gnI!Sl6ZorGfb6NzwTj^XcWwhia$4)L6Ae!KD#46 zZG5`07v=OqL03HcF-F*0aFb1FTF50D%&g<(!KsB&6-G0;Ev8YhtJWC^u;^F=6Mqbt zVT98TO?#%{$*?cV5OI^4z-nAnz@j8F74S66#P zpVx+BB{>l&pC)}WUwkyZ>;0<>wiG)Za(ojeQiT8jnqXsw$4nYTa|lUyDX7VI2^tp| ztvgBogWxg^AQO9Y@v8)IACa+$_WIy!#FcAt6Tr}-Vcb0(GL8SHb4gABmiDX zC`8$S1VH5Tj?x18!9oIL38G3s+XLGP;Nyc7&EVxi0rtKAPyaso4&^#=YDB(4_K08; z|0?$B1B{xVE^PjIb<+&_46l!`(q(0UNx^%%uPj0Id2{}AFXaejfKPS$n?{45v&_fA z67!sFIX}Mk=@!(iQI-kVY%pu1xf_W80Quqk2b~8@&+G!=;+P0{N>^Y4(*qFxi2d`z zKjzq>BNq;`@x$DJ$OrV(Mg5NvuoA#ZNkt_Oa9M>T0%iz;l_*@bk94^Qnc}aGli7(6 zI5O;zLs?;xS2o?4aRY{@VB>PChcZR+`shsq`Qm2LYtw8z6D>-gkN6gg%hGtem_r?} z7M&9s3dpj^)80_{qb%d?Jii&KIbRk>S=eREcvn}8eax@NmWC>-2?pwJ`Bfr2Db6Aa zW%ha&=2#(8cPWy6p{O0*2ct3Qz?u*RgMq$BoJx^An6(r;WzbW!#54PRfm&efi$p-e zL$6XJ+Ap}8cmfodW|27PhT8*luNnQdc^>qF84gqbSHogtMpl%@KlA`Ul|mxHV$&A8 zQmAAzm*n6_VpHIQOoRRY? zznIR1?69ef(>uiA!=-i*o+9Nw%tMh6SV0}PY<-pRDX{o0=9=vz(vs>O|HEsS_VJq z2TB1fPsjtA9AE(A58qM){M)}VFMvoyv|}TpqMuekvq8Fk)&GcxNK05kKs-xINIn7# z37?38p}6xrJ-`7{z_HK1N4{4PdkcXN_5V8Xtz%r54*c!d=h6+U0VO zfcMJ}NEzTG3l-e{yvYG%2jqi|KER|&G^Vz=nV-vmn9$ROv-by1!@M zF?56eXK zFAe4Jjz#-^E-+dULhbZ)*!>w*fWtAeKZpzox$U-a+QGT}NtXKAzO~u#^P~F>+B?$R zccHIFR-KV7o9?U;!z*z}hs$oq0+^p7+dEKDxY3Y%y6lPc+`{-K5zn$4oIHkv5AKo- zIH%kiSOAR297tHAimmuWGE6=+dkrP8%m{00*hCC?G^5y*Hlg+qRISoTG9YMc1I{1Q zqA$F4@J5#m3{IVqctP**>K6;Xg4)`9oI{KgZ3&EsFLE}KEEyfX_5L^CNaAv{Po%tn zpXEhqCm_4S;YM8pyP3c@8;6K#yO1FVN%4ZCjteWV0Z+u|z;2F47feLx34khNK5-+` z86B)3-^j1d9z`NyDG#X<4lv;tMcntrt5I93bmegCiU8yGByvp)o%jO~$Sn2&HfS%B zzAjuV$&nKER8vz^kakK@3lJfPAIhOz3e1xp^>UsgBnmEINA`yY$Kf*#<@o$vo@IvM z1Dd4bDH?fi9A6~O24E`e&MlR7iVB&}=zFz-6tt2+CqVkD1>%74Rx0*k|AAgQ*kU9( zBW^0Vvy*C5OIPN?RpAj<6QnHn#dS`9R?*;DifzrpH<0uoc_6n%4ZsIU+!_Wb0sPc* z78Gk(3E<)1Jbq|F|5OCTzv%?-7!@(40|Ny}9mMPjSbkuK4n8lCec!&?!qZN4I1ZU39H_*Llh4lmZiACawyw@^TNfPAHQ-_#6ndk$bGg=zz=PC<7f z0Vo&fjnxAT_;Tw~&mMB(TMgp~xu-~h;8?Y-9EAGQeF_@DA%g)lY^=HOB{arPwNm^M z*cAF3CIfK9TL!^u_^Y}PHu2dFkpS#$Ld`nRY5gvyy>GU`e(d_Xj+HTLts^A>HIKx@xC!Sct2cJO0JKf{O_)D9xhPsO6myT;m zSvuhL3b#W4mz`Oz^@LH{PeQgA1DKR2pT7dsA9t#~m8KUatlO0X&K`u4O@(WkTQ{&Z zRXTD;o166LsQv7b768RhhC&$MKEoOIgcY)kAtUhe5PS>!Q8B~OAQ)nar%$rJ`f0cq z#I^V23DUp>$R>O{{wQ9mCchs5QKc@^FTX!K21Q#+yFwfw;(JHqfn zgXGxLWU0|{l{Nq~HZ1|-dP2QbDsN>D`pOAp02UXCOYs7G$vG3O9-t*23Wq7rAbX;I zt{OobgGv&MZ(l+=@fv7CXD8K`_P2Vz{=F#iGm}&bB5gKB<2e>l;c4jaj>!=b zuOeVe$jjCby0%!p`mdjUD+EBaU=jc(qQ*f;S^^;dD*hGp*8J}uZ~rsbLu)+1^Alfi zBP9Bl_T8{JrbvK>SfL5ew9?1}Y;JcvbQv-4QuFHK&e9vj^9hD(WP zEIjetJMgTL*`pj-*~j+o5c&(>uzQb%sG|>xSzG$qy7P z!8SjZJT~U+THd;?(Yl8dTHB)I^7)jXS6^RQ+1+s+ZaGMVh*yjid4C+tm~A@(j`*xy z8k_V0jjQf91wa#L8pb{D!O;b3ZCrjbvg$^a0Hfik(?y-gmeEC*lar*VZC@<&pxWdk zEF8)!u#vHvjEqQa8X)JKNb}%DV67n5-&<$Xf~}i+q`dM#?Ff0n6ec@SRN!@+0{$od zt2V( z!cDlPzz$lD{6O8?dJkk=)niE@Af zG-stB!UxLvCMp!|7zDr~R_ zMjF>}(Frj$u>u!wSXW)f8wI)oUuR!MgnQzO%@-PmLI%Zsl)E^MW5Yd|QL>VY56TwE zB76U7R69=J>X3-&VW4*z7(hs`Nbpmb<53fGwFRXl7WL99xB#dmOK$V$8Ae4|%SjM& zzA_FFqP)ly0$57LGf~Tyu6LEJAd3>K>6bJK`OOoa9GKpHeh>D1WT}CExC@yKNEy+S zlG1U_qts{tOp=2VX@-)FBFwAf2wox&$B&OY%H! z24grTN+!z#oOAjl%k+q!LNjto6A*43V?1|)omo&s*oB<}OZ^C+py|zd%wABvk10oA zV>BvbP;bV;QnSt9)8q7J@RU>fU>uNVE!Z%D{R~ObgXT=uA`k}srTi%&OfBLJeR+!B ze|DC-zk<-`p9kA@d7zAaPuM|)O6fz6K(GMqZ)$3&V$-4mIB@`)5fa73o&K1&VO(=* zCrt7Z=*pLl;7BW4hSXu&r7*_qgbZN(C1L6sFc=et5ELq8Pxe}VM*9&5JU@*H3XhXc z9xntxtqt-5yn>^R0;-qo5x0x~)+JDh(Jc&SsY=<+{2EY@f`FaO_1izC0Z;+(QY1B&fr7K7RZtcGOl22*ar~*#}4g#o%knOt)kq*|HSqYhtH7p zWa{@O>%Ghbxy5_ew)cBM^UNNw#q1#`aMOx^?q=-)2<@}*KY@j&A+Ra}E`tj4WEzeB zbSX6ntUpM-bnnFMq2sOXCH4*uMBKZZi0i}Mh=R5aeg_>V>JLiFqv>oD2rx*%aYDX| z)BX?zff7K|netOjM-CmK902Si{%HhOBmyJ_kk(RwzY05M*PwfCY;4fn`HFE?+KDY% zI}D?d>yHxS8P(CTV2Fthq0thy6R;p)Q2}zR(P##Vw}ifenO>g%USgj@6|E1*{55F) zvA>4aownw3Qq*;X0j6>e8_X^pCwJICpn#`H=sV%jqa#xge;w%Kmeb5{2n- z5{rw3pH3sef4UmS{`EbmUDfH3wGpVQ69_EKC zxz#1`8fHX(UQBlYL0m|jGUsuCk*8!NG|zWmjzG-ai~!KFOUyYIbVKEjsCI=gtOUm9 zgM36_`TJu*vmqE$d%)?h@jLT0a5#ifhR}Fn7XY^$qz9;X_mXRzNM(oFj6GV(m88-d zeZY{B>6-);Fif?JyAO*OX+n2>JZ|yy5`YiTrno{Lj93CxiIYxY2@0xC&duWTm;?iVe~&QnS!(JhYiX{2S85`T)AWTyRYY> zg`Pxj$r=`)faRx<`aKI8aHdK?07C+dz6f{7L5vP*5qS43iM?Tif}iRz83JkSq0~$> z(@wP2h55S^IgY^~D7|F0{c{TsqC(sxs2ylYx!r)t43lW-l5H{O(u4InVr`jt|5ALE zx;oA@xCjUTE*KHN8VkSmg8>0qek`;Y@~+<-&Wy>^8zufZTd+3;vpbFOtd@vL5KhRk z)i4r>{Sh=MdN0;*p(oIDFV=>bA@wUmz|tfT62e?0z6T3;p_@u2Q08|Sjlkq*IKU`L zyhHjBvBv!wptf-c?VEQaQSBYEj^vigkQdc{RiNs^eNmsZjZpy)#oQICYSv{Ym(lzL z_XpEAs3IB24w9Ai-Bog%hNmR+Q1){ZNB|_L)Zs?3E6~Q>+)2BrcYTadj4%0ezyp!O zGZe0MwbU*(?!8W|4J=KDDcqTk&(3y0iK!r}JsG_DNt(cG;M)l|fSqXfysA#RjM#{z zje|zz3}!Es#;8<@r&;2G&`bSNq}K?@E(P4ap%tZXo=>p`jA8HMmBotQF2td$t0~F_ zB%;Vm*~6mJe)I%r`#=o>9i(l7IOx{^GZG?Kb4X(bccN7OVURxB)aH#Q*JY|Lv!86$bk! z?Ei7Q2;kK=21xvK@N-KyU;yw&yFfA&vu1$C37GnS-%A&)`d|3}k~PBnP;_5{+*<2@ zlb{#=$1@K9RQA>ikhuRif#Uc@4%j9F8qOaV_j~h?m$~@J_Gjq*ty)0eT4jJP{alHK zBN2c|z@_T7@?-lDB*YBl*uK}H^Y1*^&^pp^P>|jToZp@M4!pEu_y;6}K#zhYe+p z-o+VcFRmUmAH)O+CTt$B>}Yt%OZ(sD<`F1f{0yk8_BcIbbl@@qTOEgqhuPogpmx?b zxgLx{*5SMkO6qf+6FJVYNC-Vz@Y+&3ArkSL^V8IXk+E6^X87;*wy(98BspxWLkU11 zPWC&&8N8yH$g92VWzY{X{t-a^BQWVQAOzZyXya)hs^aKioy8v4Rc8x$0sJJ#yGh^d z^(R!kU`Xr^Pon0}NkkKFiH$n&y6OQ z0MMaqMS1Fn0)b z@Vg-MQ(Y-GY-LB{Vj>OdFDfBj2tXk$A^LO*0Cggz6{aUiZL6FmE485uhe_*5h>74l zb}ip8lOIq6s6x%<f%ET(J){<6~eZcgQr=1Dv|GSHde?sID{xdB3xbiBLr7ck@AV^kA@mg+|&kRfj zW`@zbWm;lVkuQfp>Lx|LWDASBD4_~4FzPLhKvuzI;S8Hm?!q9Hty7DYXbW`AzxFdf z|GU5SYjC^x4+AjzP8a|Q06O5eN&wIRr4b~_K&#v5MQ?R+Yy0fiMv&%z76(9%LM8zS z1SA1y&F=LR-+#5O)B#U_QoNGGhHhy+@1t%KjX%aas9^y}jy6yBe2Ug^GY;oS?zjvajW!l~{f9N-O1VzuJa z-2}4-9?%OrD4z#q8s`QA;D1{(&<)+k8_HYDn}84+R%jOh`&HAeG#oB(YUg0kYzEV( zME&S!!AI~ER@pOG1D&W#_pHt>6cnyv=4QQ=_BqU8pl28nZ9Hgt7?6gPfIHxzR}Hi!VNd z@m}09BM5@=LyQVME|vm!l?r8!$*Pbj2yG=50oL{yckhJ}IGObbCg;p|A;fBiaDHE; zY?Igpw-;fLPeUAFb~x!9pi4zf#7q@9txy-!Gz$)|3oM+5Jz2rug&x6M?Azo_E#~7= zPY&B57KWhx;J<6Px6C^k&4C59geps0iA^oW)cReCVg6!_5Sh0y!>A3DG7LF~3p&5h zdxNsKn;AOMf11u2`Y~&ImjAvsn11E!Dyyo zr2(ojQLhd14elxu2}<-LeKmg~F}4t#!h$B8(_j*vduxCuRO=i3a&>_Lvy{@~-ciOd zY+chdC1<+WDdfA$WBAM)+FzdFx;#}4Y*F#fv%mXWzx*S17uj6iwP(R3fbYy|si6UG z>jOwXWE%l6J%Dv_3p_wdz*g8>`r_II+CKgXf0>8@m^GdHcXQ&aS2wa9qU|VRVFBso zCYM5=0)Wsr+<)Hr(9#0qGQt0cAN>cRui&>{zxn2e2!Rm)ys!HyIsC^*rs5YbKxu&Q zqxxSU0nx9ng7=pyH}C*Bb?n%wcQ0JHaIBk?oZy{kACcrQM1XF#f_4nw!=O4)Z|_AR z2g-~zJxwZ-py9_;1>&XgX=g1 zL+nuaGs~@mS;P4#g=2S?$kJI{po6zFH13$}7iIs&-3aTa?Go?35p<(Z$B>Axp9U8m zPh;(QgMFP0yc&N9R4qqaU(fFg8cjRAKAg~Mi1Xz8uaZO>eU2cgG{3ZmLvDPWB!Lok zMkpX$vWr~=fs8Y&J&~(W5BLXVB|Vaq4X%z@SotVr__5$Z>n;*^(zJKjb-1gbzqDL) z^{q#uAc!C*zd-oHFnqF0SePPGu8K&$n85_~NgtyPI$5V`d^Y{6!Ep_qSAmv-oXGNl z7CX#+hUJ7O(wQL#D+Q}}89FnjW-W!hVL7DD*2=|>Oa0eQBVLM< ziE>e~kU_b>q)T?(n15CadMlY)oSk4rmsmA69u;;EjayVx(fx+#he}ad4RmP4XTu#L z>f+v%N&ykfdOBJmnNp2IUI38gssE3>aZVk$f$93i)HPK+@0uf8=RE^J3;eU_ z|D5S6?~Sq#_ZIyBAOG z@hn-eq*eNgyH zDSoE5eZ*JOp~(-Dpud)9rMMb25;eM_gf%A5n#O@9wgE(>B$K$s&~jA z82JT;$ao1$_koU~s+{YvXv`;0RPjy|@1A-C1a#u^;{4>zr+@ypeuh1YXMX&)_7C6o{*w&Ac99@L-S>Z`!z>vw0fWoVN=bMjO}_8QzMV%du+Dcv{H^ka<4qe) zN2FU$1(1V7;l~zIc#ZN(!2o% zU`NCrUMe_!3%-miD7V){np6$sC*Ub&%gkxHGdf;VBqYUH$+obL%t75ZpWC-IM0SZ4~+~Hix&LW!rGb(?M7378Az~*yMzYGp)zZ zH2s4&k(#kFQ3OGH@PP1V7_iRcCc(nkZNi^zG13~UQdD0+ss0}>84ZE%ba~Yvt3wA4-}}CR(DY2vt;GD$;l;zJDCwQ z${ttqLt7oW(}Q6^gq;JNhCX;J61MjyV=nx7(7}*saE?DNF4(f*fb(we@U+Y$JbcnH zUf9Hq_#w}re~z}1i|^C8`A9KSGnUCYv^e8NG>3vhzGOl0#FPLRy#3}c0om_8FSl*l z3t6!-4FrC4(=*X`r^sYMZnpAJ06wE^i9v}#$e6s#1#XGn4;&1i5kJ!+690A@Zqf6M zcd*8JZ2BBM%E&T5u(wm(Jn^Wal_P-0y}eL^M>v`33>pL^GwFjv0u@z;a;bW@BKKe4 zL+*dXS6vmKicLZoWYpU z=z;yVmjRUq9?D{b(;vriIt1+l%1Rd>5jhFTa_7j2&p*0G+~qBuPIdoQTw20E@cu*r zx~2o&gg(J)$pa|XO}s0_34Xn6eL$hCsiC=_ZW}Rhiy;ICsNmO&xu4(!N(uuf@;tyG zVE(Ym`~$}h!cM&K@y?xxj+|WL_O=?ICL+1UyREC34qKE+f? zYn3S)C#pv9>;|7{md_WP*}&(;Rj;$)qZo_wh~>02f>XdT-oCzk0FjEWwGCDbVw@g^ z;iJsAja3s<8(OoiQGZowOEHl(TIh;=OBni*hR2nRox@s38}t8Om% z9dZ*Y&^L=CgZz^%5akdmIB^e4NyC%kAM6Yp3%1I!<>8Eyc?=syE_eMGh~c1&@x|BQ zeZ3`h-WzyV1BlGnElZ{ij=pw zSh4GiX5heMC_Ye>hgSq|XHWwKRO}+#Bw6hVJD2ILCwvonK0jnD4#&_zXH6`**^w}yt%SZw4AGlYIDr24~tRQ%sc>G-uu%WaCykRj-K z%pnj7#orTJ#OX$+@kMXN$noyAR7wnR4rx|Sbi`2fgZlF%GC(!d=uFx5mtWpb;udEp zN~aGhWC%mIm8?mcFoti;Fh}a;fm$wna-_WEFk43KdN6Y!2ap69UU2^ND^1M~F{oH> z#L4yg%YX6nzx8X+KKty`Kl-Jg|Ak-p6oxd^&nR%^ z(8rwpFn*x@;Zz18paf7B?+*k4_E8+{7=(yNbzVwmkExH2w`LA~$^aaG4{Ic&R%&IW zi(Ox!nYLK{=t3diw~)56>>g*@%2sGpw6eK^c0cN3gYl&nJ>Dqqm^{~`4`9S~D?J%T zfl{NdXG010@78y7qdrd9V3+cQt(J3pJVLC2&8a!J8ADqU1fo7l6tU|!3+X`(ZRDXB z$E5md#G7rYfHvM++Dz|SO0o|?9@0cuemeowN9KHmvE0_zq#XvoTqvTiE{MXbeI$B_ z)J&3o06lp|Z!wOF0S-onj^g3hYwCbZM$Ogp65qoZCm*nL(3rV4pjwcplFC%25#Z$T zXD_xNZT`D=eQBUbJ}lt8vc(xi%TTjPxp;UIY^(WMzJ<9??8}Puu9A(Gg0Tlc+qhFn z4xK#6@$dnV4Z|b@>_6*l+{;7(7@W;Hs1Q1}UGsSQ6$%1()>sHoHDIa3 ztX{&>!daNHKH^5Q0z(g%!*|zw+oLdrDC@3k7mA{NMs55lrG#gE?~i6mvZ- zo)P$LjIQ7$iSA-GZc`vG^g{K6u{-z15kI5^0RNDPfFVSw%p$c1mEYX}q`k6fYu3Y4 zRX%w@gR8^z{Z*}JPBolrI#G41wR-VfEs1ELPqwG{44=IIYrprqzxI^k|95}>7bpO} z`R4sidVqUM09@ai8sOX87==991mwxI{@l=Q)_?VJvm9i`0eB9{Ph2g4;J<^k0LMQ2 zR1AO90_079Zo<+M^Obp@#Q-uEA699gu7FGRz?1MN^!3(22PQ}GM)%S;(rs%5D6Bv| zfbIlG$ogh#7Ve*$M-GJ9;%mYe`caIrR{{PXIkFQbz)zlg;lQEehuNowbOmWis>LJ# zU^^Rf8s$|Y^TGWnp$ZVcr=c9V9Ih2XEA3sqYo)1PH_Y{gB8wbx2YWAo%=R{X$1?F8 zTU7-V`CV-&UAkQ4Y96E6fI9zVuMYn}Kx1iWbHlU&1y1>eFSg);&w#r^Bgld4^hP64J8UD#%vv`Ig~WX$GQn1e^~yOkAX zm7k$giBSP%^CwU4Wva@*k=^@UyIw{`_^thW_ZD-mirJ&M^;d(*@kc?96VC#O!Q_bZ z$RTBV%m!=%ScMD#dd^}7(7WuqLr23@CK1_yzQ|4@aZg$yY3%Nt-dtcqBf%K2qC=}V z1}m7LyGD%zQt;xuG~gxXL90;4g3!?)cwEv>wod|)6YKr^zZ)sj^F=<-=GNi<|+l@5C8S;`bEND z0Dx%${{F^4nvVZT>1o&6N1K5r+o3XF|1C)K@-@@zB>(k** zaEh9H)4G%ZWaF#ZpJn!AF4lmo(6=gnb6Fez7TK5YLr5F3P?i3Ck_7PSE!{^cMxWI; z)S&TP(;M&HeDlx${Iyqjxk;%PM{d2e{P>Y$Z1e5>$x9Fe|A(rxfo=Lu?|3N327>{! z5VmG#*jAd^%yNR9)$z>yl1r!^(ng#KNvb4^6NlM}@+zfi7po9-5J=5icx|DRO1O4f zHU_1zt=FPaRMF@vwhl2(=}l}qw3}{QHFfRn_Ezol{r#QW?UxXe5Rwr5Jiq7pKHum2 zJvYPsTy+5PFD(N-Tfr>K6-yEL)V32`)hf~G0Xel@pgAJc0}S&B0VIY@?kg=8AY7Gf zJUWxGLkCk(%md@i`FTL1dB0$6ya(9UCHzqHx#*)F9tE;pCj4XE>Lnn*B{}Nw|BHX8 zyCogxnui@cG^EfaS`#$>xWE7rWX8Mibu&v4(*sq06vj$L%FF9Knqh&}TnyLg>ILJ* zN@CbJXrQ0b9%kKu!dK=3Z0rn0p%Jq4@(?zXFOGHzvYOp)vwULilEL7;J25-OOcBOi zKVF&6blA_>5xTJ)?&4|G0wD7ccv)(KL1O9Nr$;qa(8rCSVn7v^} zaKP*ze)ofs2&;)=qS`qCd7&0~4RF=XauIMqKmx|D7}D2r^->v?A^gpq7Zeo$7@<9f zTM{&P$S)Be+wyD}_yHdzUUcJ3WW*Pz!V)}n-)^w82iPh$kHwx)^tNwy6eaRtUI8b90%J`RsLxAZz z%!#WtWuL8|WyHn|qM?(S=&nl?PvR`J`E=rJG0VB6&Mp%+nRV@z=?v2Wpg(BBV15v? zJWcU=67tee5dxPR)?YVVu{bV4Oi1}Ij}iCRINB1>42ZX&mQZ<3z(~2EN}oIc+kj^y z?1=Vw7$a4>UG||(hM*WKoNihfDDMb0hEKU{39}&MAxg|eV>9}EB#F3hcAK! z%F2V-{+R@z3SGw(>3aD`#46D%cQ7RY9jgvdJS*C@3SfDH-LUWhURw9REP{E{fC$#n zm;Uan|MpLR@kd1e>C@l&&SgflIWL;)!3jWr)cz1FewtyRWNE4a^F2&#F$oJO z#nKF889)_i01|DWFRKR9Z5Zq&t9D!?R+yoSBs+pQYmw)IKh>ejx89X@ApJ23aHrXp z&F%>!>kzZEeIZI-j-|Mgusq-_b}|>|pG*KpK)AnV0zeMqi5T*Jbp3G(^0SPPWCxB{)7wKiN1R*QnP7$`&_otCF zeR%!)G~FntCE*ENY^!OU&h_Di$@S0XUB*|GAvm;zfFF88U)}7?Yy<9PT-&lGq`X2Y zhxtD7?*uxf&WTbh^RHD|Ksg;G`6y8l-wx0L?Z)w4$-g_t9olvYl|f4TwkS474EF;N zj$%~p!6y2<0PFerk=@%Xw=$TktnArkO#Zp49fTedNK z8IoZj)?!QK_k*)I<59G?srPG0uGjah*|Bx|R?a72@M=&PSxT#bf#zHn^>%MHq0eIi zi0y5S*rn{5bQJElUjE||jD00!2-4vp_Q3{&uB1l)wB}trn)V+)wR7(q`!DRAU~jam z!ISqD=9fqrQUhYzf;8~}?LZKij|xoO>);^L6}?E=USq9WCbzVb%x{h*t5BcWsf`uG zN1fo|${Yay27F@3CI~^1U(r*?A`^ZVKeOT8n?L|*05(1Ujf$t%ZP~EyrFA=wDgl@l zKwZH9Km`6Vf&L@bfLH~3WMBY26aCgKfXFxDFDQWAV50vTARrLnVb)F0{ntl|L1BGB z0EVfrQD0f_znlTA*4%>YRQ~@df%=_46&v`5b%*Qlhd)tUzajv}pU@;&V!;+rsNjb( zsO&)LU?l*Y0cjIH!c3T zx$wDP{>6$(Y~)jf{(%`-{hG^&n#Bk}qX2Dx9jLCtS#{9iEG=@Edg=FvfP*4|By!Z$qq)Cg$ zMM@gHgzOrwYgvxTDI6BMBIOsBkm~AFp*jG5sM3%ErP7PmMl)0+(h4MC6!rRx^EJCf zvxN#6S2>MzrVWch|tDFowqnrUMwz*L~Im4cUyKQcuEz*h{n;OK49Rfuu ziAhL-1Yo0`ui$RU3=g$2E#>ZJx`!12Hb(Lvh1jbEfGH4EDyjsp9T3_< z4uI_U>f3YxRf0Yf8Te8|Q&6eBQ|G;66NN%^RaJc01R7JPAfxSLn6&@kzS@0nzPb+< zRj9oXordJm$k-AMG!UN!lnR0!5~u56=TG$+f-&4uh9z)CuEPA!(L=ch5oLume8CJ7 z^(d`o(ZJ5|qyT|b3(h<*0$1cE4#g~n-N_?2Dxm;w-n41cwsjk~Y}v43%epN)UOW2m z$yd+-qy=EsfM5dOLk55Zpcpp^K(DMc0xNX^hg^U{;13`DwJ1PIi)%B@=$rmm#B=&wKihPIJzTa19T)dr~z0%mQ8k5UA~fAWs5-2KdY`|3*dL z7D{$>bq` zdoV4PF4zDWkBwx%-|OdQ_t;Z_5DX0o z(uL71&UWiaX>Zj6Q^f}9-s=n(6K-q z6*sgPnqV{r!YDAFt)D$0523x(<(PwUKOQ`I>KulGU$xi-ljr zfJ}X%0`Fu#aIJi<(BLcjIcEOU(O^rv>PQ826Mw~0%7xmQw+@Ysfm-AuXp1rW1x3b3 zf({}pu7HcA#ZzDVr>8eP|GiC{x&JFF)@@n8VdG2dn7dwIA^{i}@PG8A5&#H5_rKW) z&@p%a%07U3g}~=EOB)b5a0qo*WTt-#`0o!cYs@eHjW7U!J6dqF<%@4GuX6}=>t@#m z01E+AZ z{^fIK_ z)D1#2*ldF>v;>ZOVRm~d?g|CAgYrH_~KH~c6Tp~ zi?cBR~bV+Hi=;aFKLd$>~ zT!*yEENXgjGR@rDj+$dPl-wYycR*Tq+_rH)?4Y3T;4n@=SoLsO+-deh{!1E)oz{SE z10V4Os8Q7yIx-Ao1r!esUyLxHA3hQeQMVfDZdCP>_>73!IA7E*^7hrb6{)c=BA)G^ zl2$N(8BD!cK<}Ch9S(0jGeNvo9Ppz&(01iez{daondI0tyB>GCDt?3q53&S4-Jr$ zwLEx=dS8Sxs&v+T-U7-Tjsn99h!uSwjWX8qkPCDh**}H#T~^L$Rd=$5K-|l!0ww!v zhAPaUkaMlBZ$f216Rd;#_OY4VlxywRN`MpF!rYl}YAurNC@E&h)xS?VFYUy8j;Qx^S%U1pW9TT)alK?nY zX#eymw3R9Jn|a&(ncxGCsrCtfeIj1viUIf0;2-8BD6+Qy<;p55UIbz##DRxM!dr zYsrSICJ-&J?{h*2cBEkr&O;2&wdJ7E^MKD}1S;Vkj8g>Sftf7$0+3b7>u(<(PB+i_{hA$7khAH=Xd^wqe*?iFa`x}RHLhX1v79>s}(J&X#?v0jh z_(UlPJcqt}egu~R?r_jh=y^ONpDbidbqY)y#J&LhkC6_*;yeudZ@|$bY(XQZ;`G#Q zJcimL#CQaq!>FL`OM74fsV``gWkJmi3_$_hPK&ge~Zp!H(0om>BBDbIT?)mebA>_TMoS3WO)Y~QgKl{LFgadcT zwb39bQko0N<9C{*%$%U7-=`lx|8%Z?4%>t50<#Uc6QEuZJJH~s9y*EgBNJZEB8YHS zDoXS_7RrayG^M_@rgmPb?F*3)=p9~0pPqd{8h|U1geM>ib2M*1tjqu!(dRn~cU#_u zq+yL)#DNAO>Fg6c3lp(n_H0A_cR#)Sug|Yqvw7RLZ~S}(@c&ak_tF-rrSu$?XEPOm z#RdWf(2INj03*nf1A1hxeO{KCfI50ZiGg1d1I6E(_4v2H`>CwHOpnh9P=`@JEjhWs z#5>_=B)?24uRY-JoaTzwu^M&V?$rOv0wM{j_UH5Qwn+{A=^wA;0mXXx=shF_>xKS+ z!%O|HlES0_(|5oI@Iv|^-5z?2T^|U4TDwB@6KcSV&pcBR?yRQ(n5cy}F_+R04ncTg zfDh9GNKiZW(HdsWH6TTgA(Bi3rf0bEAP!2)KSQ@wTF%gABXR-N*HKp5g4#mU?{v~s z7v{ozAtc7Bm-n6qRP+~y@nZezRlN%$$zM)(@s31V09QjZj zVkDlLBA0Kv3(lWc?mQnneRd;2+0nXsPX2eE_eh#$cMk10C0tzD9$?+4L z@r)FR!7f1fMTddz>uS$Trt6hOX5KzKcL*A zd?ZVjs?*qw@v&mk)#QfbBx{1r->mim)IW`%iO3e%w&v1I9Q^HcDVhp3*z5SF4xKm= z?sU13-@?e`z>K^M=!GB^*VzZC(b&ulIgEGdP>rf5lNwPkt|??$JA@Mec?>xj*2uu; z_n!hYgfyYe@rG;>@ljL&qu_~f9&ipBG>XuW%VaByyo~JW@vPv3yHec)guaoQ5hevs zP_N)s#fR(z^+y{NS%ftDZ|e792y5{lJ%y&Qos5dP@XXt@vu~f9dF!p2vu`!L{^>j4 zdw%oQ&D+*K_0q0CYf}=>jMK)B)(?=hj~#16c{c zdgYN=K-vp{5O|GBW$)kpfXRFL{$~;=yY3qbL4f;@%*0RpUp<#AdxfZpQo^6}r^*Ff z=+-b#t3lSzpgvwrgDDAkT}L^gTok-p&INj%PeMK*o#-t*1`vHTKsdg=3$k@UgM9;OLW<4ZZT zA-v(H0GF8P=zUF7Q3-Z8vA>(6_(d{Cfz;#)!W!jy>(&o=%xDMHNOIFCmrHesYrW{3%sT2wXfRB7?qqRrmVtb)?AD|TQdK8eQ2%(-*53@KoT)V=;uybsegF)v_6@xc~c31KB(Z=-w*%*{Xnl+SqX)J8yB5^Ml?Am!f%6J)OB|e z_I&!8U;Je%fROq-o+7JAV zh`1X_kk)JdasPB_e0&*zfQ{aJ12{6MvP=FVPs>*kcZ+d?1TDDuaZL2&vOpoMDw(g*`7rhwcDneVrm+XvJEy(%C(fK82g`j40Q^PwGfJ51|~5u6VB(llUXcV?-OtF(!& z;ZB$4lfoXg6R5Q;q{{7mM{aG}v}(<&_3NH`>ZPZC{;7>yzF5Cw&DPCpHt*S8Gc7U@ ziU2x_f29DyfJg!h>62TqK!1x0pqYT)D-3`cprpVN%XwY#zfu6G0k|%}ddcTS(CTvzxD*W-{WIM$1@aEMhPDD>$pE|Ujz?~@<&XQY*~xTtmI zn|GUZ^wYZJqAz!mEO34b=DvT_CO?k-Pa_rd%rE~k7(l(2!zJ-jZRRyNB6$`>T9$xTfDyMg2&B@LS8B$Z0apP zd5CNn@r$o*+~3Zv?g}cd)s=F+<6&V>M~7&7S+|McUjbm|gnq;xv|C1iyL@DhEXFVfKp2Al*9cCctj25jD>{ z4Qc*N+r{(R9>!j)j}>Z9RI-W+dm@fsJmv}8Tb1VCKYo1kw&%a`{5H<^Gtiwu)EafS zUpEfiyNU4;UnaplOTpm@cys#Ev=TGGiG?`F2SM8Fuv657c4~e0z&U`vDg_X2of`cV z|4GO=w0q+~2*~-;RWMG}Dj-{g`c8m!IkfaFcR}w?VB29HL=bks@A-uX${5Js$n<^E z#C+b?t!$qP_IA{wI(1OC6v)bP?dzGlo>%2Yfo8?ty}aWtvQ3IPiUynRoqIFw)NbLjmR1|WFAGzxrnO%qi5t11xuD7jIo4J!e7GwmOIbmJmC zm$+o_g2#IIzxd2^&;91JzxK^xPm4e|;Cl@f?UG`#vizs z0R;TkP^ZsFqA23piQLu{xA7S3ZkYL3k!%dVi1}9^`z2Fh9B_=>Uk(sn%BAwJ*Hn`# zoOU0sQy7y%ZWiU?sH!s(0exzB>p$uj;W zR_`85F|`?T`XlyK)EqdaH6i~$v)E@I38NSF`1Yep%7t(|G zzvNGtO-Ry3Mk56hkJm`K+LRq7GE|RhK}uOJS`D2t<)wc(G`BLC} zZz%FHmXPE;*E^ULO@N*-*3+;3_*T9ACej6SkUX8RJclS-`ddkESd--#5F0@hSA9K5 z6pDp{8|gN9$)J)lPP^G8b7myapx3XG1i;3Z^u+zYz>|cKmh52$Pe)DMAy}i%J3iVUfTFGtUuN71hR;~9EGrcCq`BF z6Y(5{KTlQ>ARthmL+!sc@-M>$qyV1H5xt*0qJ*F~OfkVTZz}=lnYTDrF<><$N&_P! zytwGKx%n@>!QT8xU`isE)bE*_m%^^IC27gE5Y__itPrZU+vaGXQO~18wxmK_VpXe1hR+;PO{=>ZOuNXN&9!&eC` zkKHCaKWtDS9e|=(VM@B@He7M#ijyZVYYR{=H;_#z|0)kpcM($(#XspSL^0QCa2V`7=U%?usvpZMtW-cNdQoT1pq=2HeG<}18}Rumjc^))O`AI5h-4bu*?TA%5uTUAqkH` zH$?NVseOzoAF%BNSc3RKSF{|UcBZ%YN=Fq)0dkBdcj;T_N49>0X8-Fp!vFc=wH-TF zK?&Nv2dxPDn1>PuVMSEB01rmL{%;b%z5n^~kA6%KU~GUC{rXkT35E#3kr>dmiP;~( z{;_~I4mH1;e&+H7zsUfmaTn-kIsk!vy80FJiht{c=>*J25aFf;Fxx*I{bl(4`b|f# zh&!OHU`hiM=Vj7B88I~DKfR*wsGI=kNFs6DLMnOa(~*(f2xV<1d0{_}KTkgm2KbxL zRe+NWrT_upT{mDdnbh(*>HN-v$+jCq1vY%B^rm5K>Vd2KpmkU4h;*gJRGkzEGn!>w zX59WJeUoV;rwK$xe2Fx_$avvUY`23>MH}{^XDyopGx6Q{5+w;)v*JEnhq62-&O4tzGtGn$bH`m&4}7j}*eMJFLuc|{xaMYR9`-zt!6Jzlwr_^<4_B}@KB;!&ob zglgq#Zn^dA*T7a_MgW1Rji{+9Ly%6U1p4=4#RBHamWJd=Wc7j9K3^5|gT49A&L)yE zS|R9t?ctD9w)X65L@01Kr4?Kv< zYN6Lx!oXcW@WCq(c5nanH8aiAEblJAgYzpJs00w@2j3ZX)(-D|dH;c?skm=MUo#RJ zX}g#k=0*?~Qbq|LZUGhktQ2T_9nD$0oh^qtY8l~KkT;ncM*dDUb?#lHWR&4+P z*r+AI*94<(rgrMNMEG;=F98EsRRGCRtRT*hNB}>g_+RP!^&7u<4^IaD`xA`-Wky?X`-#e1N&vS6{`IURfR)4B$^aa?0FLDuy3z^!Gy&)n^R}7q zbIQ;l?-6*Y*G#LS&!Z3d8Cn8aU~1fr5X0>23~&l4WK>^x^Vw&A^Vz4L`Q~pv`%DEG zVD$h)-)cI)dfBY^*&(R7smZFk&8*Owc^l@0nI2s7PWT8p&O}(Ww9E ztFL#4xRWGHGeI^FMXy8t1M@3oLJh_zj`c5@nbn7hj6x0NEm3%;vCZd&x1Z0W5lj~f zd@)G>#~qw60QUO;$8pGjnFmLN28bhW5SSd;q^9se;vbJ55DP*(-o$rZr^r`FE0_VT zz9uiV59PbIb$u-0Nsd~zaFh;NvS8uYI~{)W$gKA?Va7uOnW;rdlQG(WyB&pdHuiA? zH-utL2I=hq>bX+y^En<)VseHu7>x~UhRhBl{rqtww{%tycxIbWp%FkIO-FX^Ilh@G zfJU68&LI}?_Pn=!>y9<+*X&rsBb%V@*$s!l+u|Q#3W91dIH=7f*(Qkskk0Tx5JNB! zpOO(l@hAk|CKpl*soIb(^;%TrZ!4pAi&K)JiTP?Yd`!U)ZqOW_9HvZfk-BhA3xf=3gg~= zQ0iZVtWI_~;EW70PRmVY@PP9>;Pd)bZzM&WfUbe9T-6Mefv&EJ33^G6F}R+ns&kE< za89wl$>q-vU>f>C&o(N6wHw#1TeoG)`n4N2Y#>*z-o8uWuMCd%G<1Mv)&jm33Lt+# zWdK56M!-MA|FxKb;D1EFEP<=%J`mf-`1-)sAU+js1`$+j@~VkX9Rz=5v0aw-5#^7lCY}}D3V(h#Xg{x%gImmEy;DU(d2+y4EKeKEFkof( zhy-8*d)}#2y0pc!>hX3R^u86`h{O z%C!w7n`xMAM5G8g7X{Y!xe(5+s@6ZEuQLE-1fc*UFfY4K3yFj$7UU!a#B=3~>j&Op zSJBU(F*%IOZVXrsv5j`rM=gaf_ygd`AmChXj|pE)W*lj;ni}B{hz=1j3e>1?68Lr( zQQXw-hXYJG4=3i_tLKnY7)Z58T6gtx2!1}lJofSPAcTG7l9G1c@_|rOAP@+5UIlBf zq59_RYU8F&c%bFPJP`Xp(Yc=^xb-2JK~Y~|(~&p!ytaPFYefFXT>JbTHUg0P@B+5C z@XukynQ)6-jD&fRNvEA;-NaZXVh!a+4!zb zFhh{!uitjlEpzdp8#)mQlbjUC2eb#y0l9)|E-Jk~84sjUe8|!y_#J4?U?JX3bBWRP zg=;^&#`p;~I^%OJDviXPC_WePseI+Hc>b#wpZ@l@zV*WX3k@0X)DgId)F>e%WSkO7 zLz+u$sA?NY`+q3hvct|pWr7|iHCTUW4AB@8nIikTabO1bZ9Gl_*s^XN_0d|<03-`W zp4Fu*`_y57y;?{aIa1pz4Tzw`a${QeGaoA~FM4g8uUtdaZtr3Afu4Sb-QZ`wi5@_oy{=lS zE>-rV&?hxk-8aYc8dYiFVxd0s_a7S@T=>3eKim3fX;DksM7u3a>3+_qd9TA^f`hbL z6AcFk9(8maOQs9H$rdIA-LwQqyTB68VjfwH=0q+mWN5`lwG7K`)ehtj1jVu!k~?UU zpd=l$QE^I7o&h{qmE?G;NKhfR=L7V{tbKt-O>qNLjLwP1fL6Jk&(3rzQY2i{qUvD6 zbz4&gN_}47Fw?;qsB6j-_EEkG!m?k@Jj*hWoN~hs5mU(kqVcv<+fE3X>J%36^wup}psX^wnHqBbPYsH_}*aIbK(<&z1 zTZw$2!p1f;bR^H(#urA(#^cmtcr2ol%tR&J(PyH z0Re00TJ8>Af+?i)1?0E)6hq6M+ygSOVep4X;n_a!eLP&rUczK*lx@WjTF!chD$xQ( z1#Imq62Md6*sz-LXPChunpfB#=XauUWv#(!az&S2(|9 z0(d{l1{4@9`z!p*{l9ntdQ$vLZOmi>y&wVc!l3`l3mv`t55Dl?L4NjoS*7?ZY=NJC z@!4mdefrr7_}(A@y$ua>{7xH632>~r#RvU#9K71IU3&0WxUw`n(CaRc80L*#?mvP#7+_VsyHc<$oExAQLZ4 z$w^jwd;t5%#!0@PB3^3R-ISY*d+@$Fh3yqprlBcZg^S8Z%T(#D*L%l&=`>Z zymWy}5Ljdl7hcyh&>jkvtp1t2W?R0_ zYjrm4kZJ;bj^xT@%VwP#=G!I%C~V(3|Gy74i;;mm(nEFhBw0M{yH;lj9TnUwPq$7hil9dC*q?S`Pjd zOyFmqedW!kE0PU;49p0LLvsx?GmOlenc92XZuJ!B<+*9;F%Q9MeaxeCDnK(vRoX%` z=mGv?oPSS`jN1c)o z-AM1)(#)(N9ufZHcvrJN*Xhx6e8_HyvrR|ZolY-sGOn)JbihPry*Q&?I1kqhktPO; z_$O5q4SwvP9GHD0Bd%@o4srXmZjv&y2c>~zs43%}*MMqCtQam%z_yIpwCA?8C6a@L zElnte0TaW|#Xo!iKgs66rmxZGa!akt4e=KQkp{sZ7|zu|0mMfz?LxvsdRZ!4bz*D- zGY+6kU^i!^=42ZvIrwRQ5^XfH2tpu{noDdL3S{Hnr<|G8MVbYsC*s7xOD6e_WKVGY z;~hJ=@K8(BWxY0Ym~?RH`oMric1lA(eKilC^ihxc?XQR6eZ_Dg}fp6iq0zwy$hDn?7bdYw86z4zMkMizqBVCuP8A3D??fYDFQ1IGQaf|?%qsICP*IE}3O4I=iBfu&E$N)O9 zfP@Cp29O321hf*swYR_fm&CaWfHOq9g#ntommn=)#=`7~XwGFtKfenFG=~JB7gjHD zYXt`Uj-d!yeE`ltto#}zaK%OF@^{`fDZoU&wM%4i1P$B}KvWv8W6Oam& z0B&A;`Pmn~eefX2Keg5~2m|eZ5u>0}-)0QRb}7h8FvJTe09C;l^#8{9dUlV{MS~5| zFtQGB=DEd^qjL^vYOWM=L3~nJfM$Btczij~ZKhK<_BTZBF?6-EUW(4?cwjW)#++5= zoO$SYL!~^VF#YEc6@hpb^Y9l_CO0Ob*fc^Vw}M%uL2f^*h1iQAo5@<9iQjBnlBZ! z>RTdM0$iIM1uypw-%RMl5oBc;)-xlLXbkp1*zw`fd6X%8)Du(vlVsA!?8})0)T4ub zyyTlCEfKP6x9eQpsgSF))76r8sA>T*flC)5^8sdPJL&N8~i1SV&R%g zrGW(jb5vd6&9MYO0^c+$DDS=Y#TQ?2=@a*x)~;Q@Vg0tvtG6;5<^L$2CwbA_OCWH5 zXu1*e(voj@ijr8CnOYnoJR~(rjX;(QqfRzQO$938$Z5HnjFct%flypy5(lEHVdXg4 zQH59myT8&En!+mC0H2f)P?+SX^kn(#<@u$_JUcz(q{L(jkTFTcF5Qj;M7`244F3Ix z@9t#t3@)EZ0E9hMKQ1FoGztATUOvdgf1g%+>gU3=WT*o`(W+@ZZ?E2Y>cdlSVlMV3 zmQ*`gE!zL)P8{p88vySI#j3q_g|Xl4%-pAqy8%35fM`_eWe}C&@dXUxK<1VI%F{}k zJgr@~cKvI}o6>?ErP?C?1prv*5=yI&qvaAn2|!1KKVuWL0s(3b0K0HFfK}h+ZoX`t zlVu(N!D>Yu$GqUrzN2H^|0{*S3K2kuyWcu6fjA}skR`+-qW-rMz#oYOe3{F>%>TC* zekcj_wxt80y&v-tf1r;gdteQs@nBhkgam*v*m%cbFcypeBnJjOF5q<$oX^TeesuwpWp9wUQ+X0Jk|X{P`(I-CX#5Zb-Xu+?(lzV?ldT zeE>D*B=~=VEf1hJvjyyBgqXVsYF^44ciq_dP1{YB!%DbtKn$>@qcu>|rhQ!EKyjE@ zp`~$xk;4+Z;zoc{cojyR4DW}04waW&h(t{xLZX+>UMeNe8)(ds&_fj31;&+Fh3Se9 znqETO7HUx5VS+#CVKqSwKl>|DjW4l^3vfsxIim6sVgF&FiZh2qnUmtWyoU}+so}pi1!beq%28NewHX(Dv z1anfv!5FHl`5-PS-Zf9Wd$uF`3v{(zdjPP2H?3K-8lcc8 zw@jpl8Ogu|RE58b2Qw%GH{5P%fe`v9+4N-}%$wHqFX%SZIqfd7=~ovZ5l-^%7B`iO zMi|V05C?Qq!05+DdWGhUR4Jrr>W9`d(c;lzkf$2JJO#W=bxu(oO~J zLTUYj%v0p=JA6>UEjRyOl>xGrR}NrJ;LTV6dFCx#4LxIyymyglv(9vJ1W|Tph|fXz z<3dO1vm}V72!}*M3!qh&E7b#J^SP?p+WPj3Kq!$YEDWe92spJ!?hpYhcL))yBkz0< z?&O-yYj$kduy)m&j}4)T;h=@K@I?0R5h`%${u7piRRSmh=$ER16-7`MfH=2r8&%+$ zAL#y9@S6g_5b?zNQC+ULRT?Oe71BC__^nMHg9VyPpI*+q5GG(z0}L_1)Cr9HxW+%j zCD2HLsFzSTVNDd22CUfED^>wu`V2!iGR)(TlmIN?koTYsObO63@gF#Wy?`Fjx1Xi= zM;-L(7oI)1?^{rT!FjX`>+-niw3DfB?e9}b2^V7pi2O*^M@SZd?T4hq6PLC}krOEd zfP@`>N=|Nc*SIa9Nt-17Y#8`X7C?oglNlBU0An6t;o`83jz9Q%^Z~;`Paw`BOby^- zQ4fC{9p%fJDaw$4TI#}>&wtpk@dDytina^kK&oom|&i!b32>3GzMr z#&U@C+d(-Q;K2_A8s_wNP>lp3>*q#Tf#X~<_`UxoghkR6S%sj*q714}RrSp5p%YHp z7S?A`ieRl-a$g|Im&{)z=SAtwWgODPPCg)pnd_Nk)Cnak|ekzrF;p> zEg6Zz!XYd_rah+wB2^>YKjHzC6lg!v)33qh z%|3Y0>;@;)cNi26L8DK89x$ltups;3Vq*=7<*^ZbhxrAcXmZ{FY${erTjFNOF<1&p z{F2&=-2H0hDxuf+5wXDa$oEP`uz_;3bBu$CD&hs27;>!?AL{A8JiYaGL>{-<8xHK; z|0d16CS-kHOm#is0Nc*f-y#|ICvONDn+hCi#3e`f7G- z!6S$Oc?=b$<=5n}r?k_n$DsE5Ach$mld!jO_x7zHoADbGY3xHd__sFe;u>TaNE4yO zXZSnlTfipbq5VKUV3|d%#iTz<`-@le0G$2l#+QPr(i0JuGms(yb??W5SM) zW+!?YijU??>24~LAPPr-9Wm}DSHRaMey!Qp%fRpvd78u^uLGcU%Bz|YXWQ=gK3=^8 zHEuo^8aIFjr2-_TsAPWx1~RSJ;&3k4wtEkdmcRfogc@;z&@EEYf0BhCwvauD6&P~? zsPQHgyJVPAYh1r4s_Q%+o7_=kF#`6Y!KV>y3OAiU{{esp(Lo)tyM~^6*|tD+LWv@0 zc6BNbvSi3{yRrs3MVmxykqgDSBBfaV3ydT2%-KVoonv8m(FJZIcxjO#5G5)s&}G;{ zTq8xKhuVrcsM!KYKy@7;LLfU;RR8-4{ZntUSWH6U+?}YM;6+!1)CdZ6hHX>fYQ%+8 z_HdKr5~*d#W~8}fxUO!@PD9rD{`Y|ZFa(g);p$b})@(N&kvU2LyESxWhI(oHjxR1P zEs_D$0(^;6u)<$8fUyc?J3ytt?PHH_KdNW^uL~KamNMioD2`}j;%A-mm0a%W)&H9d zTA4vP;HUH-^Fm`leQcQ*sG|?UEbN~?dtV8_s`bnJ|9YuUrDOVn@;O~jJvx^a{H7Gp zmxP^sern2_xN`W77byR~_1rg~A^v~inP(3k{PvqKKJ&{6f&lnRtA5IvZh2r!7mEFt zR81}c=A+49oYxEPnh1jL5ob6d#w=0}3dtdy`%76zEGYeKjS3KuMAN<$ZS)25r4pTf zGYSiI(E0gk-@zTRda`ghz3)I*xE=YgOc-?l|5D!}f6wicFu>@;jG~ z*+81C4$}s-lCEIl&>K+Q$+e4lBpY)3_Mf7mesiyNn@Uozx3{4$OtqYp&yK>Ekj2@E z$gfeCEHWM7A>T#mtItQ}%<`p=Azi=pGfx9`DDizJ7@wvrv!ippjn_YfS`Z%0Wt*=HJB10+4%vXMTDLt z0VvwV0N`lor_dz&^*aHr!M}-RUe~&h#`QYzfH|>{WIdCKIJ6MFSRn(lWtL9fefb5Z04o2VeSswK^wYpFZ$ACp zFaL6dznMht2(UGB!<(JV_4hC!xr7%_Nf?<5G+-`mtg!L#w+CacP>jtU9<%`ox}pA0 z2i+3CPf#o5QFzY{;Tq}9=#EHqci$ciW(!M?rx64x6>A!gGCC^c#}hJQyR(4D$irp4 zI8h&w=o5lIn1fAHUuL*vfZ7(A2Gbg#@Db(vfIG;yv&2MxbO9?bW@uS&CZ3ihZ8E4q zgs?uj%p)%ZCG-l)pebC3HR@Lb1LJUL2uu^M>#bdprl4&8j1&@0e>`F&>f&gdErSf? zaU1`bc9Hc1mG_bc1DKCWNkh&${l=W5MS-h=9hx(3!?d)^d78HDeh}5CcIVQjuz{T- zUorNSricJD1r#un02q-(kF7oKn+9M6u=@ldz#bL%3Y91oLRJ_=Iupoe5L4l^6==;` zcL|Un@>}^{8Y!72p~7a?BHKdJPiLOTrZ%F=NEJyp9ZfHxA;dk7{pfPjev<T zBP+_MUOjd0)l-!G;0e7lAA^_SgpO*a_gYRV<_dXR5>gj3hlomQ;A`2Yf+ z1n~X8*IzT$!{r;6QJ}>Hv=sk%;+p3by@yUgAIg_nNdmlWMuR+Sr%-JQ-&os6AF=v5 zc;%HBUf%x-!r<6}z4*+xJ|r1D{l?SZ{DohxP~nvJj_f|Zxw17{q~l}vY-wJ^KDzy) z_$y3~(S0Z&>>sp`U2TeWFJP^S@(;5gQNjS`Cl`?e$Jk*N*&?>~`*6AqODnZ==Fb@GxuULbu5qz6i&NI9Og} z@XVnQ$m8ytmWOaG=87d#hWsy>-{{6cF4f&H=9|rLqx#UlgsCI{H2xVJ>0~xev8yo9 zzyNft2}TjL0i#Z-!avLE^Yu?&!>+GC;>`dkxfgDZFE3NXaO1#+f+RNrY{8$nQP^<~ z!;TyhEq`7EagwD%Upu^eU@((zEGShpIc9B2GZH!WYZ3qw3ak-eI!LmS3y1vg`Pt+3 z0r8#Tx+Z&*(m)uIvH&C?^t~F@Sm;HK#drorkXC}a8A;5WHwFj3>BUh85Z%iTR_s1Y1llZHoX&ZF~3e1MXvl_?hGW~u~$LvXVWAkTsj zi-kMn@P$|dcYl}ASnT}A&X3SO(mgWOgj^sEC!Q9gi$ahKB?gsgq)r$$wllAxZPX^A z%hdoxK)b&O0d-acFDL*rP$=Z@p*vr&fjkJRxf~kmJYnQWKxAl&&;aB!Yu1-xx0SUr z36`<3fw{F%A3mD#vAvG5R`{du0|8*00R3N~PPAPVDpxB8$JddGAMvi z2vrZDCypY&%mcpN`Mz1v(`Bm?;E&740P%Xk>%^gAawU>mFY7h_l%w994^X#H{FiY* zK3HMQdH3)Cp0~=#;bl<^j7a!TOcAhhQ~~JdubEPVqZFc}9H1m&?F5zCflL8EJ?~Fm zyW4fPe(!(0$ncLw00a;+z|+6^3%~Gd6@d&gQD=H8S3UpyJ5)uxK&GfWaq$E1MT?8| zdP@E2ldTa(Ncs51Tr|)uQF%`iV_m|(*^$oPl+VNhR{}2om}8Z)6z&UXgSs-43f*zy zS>yuLexMMFmg9z8UtE&EP?FsqMmw7La&tGp(~({bcHYg|cw}jb`@}Hrl^y_tA(PwT|Pu9z(hZP(HtoP zn$-ir0}>(>3*b`(!NT@|0YtF-4jfhu@U3Tm6PGX&!85X!=# zuo@6P0V)OX>%0ps*Z%j8a&%{fK3vMwkD83n_*h1fP(N7>ZsBVS6OIN1oudIf z3g_hOWghZ~jl(9E_4r_Q*I*S&7K;69d73uNv9sqZkt(zXKuca+R33}6Q_5qSJ6YE& zH}JSEeBP&#AQnd@RE8nC#Yj1*;%l#ex}RVCYiZ9(F{Gqex>ys*O%Y{8pE<1Co#}@j zVK_uh4ErI=J(yf#bBUT)JA+5>bBcl#^OWaEo5abewDUCm45u_jK=)sUp4tfnQV@Z~ zOVL%JjPGZdCj_7JK5|3*+z103jc#^g!l(u?$qy&oDoySz*v9ida5WG==sj}dsIMcg zj1!S$aT}gtxQ^0Aaq6!;ZQQUK^@5fNaNu0-?~%Q9Rwz>ie|s0t7Oc<)EIe{_%f|HMaD@T=X{{ckPg%JBdHj-p@0ev<%r zTOX$EKq-%5_aB?7o+bfSlneK>q<)3!uVerN3#yI>3H&{|1xO6c-2A+-5`a2@zyH_o zn@_?U<)bfZ+Jy4?3)&Nn0jC^pZq=@{O;~B}W#i(-Z~Z320Jy;9fC`DigD2^S0+QS) z{Xa@A{r7X@MHu)`*C6~d1WP>bSm=i5kuugh{O6>p{2x+`jW#m^ccfu)a5+;4@bI`f zT3VuIFYz$&q%%I+=SACXAk>ZldpI2*&N-X<>|(BfepD{@KSv=0N76E<29@itu{q!8{8vHSo1%&>WSgPLx!l2T|fZI2s;+%iXBDECh6lVx#9wvcGRIs`8 z$PqRqNk>}X1%EsM03#71=8@GPlob1W_w$nBKc-=Pa|4*aL!X|E&y(Ar zGf#qBqmQEb6%Bk8sEf(}G&^uAMcdfh!J~>56e3Y2F*Rdy;Pyhx!-r2MfeKl%MLP=f zJ=itP)c41_N0+}sbn6K>hb;i}pLsbuL|Mg#5vf|zBID-4dR z=9++p!2J05toE}d8p>tXygYDEu}UZj)ighR7T`+S$Srq1HD=M7lYYK8lE7Z2ZA!9>x#@KCKnCaSs}5ZXRkbB?F~DQ*?^= z`;FJ%(?pVJQFF`}1Lgw`5^=3vO*x?Y0G@_bi|Vx}Oey>P!Tfl*D0*qQ7WhPid?d<=J2yeC=U$ z;1Ep1Vudq+;*+cdgcHgw;MD$CYvnn|Zjao9xb-=E$y1ugimcteKsMb?udfH>k|Z7p z{*oj`x%xTd$&MuSWUOVsBo7>8wNs|{)qF~$r$mlS^2NylADsT%?`>PX6;Ti>fU?TL zcA!91Q%JLyv|K5DwRIcl03JVt_x*W(lzjl8z?H2a4n6_$4)=US-1B5cZgc<&eqxL# zqP2VoKsERb|H=_a0=S`FLE{kizA^Tpi}?HR=&C<$qPOe;qSF4|cL`oA0T|}bA7CQ> zdrAP(0#eZHr8&P%y#O~@A4@%95=OZN;DtG=0?HXc34jLy$m+FT%xh%J+`IQMtg3^5 z1vQ|eMj#*8VU1Z-@2AEx04|$ApgBgqN>bj@d2FTJxMLoJQ%}TD{hqT+fdDe|698~<{{bvgM2K$J>akRNs2@&(Vsg(4-)s*N zrjQ~4M_H1dipwM2ECH&}0$B}m8Jho!JKm+E{h}QT^nw@?mN`>S?DdxX8Jz2cYq=Um%#L&~#aT?+9EvHURBQZ6u!#EJ6SFB+<`UdG6D9fB;|stY1akvp&heM0Jx0 zL6`Wh0RWI#U%gfZz;zM;`vB~HC$`zMFYN@ zvfn}g%AkO7cr1E}{)>u#d3}o7L#SH-fhFk4d(3A4@05#t>k=o1VFW7ySZaZ4|5rwW zsuT?7SMK~d<*0SqI#EOR4K=KY~0UALjsv!%zK1?R0)qOdIqZG#Udb!1$YIxnzo9V8#RXqG}jq0 zmV&<<)ol-}IB=lI)?P264>~Sup_JqZWiUjyjO_l~=iU}lP6+_9IMCki6`C1xdm#HD zuFSmeEmQXE>bIl?8xHhx$yRAjZ*E!c-0Dyury-qq{h}hX9B8?RM5)a>>+3tXiviu| zn%a-F9|<7kNyMq>>mcuI+lMV7sSVqT*N+O6H?lfx6FyY_B@_k3f#e8yIH1vkIGG1P z^M&-(3Xfpq!@Eztiq9bHy)653B*`qgbchuX+PD!>MDHIp7`?W9+F+wC1O6!dsUmxO z2L}Ls5U%8Zi%!H|-zDlHpSKG^K<$#D)+&eO&J#C|18r^Jvl<;>bU-%23EHt{%~rMl zP=XB@CDEvc=_w+y#A!@)oN!Bp#Qp;go_F5?SU)!E8=ThF-3g}`bC3eSW!`?%*LGsChQN*fTu*eSeWF_!_k@tJ+xm4yr77S`DSK* zj~*%dwbd81IT3h4(wF66UF>Mm*^$l~YbW}j(BI{0^clFZZU&Bq3|J}eNK$%=S+c;b zoQ`r$p~zV$MHs1w>gguibIn-%b^~SKqf<3Kh$~aj?a#;%FwfuQpz!y4g|LJ8!9Y?U z8*>xq>85sXLEgSY#i2r)eq)?g0^ZX!@LqH(1Na3=uf<6z?mW^y79PWn!tZgrj2D2w z5E!&|)S8`;8~Hml?dN4>2ooA1eM+A378#?XlE*skG<`vi{NYV&*^lVg#ZMYdum<7q zQIVLM zxp`*<{o~)Q{+~|(2|%f%x;mx0DTO*|^zn5z1hoaHLa!JVXNd19lg3z;78-B>#g(cK zsthU+vK;?NI1_vY@g{`>Ahipf8YMJpgaX6J`6plLAJpt7olBdGxxLnRXMr9i*M&8Z zG&?V7skc^!{_OT0{1CB}ULFG!{piuzLolQOM%y&)JLfsHXDMRzHzo`GZ z2ki`qNHW|U{8Z=z<@U&{sJ7{H<3~W2spkI#4nT9vapF^ClvnN84hL|})-|k8Y~K1Y zJZWQWpjiz}3VML8tJi$78s~|J#i-c`lwF|g!VDYO{Gto+T)Zq+uj0S8^|TT|ne|_` z;8FaqF#Oc}8yc>jMaARb7yRizy-!Dv-KX%rcddb*ssPTr?@HEhCBCr>L=8xPBn&|P zfyVkeR+cCqUJ)drG*E^Pg(8~Q4Ki3(BBTPD^2a(bFjEdb@HQ${GhM-j+gJXBdC;3w z08z4SQ4%psZ*iiAY7dsh3uf97f{?{86cjc{dJtO-fry;I@bEBDVV-6g;9t_v1s}rC z$-c3vxCAL^4jCa7yyJI{5&rPB2)~juF3sF<2u*z>vq>e<$==QM5DwH&fZT2Z7RxI` zlYZ!QPZqPx42km8?b~<09Hk}l1WmFb>r^@9v9I@r>jau99|cyIJ7>=0JMsD ztg|ayr1+L}tpcf=q=A(;K~IORsu%}dv{~*A3~+TXIBG6Q*c+7tEl)^vP*STl8`7S= zcWY}U+DRTSH};Tg-R$k+6@?MpYlPdxWws?1Y9ZpUOJGG=$GLWgTl$Vhdq7n z8@_Rj0W^N(myXKMTHr_{?V*wFHHlD+U7d;osj_-v7A~mS3_gE!dNNyRJlWdF0Hm$7 zJkCIt1pqn*Zi6cvx(q&rw7?`0c}?nzlo<`Y?3KH+v6?`w?D(ejk^o|fl5K!WJ~1te zzQ!kdbcjjSQ+d2{^O_AiaCG3up#(5$DFj>PKfja$?%6JCT}Q#KLx`U-lG_$CZv~p+ z@@lcaY|p`y;pCA3F!Ln`=(nHX_xqDSdiS?~TN^!QPN17wu$~F-KjW7!c6kI?5`h8$ z{x1RW4wVZ3M+qeMt(PljWsbPggP0VAAcehaf?cegD#q|sgE)EZF6yg?L4_)cuvDf+ zjR3*MUy%!46Kn`&zoD5QXlR zU4Z#p6!XVT!nnr>JzgNkpttP|*CiGE*Ek(@E*5O3NjvC-d;P2f(l8hAqinBB8~EHm zj$JNSPA|9r%?Hbc#%W5loRgU#+sC~)Mu`AQ{0Buhf&(%!5F8W*)ssfBPnCZ^VXQfN zlo1P^P0Gi7W;~Z@|-ljsjg{8J9}o74K8; z(5~aK|F^AQL-P*=pu(TPhu^Jhf9x!+gXd?WfzjB5{B%Z z3up*0iQT1W-hqLYI)=x3lOY~@Hnx~vAp*V00eG!}XpPwLkE#4&=Fl=oJ`>|=nQ{(lEQef-m&(hu^`QV47x8di@h@DF4w%OsJrD+3KDM1; z>aw+?g)>dZGTe~}CD7FKtFCE8=r*MXAQ#O22uJiH062>G355#@K&p4=xW08AH&oS% zYm;<~f+eTbhYtojW+-^yrqriasHQ!rmW0O61GQCFg8OwhF8*X251SmKPXLyhAS7)0uq+VSFV+&kcf?HG5D$zBmfd9 zJC}GfVL`D~+?Ea7i2aY(Y}>lW;Qx36%4^fL7R-h-_SUFjJ}@l+ z&p%QUz%@8?`IeUYtO|fiz=!}q(GV+01%J8zza_EeTPy4!UEJLNl>VQY;4ce(sRGdX z(|0M?O(So~e*M{>l}W$6V4p_|fD#BT4++e$z)TWNqA;rCkdH+Ww!Fd1%p=MW9ZQL5 z1%((96{zFxU4CdYJW%Rror3N`?nA`yYAfdH|4F-Wz8M9;nU7Kj@WUwHZ!G%a_-~P{ zn?>?-LE`lKV5vtS<6}4{t1pe&t}ppo#`E;KrTMtGh1Jq={V9JwigOro!2kdW`rC*p z4DdZ%0ZR^rOYHWE%gtXZ0rrcnQ;0&+$!GBRc9U!d8@j4x*GXCLft2{LxR_0J$GGIV zmncUO2$R=cl#zTsMi+=36xge6ICANeJruVnc*6Z7@)=5?O5(1o z5{<=d>rw1-$N-eI6MWk;ZpZ%2zl^=wbvA_O$5W_|i7ERwc3v70bn$VfTrjLE>1I+m z)T&8vCmb*lnAF4a%y}mt*s}3F+Dc?P;$Ji%+;_FT-vvYuFh?R_=~g;^{lJACQqd?XbQo zIp^&eGT{uRmv;(tu00f8dn#9L-n?ns+VwyHOaUqX%?M!Y#~K5CVgaBvt&{-l39Lmc zWo@7N>v9I+Yr2s+y zACLjm|C0fD5(@Y`zpV^F0w4`2{&jwrU)1~B^0$V9{6^y-Q4lNSIeLdu0)N8$jT9g+ z%tjE^fuROkJ%F`OWX&0b0Z*&Zw;oZ_EGH8ChXAi(b#t3R>xmr5lP=^$ZClPr z0jMQ5c>B)nfky*>{nw8SQNMw})MP9VOE?eDLn;se$mrChk+DP0QsOdzPO|7t67B9cL_|dV&h1# z0V0I}0<^xYKqLT_kpsB7JSk?Grhib|-LPIHf90h-MZVA16iR`8^}%0vxe);xyXu_s zk+`Un3gCqW=;#m&nR#U5aruElf#?2e1XN_23PW!?E>uJ^DdMaNQ487v(@Z9uQ&YoE zK#_nXxY*laq#zbNV=~>JJiduMFq>W#JcN!N2?hSmX3+?Xq*APdW`FNFieF}Zb*Opt zQX}Xdb%U>XmH#M&2Ea4;IWjzL7DJNiIpAR zD{N8UJ-}N8w(zw9P|i?is4pCDN_$0wE>Jqe<7f=Hs-P9@zcA6!yH{}!&4}O5nGB>( z;tFUY8K6sq1W7ZxyA#cM?iH?m+}3$A%mc9=AfiD#*1i`w{8y7OKC@7Lz;+h2(iW=1&692k0$p z6+nz&3joA#+?Wjr3*hKI#J!deITj6pW8gq50a)*}REtNP_KqtD_U&X$3Tos^JV=15 z03%(gD9j)I3}<_Cu;iQPb!yaGWEUR;)|U#=an zd=MB-0^Ep4ATb)?#CAt}J5u?nW-0=1WQln*+Cs2n*vEWK&T?g0g2v(!Azu?kGN*1>}M*VpQ!q+HYLodir((^CLb(10fi0J=?5~LqKNbEoP zSIU3{#Lix)gyxN-*~eWNO2q6QkDQX3^AYGXKzB05RPD`EHEj*JeUjl=eB?eIgNVwl z{r3qIjQw`LsyRBGcl21n2AID!=*FHsi@#X?TwTb`sxm+3QX@_ux@0k=ID^??yFGc# z4Wygz@h>ZWaW^Pyx(cv|9Y|tfR8BuuO7L2^|KE&0aOM+>6_rDN!J|E zWPmcc-}3(ESQA0h7AVGlVmtyYuK=ck5DAIsb@g@?`Ua|LzWId#)n-u%8DO(3@&;m?%SSy5K1DSnA@PD8NQ)?jsxt z&&R_CE1(;oaYO+HF|ph`9BE`Om|tXPXY#8DH}Cxe5^A*k1A}HVhXc4j z){KsXKkbW(9GE9k6?8UkPx$7BV!#?-kbTl<4Zs>>yy;0db3K*^@u)w90q(`28p=eb z4pMwZP`o8WHYv=jaMZiF#xZg9QzAP4 z_+%rYu(cBEOHb>_Fw{c)$M~|0&S+;eP}5AMmtZCQ>96=^I-4ARS&c8E0kx#QP-Qtc zLfqy~dXRulct~7z4P7kWNTQEkpBfynKrr0i0}Lr=!TR-&p^6K}g6etlQFsRwFY-h% zyHaFP3OIPvgrP)d`9DQ)rL-*gT^Ky4nUrZb=R%J05jRFcXo|f|f1w1xi`hQ6n`><@ zJh}L=hB^XdqOI*@Yparq)Dm_b7Xy&kmp`~7pLu{Xz$dsT(g#jA?qWd5oKRvh*Abx6 zLkJXLQs0Oig1*$TjaVU7*7CEIy)Nqr@GI~83n1hrZ!0UX$48_>{QLBSt5<*U*$1Ef zlQMt=zASriD*=4RJeyM%|JNUymx^T*Fa+!R=!j3K1{x&a{tCzKfp z`XgJmWdaz8J zDaocI?ZSMA`d|rP{QD)=Nn+!$flJ(&lglw)dRLA^xPq!p zM5pi+9u<^;KKeO7d8z>v6hF}e)nIP|4RsyU^<&YY@0)=mT!h&!D)eACLqv~5=tGI< ze~*8KtV=~LU7}Ib==b3wN127g)vFGC)QUTv?tJ;>y#dG`SUreY2uw-*(*+(-NnjPWzGy3e_uyPLo_vQzAOaoSsSt17HUFzatqxl8JWr#0DA2lvm`6B{U#l#-}0}@V*&lg(Y3rO{AktMmr!ZCNc_!$2I z#xcbL-v^6ES=n1;(nxqm71MO?afJKD%)@PnO}XNdu0%65ObBGxo^f-2nh`*>l=p{& zWI96&g}IZe1GkHEllI3Gc1nzRZkhwD0B$q&NzCRg7Ih;vHtG}NYfh+m zjOwBhs64bqQmtYfM*^T+fk(&264$R57ckCZz3OO8o-HC0q{QR}Nw0v6$p8-E|EIW(W@? zlYJl>MP_>XnewyzytD}Jir+xGgwI)Ss>$YTy`;(7$)TxmP>X+zFeITsTaE-O1Od`W z7c%T8a6J~P!cT$gfHexDv;aAatd`BR2T+VlqA4UM4?0~u!{XyLlm$QtHHxQb*-PSL z(l0|LmU-Y#!>6o~b1{2QkcN*E{5;T>;CsWl$BILkyKFfhd=2~dM_=A-$^7^K`~$=H z+mA&1yf;9&L;DlHsJmZ+5n_$gX;i5KskZ?_Jt=tuz9~<~?eT1!nJhcgyV+?Y6P$!U zaPgyCtC#^C-@bYqNB|jt*vIh{JP@TdAOPH5w{BRw1+#!PAHPSnD}9nIX(;0R>cN)? zf&BO;wzobb+_7@3*yqW-R0K=Sk7q1?4X+Puj-&X$_~{3qefHT8e()KGoj%>`ApaZ& z27k}9&WZ|HS>#uozLi6`E5wO@9cvt@mj(|s$)Q{Z7$YFdAiRtOXdM z)OJ2Gcx__uzQcRrP^jf&lwMT2#9YdvB za@*j2i~KR^ft@@(o}_lmgAQjEWWK&s+w>)K`{l)`(7h)QOK|$(-)SAD1mZVT$O~}; zc%N^KK7s$oTdZ};S0EAP=4q3?x1_ShMU+DXgs-H9dI+}Tej#=r-74@N#g)&?C6cBuZxTkr?8N^XMa?+gkkz|p|ZRHiPld+Lpo5G<8;FK zci3d5X|D@MA4AWf_}6?;9lgqWI*-J7!qb=ZJ&C{hCj zIKO_`<8jv~TsUy|@o8Q44akp9LdwDZ&PZ;=`A;C6mZ2Drr}KpfDkMzohT^V1=S7@5 z&mUo)L(GF2LSyQn`KPdE5=8lN_R!U-9149h)zK7od6NIOMT3Z81_luV1JKU7dbC8( zz46Au7ry=NZy)^dl!5(_`xeKC@7=ia-^K1*d<@woG@~pnmov~l5N6%|l$nsd*|+=r zE295vj#ca5vCwvgZS-P4W>^Gb3`-Qm7{5T+D~uLiIZOh;KM+r@Yufs8XDCVKR)Y<( zI*G~Dwcvw{KtN3C@R!XE=g`6W)cNVitHlCREAe#MUvDR}7rB!KbwNlo|4zYNV!XGQndEc7b+%{wjJk1c#t{}cXKf57=n zxBs7M)5l`$fc}5qSc548sJSqefLMV%twg^W^n(}hf@ATAl@bh8P>%fany}du;)|0C zR#XW%tQH!YV40Pq!{{8&E1}4t04}Wr3YuH1=b{e6nkZ6g%CE#5K*W^XuvJv9!{uF7TJu|WQVKX z3sDvjfmNNnZz#I>73fY}*k z3=^q%@e&LOTFGjh)J8+g3VBQ3o0e1`$QY1c$VJ~>7$W$Ml8jO5A^53jMB4+mj2F$V zH_uhlg0wG@@8)i2%zkqBgVQ=b9eRpMk9gv|D0nM?uD|m+s!iL>n50P+aqed+{O_Ib=|pLumo&=hWWwH4=il4n_j_gUxIvk8FpBK0oDx z7~hf}Zy^fh2ZZUuf}QRY++zNkPr(f(th@XhBM#T#LA_PDjXYntUz` z$R$L+B*h*LGB)s!HcM<1vH!Y0q(>rV=8PclbyKHVzTqKoD zUJQ_`IuYk?LY%y&A7wl~7Lig0xy)_4A#kvFfi@c8g_ep@6p|Y!G@k>#)c-7za`C_N z^g%p6arAt79|Th|%!JUUsg*3n@AsmV|KNxjPdVTZvuOGE~30~4q@PK2z ztGbHmW;om)n3`(8dZfMS#0e6>#dm(|`O57~0nh_t2M7iLMF2X7GDs8$tAE$l9UHc- zS%V-GQ$RV_WHZbFA1qLk@SWVT5&u|tD#i&{lK@&zvIS&-H532*fmIe5XTHB6`jrJ# z{(nI4&*e{qGU2lnzZBmR{xbyt-hfm9dZ)$RSyA`W2bRl#)21^p4Zi|i-Gx;Glx;!b z8StX)N5J3bxxABXpuZJkvXulN;^4`VZ164?4RUANN8i>E@>Fd9_`Um5`R{Msy&L)} z%pR@(k_%YN2LrH$L|;P?66`$Y2n*I0s!cn)tdIZ5=?aoRzm~^frw4cdp)LVXGLi22#q!I$F|^Y$^k8%9lIUGddDp~u=>?O zmi*kK^ZoQ`(e@@xOf}ZW2TXT97r$P1h-276F0r$zN3kNa5p4Deew|A^%$3#XC*sZ~ zk2|L0a7q9K;KPeHfviC^bQY+dOrlC{9|Kj#IvCwgwjVq$Ebdk4H}fuh#F-Unj{vss zP!nsfc9KAdmY)KEv5kr_-P~oDDk*J zY*m#H^1u!opgy|z!1I_ka#>f+i2u)l@}C^!FQF_h-x2nS!9pQgVX&Ca1F%3pM0`kn4fkF3CTrUrEY% z2rNr#Lz0VbIx8Ij9 z(V5II@z8`7h9ubMpY!JD`n(F<(Ij>z{LI5FFows*!k!aXsRI7E^0&4D`EN%KWUEO4 zbOPq~=RVk@DFG_#tO%`!7tlXM1)%+Gl613KVY_|LHhgmB-v8;IPwPqR?pH{u|6g(U zHU}f0p&7aMO(B?0u(F2 zc#fC70t`1;<{$L@q8^(bLPfvPywO|3%`Kq}Ph10+ayCxv_@wOStRrav&Apcwi| zwj1QQ2hMui>*}g82PZwqXNX6SJ0^A^gu}K#OGX|Vd1DABU7#V+gGa{th-gBZuO)H- z!#+m);#_wm+Yo7~IY*3m| zp3EFLH!~w`P6|dTc1cf^{SjLKmvQvmuUx=B3HW7FY~$`!_?-IsC(go;Ka{MHu*8Lt8wDU)v+;$qv`yo?|*di)~2;<)c3F2u$uZ`rxFS{ z1ul6HR7PrGJW(%k=@p}bsWNDe@Qudqa22`FpOk-(#Q@@zuX>&(3eJUU_5YUT4;L{N zfC*KR|BS5dzg3%Gd3~2kuyM|o@bOb1Q&Y1n)bF=kT5CR8?$&{2!oRQ*GXD9&@LCXOs2#z>5 z(%LFPFR7lkMNsqefm;;vNQfQSd9B{#_q&>H=#?}zO*I~sfQNX>_wS3XC<6h_c*u)3 zSE%#QTQgnco2ps%2)aNi@alz~D2Z-s^bXngurviR^xLoMAAJ7sT}prci$m#2l%(L< zaoMilv1--FkG0_~g)Nx%9NKD(_sfPG#J_RdP{mK+^N9Huy-)`r3+GB^p;>{Ir?vRt*(~2Kb_ch+L+8)jT1fypM#F!F17HH~^a?(3 z*)#xL&pPV=weho3(pylU!dS1fs3T)=j(M*o=51Zt1|lfSd|RsDdZ|oc{ef}-2TlQe zAe98FFDM2q=26)nOn+JJiT;k+Q)=ZK=1*7lmhlS?-2UM|;?Al`PPGyMmn4!vl!0Pm ziNx~|$vWgw@_G1i-EjO6tAW1e)7Y$(@gob+k>Wb>8_FX{>)L6?FTdS;R9Q22hAsArLYaBJ4oapnK8_*VTo+^Tf~6V~hij zREYzx!#F7BWjgB)a#eRgTfv0}?yCxa7K8gB1NlaV&eW)!_cx~q=RTKXJPTS`!qEfS z5@X>*^tZU6`KtTF(_Ur-tSP8kNXHbSLD+$8`O(+A{DmRR zf~@G*2Jcb?rOS{FSODlh%|BnN^~N~lF(Sh9>t+UmunP27O`&Dw-8kaQ<3B7Rd%FyH zatL0c$_82Yq?S`F-)Z&rRb8v@V!@9-f!_e@D2WD+32z-1<%m^HzeKO>^>cGz2^0#4 zF5Vcrf#ZM#%L_a)x0fJc*u_s-uA3g44WsNRc%*bOx%Q>5Sz^bcBwJ*MWJbrd=Ttv4 z3yJt`qb1(O#ZUBe@AKk-7J{_xBeL;fV=4mf2LPzWUTO>mt0Xw$sgfwGWbD3v4GDnB zkiq%O@_N$OApCiueqTlZF~vWo+r+-YUk$*ZEj`*iIB0h{0%8y>O;hx%9N9R%%#SAQF(?<=5{Z8jh_oJXIj3E3FXMqOZBD33lTLE+pWm z4FKVP`x8$`@z1<|1>RG6ufVkO0SVw|@B5z`h?^_bnGe^U2H5U>Pd7obwN0(oFuYa`|2EP({!7o*Iy& zwi5H7IDJ5zs>+%g<4xPioQ>; zJ~iBwG%kPKel!6slbLZU5k8vG*S2XF^@LDiHURrgKfXjG+d}i&jl3;mw7f*MY|I1v zS(HG-4jBM*Cz>W-w|u}=7YweH)eH>X$q+Jo59Lk>h?#-%YSZ$eL_gdKK#%cJZv@wb z^4;IhhcU+}WyJcytK3KL#L(~(5Q?NWG-(i1L8^pk|9--yqicqFh|#i_f|#RyYREWx z?yB((Hy+i%+{l6U4~ObHxa|Sy2k5mr$Ug{*0q@Bi31pAV;&stMu>addjeZKBud_#x z4~)mT_Tej^pEBU&lr&5_LqZQiA&1|Gxi5H#ShC{XMtnZD>>L~yTAi?m`=m7r+^^V& zdn5VI8Onc_dvy*V@L@Lnd$lnH1%Osgl8i|gU(Yup&yO0AKl$uQV{0>B99yqH#9xuy z^o}vwvW#~4WxaSrkNu;6xJMqqb${htyEfqV(v=lTG?mOZrX@$GAGE1Ved6hjN_kP3 zg5>6*ekH&_9Nv-KjmD@W$dChv=nu|2r`k>cY(SI0Zr-+Ks*_NB}SbNjItl1Pdy%t-)mZHLDh|goAz-1SI+miFqm^X5~bEQy|k>S6@K_oR*nS zA%m_xHk9*X*-Ow;&VC$Qra&St`cU;0XJ#QSzkIg;o@7VCba0B}l+0uXerGYypguv* zVBkL0{AQe^AXbpG)#(N<%r6oz#ekhFx7RrM^Iw-p$B7o$HnbVT?8Z4-!uCO0`($4o zRXH8Bb`B5%r#*yhQVsdksE0c;5DT?JN*^niJ$EyTw)lS!O+kA@QV*k8wvFNS^x2^@ z2vZ2UU5*UgnT$IqJ`AEngc$=3eSLWk3J~)H`afs^Nf>O94qo$3Xot}yE`J3-JaWlL zl}fCQ->x20WS*#e`z31$MG!yVA#KI%q5Ntsgu^*G zf1nhO%+@rrAStaqlY2sx!0|-{l28=lGoKl1ydgSoQ~l1`bLY;TdiBE(KV&(kx?wJi z{bDwq_d-DydpYe5a9E~r)y>w<*j?E;-@G(%O=il)VNje1rR|M?;JkK z_5ajU8!Ogr+_-Vewr%S+ZdfH?ViVxxiYPcEK}#rHvV-(O04Ix+V!SZSZtgsP8Sn0% zUANQ(+-j9v@=1gM&EYpm{hcP1c~H%K?A3{eZv4JaF#F<`khSs4YC%Lnsb!x`qu ze7}**mybHCs^`LCgrmDEc1a9;_nxDT^VI(kv**=j$@PaVQfRGN7#H*U;WytCX*Q9G zMkND9**}i+A6|kHnDZ13p#52Y3lK6?`%nlH9%K`yxQubwG)2Sro>mBKjLsF`qiTC46qunvc77l8=V=x8i zI^;LQz)}v!6zD4JF}0*~psAO7feC(e|Me5n-iRDhAxIO1eHkYd^QF2|XG%_nCGx)-Fb|(ZxQTilQ-V*DEL*(*+8$h_0Dyvv|QT+X< zyv`$D6~k*pW#Q~@D5p) z9T7BuyiUL0(|Pu-x8CYI+jpq%?Ah7b`m<-x)}O7P0RcG63#>i`K^k`+QUIJ0UYP}m z0FkMiYyaTL2j_kCN$Q;MYfyyUb0$ia?n`4a)kZ6WU`XTiB%q&#_^lZ1K&~SN2=srX z*cb4xMX_UdmPca_Z#>snV@yrWfRo1p4-zaHDK`CpD~@!FmKYfzbRz@GVKy{?Dr7K` z_O`p^uvlG7|I$Yfp!l!gYlu4CAbB$((xR_kwi6<#GdOOk!&XHZmkN4c-Um?jb?f;MfE|>KHc~%D??!TT@4wUKn#Q&$C68@)GI6u}sOfs;P zeE)ygn_nwdpFiE`Q}n9>ps0u1^R5PkM&v`{|5ixr&t(_@(fIujkQ)3;*^OaMs@`9R zR8ihh;HC1Fl7sLBjy@45I)3jq?h)y6V8|Bhok6=l*QIgm&c->pP*OWr5p zdvCmu!GU5NcGN<`i_4Dj$%T{4^l#LZ%*+hQoWS2?Wi1M0x{>h*ndV-2dH)3v073Nb zaa$hxA0PX0G47Wo6iO5%iBzaPUhIDy;ja9~V3J$1X&byUx*(zwyEw-+uGM{Um^W2j=DgHk#d70zpFd(C&zx zBq{N^IqG^l1%-c#6at15aHdg5>9dFW=4R*4&Yca_an9Cr|MRPE7Fpr5^}v4v*FgT| z&oxtvK0#LpOIiosK3m_{7rr{&rnJ_mg&S5ypj^`(v8p}JJ zg!>ixYX9Y}LkYmE{~)8SAODyx04VSt7kV0FR%pC!6v$JOpHGOco^7)E&6&pza6)(N?(#GeXU$nc+ydk%_88f z@&Hc*KqL1r-EYG2GIhQw4Gey-Ck^+kMDTx#o(lh!$X6P$Our4BZ)ScX`B~zjN+bF# zW$Q1!5EfuI4JZOWdY`uABU%BA2wbKpm)Df_Y%-SRDaeOdyGti?O_bF^s=v(T|4>H5g5J9?BS3ubb};CnzLw7@He7-Z88Z){GXi*@YFR8sIe~N-WjZO!TYvxu z;+J0BUPz!phOGu>WK`_Nb{br4a4^w~BH33O+b&3>z91trZfBoY;6fjsQbNG0L6X(S z<0Xnr%tH_cBi;kC4~ruKSC7qd3LZYF1($2gM6*0e<5pL5l>1kkzG9pfMQGyIHwwyh z`&|3lWkcT|{3ss5?Ss(Yb=`1k+eQj;_Jh({rUD{L(JGXnBv}>(T1IP>9*PkO^(P<~ zI$9td*dZ-@jV_|TFZ^_kVo_dZCa5WQ60NlUMC7Sc79DRN*4c;A!aXbe%jLp0h~&|U zmM6cVfrQ{{AAd*F4N{%g{ES_=3-Hm(3}>MT&Nc8R2>`W0swE&mX^Bz+pe+FHU(LQY zm8xm2kG%iUNL!nbN}&kKTqV-lxR#OaY?lPx5?5epvwip?Tht>y@8~!!e%V_~K3o5w zS9QeuyZvLXlq@5{#R%4OHRzI~&qptg^fHShlZ%BC`DC1lyt(eV>pKvK7N86uEP^1c zV}3*7itbV#(5V!ez8Vbkt+^97{`R-N@k27D+#Qu0xEt*CRQh*EnwpXq{v@cIV@}c znO9{QKn{2x-0?3Nc*<@>pH30KQu*^_{Dbu_;Q)HF3Io3FkH!W8zveodn#L;nLC+~L z8+(rL*}ZF8W56h4__+ufo=G?pc>V(@(-t1&7v=fyq(q17Vq2V+7+@v=ckG@H3PIU; z5uz=RT-6V%V;ky%$9R9Hr2RtO@E#8^94r4pk~n)F*RBQWTbmr6A??JA*GJiaDy7o9 z;R95WIwqYt`eI>t!6ffyAYeWa;#UUJ*3B>Fw3p=}qp6;=NnaqT9YCB=m=9{9GzCH5 zfu;1&(oh8T@~pTs$O7uthZ|6oLp4buj{^>c1hfXParvjK=kNj3BsmHVG9ADO>tdM_ z5~^JA0@I~7Sb#s0rS~qX%a$1+pAc3vL7PphB+q7849tL{((@DhJgU&S)+Pd559Ay_ zoJ2WfVlqsM0_9ihAj(of8+d$T{m|;>!03oiW1z+`?jY6wn{V#p_cso6Poq}E5Zn_! z(bVbkIGV;fL%<(0#vsf!ab%Q%plO)58tRS2IKf{<#m^ocRHM1snW`>vlizy#?YA-O z;qspV_hYAr!0+oEqevP#Lov*C*ng5Mx@{zf7Xb(nc|1j)orNC6f(|Jca--eRG$k)8 zFWPXoIn4Sw!vD<_fL~w!H`_(td}$ig6ZDoKqwJ>&Rpkk)ntx!BKIqicf_3hW4zNMP z3BJo#fSzHoT|n7h4B`EH_q4 zR&0Yv3MK^zaxiJ&#z$sbz%YJHdI41ASS5d%4y2<`B_Hz3-UUList)al3hhzzlP^#N z)J&@i%4Q1k1;e`2tovZqARLSsgUAjjFb>9tibiH~v}M6fl9yA&Z4yMbG<~0L_MJ<4-GiQeR05l)!FRKAm^->kph5(oJBzIBP*mTPSCE(0t3RM62 zU$znb^6!=ZPYa->H=+*PR|;e?Gz(LVCt4gL2GfrRa-@ts>iMZQaa&dJXEaCkr=bru z+Z_R~YJP1&Unea!j%fdYQfZvTAQ3`>n87@3d~|;$w!<3UDQNkk$?rSBdbd4h?>iKN z@Z&)ZADYmZ>;ZMrQ!9}>8ZZl@=0{~^P z*#qGHb?ry+9$`9ZsJ+r~i$wITX*02f5eZ@o2N(&y`Qe*yzKWOkDToc)BEdC5?c{xMgkl z^2>)&vW0)dJMcQ@pU>GTJAf+Vf`+~DAHM$n-*W$dW20sOYqxCJvT@5QoWC}#U-Ozg zLCX6;a23G=H4T*U02AjLv8Q~KkboAPC--nv04NbNhHNcUfcRyd%Vp_3(_dq~Ww!Kb z1H|~HTrfu*Kc20@-#Z|A)=@|r*x$VTKeG?w-rD@*xx54bjS!ZfKAZ;MWDVsG6V#L; zssbcEMhXB7{8V6|l>ij|rV>~Y22k~&X9ESAH9-a%rz2SmV$3YJ0=s)jY~+m*&nVkD z3oU3~abG)P?%TIfV?+1C-pzyG1JJ^Z|4ujmH6jCp>M_jWg=)0!ELgP(v5demUnBwr ze6roi!tZb#`wG}Oy0-=StCW)fRTmM;Th^8*&meo^fLGtn{Q$Tck@9@Mr^Z(@J;x%aE6U%pMbt}7X5zi1r+}M8F)sG{jD%(b0tV) zG^tF|MwJpB4owZyaZH8-nHDlkvmM%?47%yfyA$EzMpP<9&y7fh->V&GHjyYq@CXjH zI|DK?&>*u_<~(BNWF2q{rQE{W?}6FsD#?8JT1tPgaO9rdyA$|-w!U*LQ_6?yX@TeJ zI-w$=TpS99I?vjPeKJ6jF#MBECykA6j{z&$$i|eJ%_yoHmo}QA3H{wUV)Mf{Po4Vk+ms|99zHjF=nx*G zPHkGr1s=XHDL@LFRQ~_{AX99`>jPxHM+BSQ#9on^A1qB% z$alT9hxa2grX@BQYzMJ723jWa_aU5CE75UilGR-Td_Xe~efJN(_l<9?UAu1GH-P`X zSifc6+V!hQ1Oz;1E9Vm}1g_y&c;GHWi#XZZzeKl{eX!v9<4W^>`Gx@Og6^zjfR&N` zO8sw!f<)j&g8%^8>#n!L-`e=41+a{JSLAy5&A|Nda>U~=V}8O9c|q;}6Amgsyp4DO z0^$_*R=Tj{#|^3|dVr}wEE)kKAyyT@$8xy;Et?Vxt5D!;G0+-ciXTJ{p_}2|IY(+@ z@7=@r0`w0R3UGjyk@!ZGAx;ul&Py&{$R<;`}!ExA-y*LHC{8gO7Nd zu^ML4P|*d)=`PGlro%|QK&*zm2=Ag)>cQPm=U*lpfHc--nJp zzgOY(=boItjv@%P_o6`HjCPNjgYXeN{&t`5Vq}O0nd!P#>@<0dI74-V##f48@(V(p zh6;c^K?f3@#IzKAd})<0H(A948^fNSH_`MFCmk$P=HF(qOe6$m0fIL28D`DIy;*<4 z_yj;6h)*s2uA#X3w$w`y>@8qe7XiRyE{Y4F_YjfqW{P3wU#-hG81n@903bnvpELxn zifyjPMw)=2=eu8XR5om|UErel&O5>IJ!Y3U)u4OX+{ju4=(Ki$~I!#@-g`Q;02B=F^SB4ea#h3})vC?|GdoA8I9{poLg$(Lu_TpVV12cD7^eV}0E~6$E*M>?DXIrBkec;N zVQ#hK6ac0I*Cow%&-WGf;tW|UKKffGYE2Lm^icM>&^5TzO>aSd3Vr?3A?Qf~ihR8b z4G~_k!rDZ=AO(xpx6%q&90YO@Nr(^S)0(3*2p-#rW? zu;Ti3!K^{xZad$9vIZBXuYmNi_rDh$E%CpeZf`$?7bQrqE^v5#cAGz+ey{*vQY%5W z@l3SHu&SudVJHHN*aa1zXh#U4%u+@MfOgVGFSkDN4WreJ+zSO-<*XZ-w|zzgvv(h8 z7W3sK3gKBb6Nq(?KpvPaH*SUmqw}c|#>}Fc&}tK`lH&%ZfC9tgpTm7&N({{wSRRUH z*gsy<08Oh#X%Ck61SXOlb0<*caypv9E@UHwDfqPfyg~FaGtmsxjO$g8B0WNFE*o)r zg6(7+ZMWir=flxUC4mDZVYmp33Sg|!>C(kd0Zt^Q;OS|Tu5OO?J$r)wKCYf$yJk=+ zC7;5_Cg`CP`JwChF~;@th@iCD*@whFk&8kgp-Om4-gBm!6wn18N8roI+a65t`6yR7 z#fNk;wkm>`2(PLS+9~vngNKLLLLu%_G=V$Wo>HWzQXnI6G6@&%%mbqQ&KN&VZTwY3 z&1U*XfdRmHm|9!Qcqh)Z!jX#t_{H<34El$TI?C+XS)!ksf9JUm-+1$sLZ9H=2QdK! zb5{5pIyHfiT>}jOZNFGRUrrVU^KIR<`8ef-P7Ob7gxo|GW^#z(Tzfd#kUp z{IBw!;AeG}px>+I1Hzwo)b2feWkO&8^H1KADj6_9Z5tTtw`|lkrf;h5tDG9~Hn&1z&DtF+2MC5*U_7eEEzjUorv&DP3Ihxn3YSS9efY zhV#@5S-F7UL@;!otxTrgk`I(WuqNa<%1Rg@kztk-OQN`8mpvpnF5B)gq3itDLlZ|V z+kFjWhiZ=hPo8Q~*nwlLT@U^+D|JuOQ1=}kN-nQv=X@1<^GgJ3@u9w0iB&@|v_w!e zd9Fylhz5%ch&c(IMi)8xti#v=#bm&TbhtBDgHKO;v-_T0H~p~t)ORC#Xm)Ek#Qt(t zmVU_T<|qt+?zs15C8L`V^bUwao3~RrG-%~Aqt*MG2nL<#4FbNA0Q&f)0$d3o)lHIY z@#^MpVU-Z|B(w^&tW1BO#lf7VyrnE;MGShvVQm#^O|U_7Av0BAT4!enx2ld_F6gcl z>LQSDB-lt*W9k%Ik3_J#miD`=Az3wt?P_OTANqj80z$49(u_Ni2+hq9?Gr2>R}=n3 zsY1RQhEEkEKbn1#6lCL62PHL%`)v3TZUo=X-c(a#qc1MkM`<#|WLqM)qOP}Jef1m> zxbN*5?q4EtucQIBE3{M3$cDN4WEC**h^0`#e{#TbKfpS{B=$`!O{awCKZh#JsE~`l z<53FFh~6%39UhLU+bKbp`w0JhhihFai9i zDdpVj-~G#f{Via>^&35?WA>qHG=>3tKfZgn7126`vLyj*;z@xEDTkgwv9A}3{#IPU32K138%C}}8iB?m z;0Eu&DFC?eB1uEM0XW23@&(Jc+K8W!07NPj;g|RE25O?*?qm@X0Eg)Rzql4Z(aa9D zfXpEYX#J2RQ5GvH_W>&T%LalfM@Uy%Y|i;hg?kIl5VxW=kEfleV#v!| z7jvqc!oOs+cQm_SMqV`4yO$iV=I#o&_c2n=gU`X)B?>zZi-a^{d42f{=9WDQSW7Lc zs9v7@9QZDeA>BC9ok}5fhz}+Q1h7t>H{YEfdH{>zlm7)~o?xzO9>`x4So;VT$WjKoKuQAl%$Xv5W3f+KXfJjX99Wf0Y z?If9`#_z_VIz{|T2;7xOXDR#@_01$Kwr!08DdjsA2RDy{cd9qBr>Gvd${OVMGg}2a z=Pe+-16@g)vCx>8tpdJ}R+;$X`R@H+rp`7t>NC6IP&REM&_YLu8EJ;%8M&4RSz|}z zYHS&e$Ciy|94iPJFs^KB8%X^kze=tfVI=`I23%uOAV8q~5TN7(g(gdrs-cNWqZL6L zp{A&-imjBii_{OTq6%%4R;{Z3e$Ri>_VFvm7-Qz%=iYPAJ*U3{$e2U;Xc)#WYR`=F zn@3L=`^}xBz>lRlvUm)qgG8abyDtGA0DTJSF1HIjuajcX@I0=EU6G;1zI&O91{_q9FH+7P%VW?gPEWB7iFaFoO>SGXOUR-ag*kQKlw22q1%> zYo6&uWu)H4I<^qBAaAribkRf|=8qxm1srlJj<^5NA zkWdc@_<{Y02_W>cCVJSwP;_5Wb+wk8H?REXU;W!};Rdux67bed=zVY8v2_!@0N4LJ z_yI@(3Y1_fk$?awSit=c9HCsxOyV#3HaWHjjpv>|cu6YIZbpFwj1e!b{zESL$RL2b zmq@nx_wwgC2!_CeAojhGBYiM=0$k2P(>o{ye!}6;@xkypmIRjYd?lW$oMPf$wlD96 z79YN!4TK5B$Oh)KxCj*G$c7aC(Tf*N0oDNKhib=Q^qUGoP4H*7BR&Tf^Z8XN!S~la zUZTR?efG#0lnXSF7I;L8f3X(zWHl7Ts&*-nl}e9{efuP)g4d@*EMY7>Wf6$8YA6Oa z4$*eKhl%^G;0k_@sG#_S(kEM^r?;UGaX>@>SDD>Q8DedCaM%5P0D_8?<}hIW?7#j? zx{MT34;+L-qK_pO9BG);NMK(OVO%vvDG_iW>Cw8n<7nqFdX+(2i|Y5{k?{nRH82a$ zXYMZaCK~Dkv&IWs)?O>uH@^n9G}&44%sjFe<>(DFeG2V*nSYSc!!Q8u(}HV$tswQt z+gkE)8XruG>M9o8;o zfpTKz3_<@vVSf#J3Bu7b$sivanZnK}nE4(#g5XVmHo@`^d4^WOPX28@K^g9^jIOK< zG&iq|rei@Kh@X>)tewqRPg(h2T4gjyklMsfh`<@ta}Y=%kLCLOpt@c(1_o1eM=6H$ zUqUH|QaqR=wHy~GVDk2^762GaQo8LMoImj9D{qkz2!99)VTrlc_N8th-H}gDE$#2p zKc%%5G+_@sg2UXj{Qt3IV*hotmSque-}}V;E51fpNNoQ#mesiz-gxyD*H**gp#Y#2 z21gJO1_0j+Go$TEg|4DB^!P9Y1=xu|m)z@Ky&`}Hx(5YN z5CBNQ)#9HCo4kPi=M8i(^w8q-Nu&vmQT~_u1S1pt2{iz}G_=(Z^W!FiY68#>L=TW~ ze8cT7%J=7df;B*O9|=Bk^^lhoA>fHr!CpaF0j~Olm?Elzh2R712bmA#DD3&5J$3M( z=hwl{;lQ37Gt`;(8DH{T`89I^f(#@OyWjeHi+kdY&;&N4V2adSVNV$Q5NKlb>m#f5 z0saQ1L?VXw0M_r6<>U?(V4s5cvL1RJ$#s=ydXs4;jLZ;n3=*9u;5K$^X=&l^XN2u_ zX7`iyAYQ(c#Qv%nV|YZjFSIX?->MXc@U>G!NLkCelRWxQbZ3sR@^)%zgc+4n2Tr-B zx1oNik+2Oy5Y+o-0=W9l>?f-U&Pw5Ku>ozg3z=hAKYr(n z&v)+RzL=bzX6crHzWum=;O+q?a5>P|2j+1CQW_X_xsy_HXRq9wy>g|$A$d_7(NpNK zpa^)XwZ4rjp&pGjW<%=09fi~-oo^qZ&Ab7RAGF2Zywr5bw*QrVZ_{?O zwEzZ15TBdeN%lKwVm6@p=v(i;`|ewBzuk2F!gv4qvwxxkcy9d*KSb%zP=^Ni{PRo! z1Oc}=_}j2?2OR=Vq2BZ&0DHPP5=b?QTK)ryelmbdlZTi12fRcE;OO}m@oK9LK;!`k z5AV)#Ri`Bj78a3`pPOW@C_0f=d?`7g~BUiF#>vr!P@ zJ$%+*h8H|V>;bF;`0XI?3mI6;Ke~tg{0edMD8Q*kIA8i{;RH%xRmGe)77BrdP|X1T z&i?xN-``m$MIC|*(&`{@T+Z2S?+Cw=`NHEB<$h4?%qmUy>PQKeE>pshvGIowncL3Y zrT)HNx?XZp2KwI)**D0)C{RZ|k;^f=t35f;ogE|e)q`U{OtLk>Uz&Mxi(WR7V~FT1 zfpMem0lh$O=N3j{-N|~k%i*uP+MXeq1kim3M4q)1o3@9mB|_ibo-#Mym9^z0!j_Yb zUwwR-0e*YBD!PZf`e0kV1%RudFiPgYy0H^L4AEy!Xr z0nMovxLadTB=|J}bT67zfXFC+l5YYCyHH`sxh0<`7f8_x znfgTlo-VY+IZRX!3)JfOhvg%k1SQl+VwLWVON5q?@HOy}`y}G+19|2Jy{e!7&a2{k zKYdfx8qTEyAc4vd!Z(um!^B@58wr2}u>mZ?`KB#^>i3X2>uGvRpZtj4;1DZX@POU;B<*rS z>tfCCv%&!CgR1}kmw)@Gj{Xh)kP$5RZ3;LJ^TI&ixHqOlUGF--VeN@B0XIJhfY*G0 z*OBN)C=*l_`B^w$M(G2*0b~JG0U)1v|A2GeVxYPJWY-Z=fS<$(aAibatkQ%H{GdHn zMKRCruY7cH9b3KMjv=pt4hU9SM)$aK&?-1Fl`UoatRNSF=D6h$+5+cCfi^=Lrxbt@ zbRCKi$lWS9wj6($+YHtq=wKg4fY#Kc`?0u63?n6EP~aKzSSg7-_*V^vJuPQwpjft!2A;(x9Ls$W?p_Rj^K$;l-e z8%lo8$XJFwB}6un%|x^Vwi*oI1wGIa?xOqva%9uMsUvmrL@oC=(Znxv2z@LfRE-Gf z{&x0%AN8`O*rf-y&cy_Ww=(gcA8m>EcQ-2#B-yWS0_ug%xvB1pOI;7dSCQ4E2wiH+ zsF+%wfIwM?43c+Y6K(fA=XLh=Wb%U0=6cM~BCjof-bM1n>s}9mc+x7N^ z?kW5vsO~}b8X;H0-r&hwmBX9KptsDT=3EMWW~?9Kd_m|w;Ou}l{{XXS&iz+^`YH+F zKQ0mU%%*?@-UI^po!s14+oG> zqwk>Sp;TC?1H9e;CBp6qdJ+KdN$(G30@448x`Lb=h>L&1Z&~AC{_zo^{@_i{@NqvW zK%;VEo3sGRNH)1_P2s0RN}%DGC8mJ~d_PE#mANOy}UZq zVLye)HH5t$zazCz)W3*{uLX0qGzYK_|BoI1A&>$o0I1I7^{)Qxv==<_FX2X)45n$Z z(p~pLR0-@GM{nfe`^Kfb{jpdFwxQ*m`+)TlLAfEuh{`riyP4kgqEhRbS>W;wu}t^< z0PX7RU3Q5mvI~oh=ec6EHm-_~W@B{Ig^S7;g)o!r3mc&V%@1Dw)KZuC+M`Fk^3H$} z_$bm7Bt$D@XzOF0qlSt=_~UAdSX?Eo!>KoIZHksQo$ zYClB+a;^N$uu)wSL_^gT%Xwh(eSlxaAyG-wE*s|fVCRLE0j#|6SX)>Ke^=y#tKjQH znRXed&Z!nY9qep4xOH}dmm)(9cwoX}nI;ZUVqP|po>JF#1o33i+ZhXF?dY~d!`up#le2->` z7VyOFdq4Zre**#l^&|dq2BZMkw4RE8>n1Qj*8w*_w}XU$JP-wdhxl)#$CJ_3*T?kx zG792*_p&prZ6Sw^U`)jKkO}riO@PrJ(EvmM=>I)Jf41aO^Su2dwJu-l<0-^1zn>w$ zWq_gYF%@{C);*{Qe=_xn2?6}ytDo>E0o=QHgZFRTuxSA1o4SnHk%=Zc)6--C-bT|# zKnDQ@X~BG;I)$_Vuv(dBNh1^s(FdY3V9)g0x^LY#Ck;~NF7Y4-5SuFn)y871k08=e zntC2$E2chZ=L)aoi2ez=H}QP&(U`G?cA)b&n1O`(P$}4DO_KQ2>;%}~dvc|ba}aF^ z<1Y8TGzvK9YC(4cEZQ!*{LBa}+7g<2$bq{SYv_ZR;cxeps$3ZkWVV8QJ7YS~fTaA{ zE(9`Y$H%BMMsyA7fee#|c`tIu{IUW3o%wZ)95z&kEoDYVVyKLp5Azohr5j6`;tc@_4iaChy%wM_Rd+%p|_>UefBW{$JE;FR!vTB|G!QwU4bQFLvEBp_76Cb~H&9L5=+U%hvv<>%&#d1ZFu-d3 zAEE#NA%qJgXHXN+AQBcigb;~g_7_NkMt>Cc(*COJzG!-IB20i2%KNa%@7uq$4G2~K zjf*6KPk;u!>-kNd$OQh%1faLTy(>5Th%11Xz51gJFpU#3?_*RBQ$R+5(xj>r=`LuL zix^b-fKk#Q0q7kbDVmtc&QGoTb`=?aie4}qO>s&%>`>enxekEyRJs|V*Te%q0i?LG-|c482I!4$8ij5IIrh_LcR+LbE8ctn#}DV2aKTXZ>Nw6OZ+s!pbokq zJqSC1Mzo}p{q^(^34j)ahGFVp)HAzSfKHi9qc%!{+DqH+;}eXsYl+6RzOSJ|>(5Re zZL?)RE%6(0?+(22F0C+1OA|PzUYuTH92-~(`WHIBr24S_VZ6x}gA+H9mR!7(p80hY zf7EA_GQk=_36zDrB~kO0(#vYx`qiGMe_qK>#%6v%>uMGXpgh)FGj<(g98V}C+` zC9GMqC2DgB+wn1MiPcJ_J4@&fI$-~{mhHT<&wqFlf+-FFS$nNj^6E9IS(h^X27m;gbSxERzXt zd~)MwWQ2eD7h<0b@ZER!uAI1n&FIfRv1OQ=Iud#Paj8#-X5WKHM|8?$H?2e4J(Id69-3@)944p=9@%+n*@B3os^}bQSl+-k}MS z1Crfkq6##CHD1Ir<=+|nf3M_Uai;zu9@eK`I?rZjk_~m$MKRWNG%l;Su{p+8pbLU3Bk=H3~02=3e13hGKyT)1>$QVsb2t z&KGe>+w3ju?q9=dd&58~1&0|7Gj0VNGJt5ZI^d~tD{4Qw~2f^FMV0aGXg z>aQXCm`DRG;8M&Ca}dz-D60!(E#X6kdlWF2;P}exd+oJ-*WMK4Gx&MKJlbyZ1&022Y-Hc&xdDr?AY+)i`M^} zx$eFGVIObYY69S+>!Aj_78pT*W`LI(*#RQ{5lv+MqICJ|pKQiSfav#Cyz;R&2?fstT%MM}40o?cY`#-<; z-GAP&3?TH0bV>p`1ii)n^XFHtm;l!9(G5@rm;t7y&wKIXfMf??Dj)@%H@1Z#r!RvQ zR4D`BM?A+@(FaTpApwxe&aS&VA`1p7uP{>!{Oyrj$Ha{ndbv5~kp1NmO(zL+FE=60 zu7?g6xSzXnEEcx&G#QiOpVomZkm|WM^^%83%V*VFSgRZ5<|_z+GeEHOyQMj@uFo~s znOXNi$>q`%u?8_5^CDh4NBJvMM=S7T;hcWptdTBx+f3o&)yEZ6?5IVpf=fBDjd&@i z`y0}6QUP&!o$DVh!FC_1Hnm;S&$;+z_wa(Ymn8*M1_4Hn?)A3aQvM_w>cz>}UZ0_2 zk7JFHWkIn_=3CtooyfQ-MvS}&Z!KCb!v@8 z@*IprR;VT5>t-f*e@ys)F00U+pE5}r6M0P?R2=-FQU~uX{c$D9VeNv8Fe#w<)&s3T z{Cfgl5$u|Dz8XrIFp!zKTcecPQC0w;O0mF$oJh1QaCFXQ|{ z{(9Rft;mLL{xtxG0WdhwdgMquLlLv&VFxa3@k(XlgHL9pGhsaf<2nyM!gTSt;i+pF zPQthe$oM3CNi^iG{|L^$H~#~OpB{i~pUNP})!P=p{B`YD_mS6r{WpL8_s~Cjg$&e$ zUm(UtN9j1BH?zJ}d*RAja`Ey9U99Xt-`h-uzmc$4mk_w03IO6iEy0eBmH;>b(hRr} z;7$@Rlw{|4m{w-|KQ6Hew0ScLfD(Y?Zy$1n3#UT={vx z3#Jw=0;a+Irxq_?rVj`-gXs@K!EX}~hK@`K_&{{dR^RpYS>O0A%MD_NxDGH33$LCW zt3Gbrrb@k(1a{g_vN2ZeR|!e(GrGZO?&f0fH}H87Z0SXUljVRr?)K{O(LkoK^5nA( z-*F{!8zFpCkRQo(7m>Y?eXrj)<~t?mg#3b80l)n0)1HL~r`{+2>6t-gpGXE)wIh*Y zwMIrDeuF+J>CPWRN{{K0*IW~a+76W+AdJ_@1=$ZbcNYfAY6*0lE)T^Tl5{p!w@d<{ zptJ&yg`yuUVTsbhdrn}JZx%WP&^L0P{7x|2f&l00^Y!cMb--_;<{^?gI%=9J*iSPK zC5aUiS@I*1uv?w^Q@rDKy2Mwqhp-w~ca91Er1emS`9wd9fN6>rqMxS$Joms6hUVIU zARz{bakvf0I-t&Lxt#k%I(Q45)BZ=aNiR{ExTx|TY#Plp;x&NSx6v=OO6u~T#c-F> z#R*3ZK`B1LtdRx66SIY+qG;B1z;?KvTWxqk4a7yxEzVC`m91h;Gi!wguoNN1$qRrh z_2U$TjfY(lve(y9iyo}zZV&!w9d!;Vxe{;+CtSe`__H)F=(w>r$bZy*L*IIz0KZSe zPsH=e_`igJR22JfgkQhA3!LDU*M5^<|IxdD{r21c`uk0qjw1x=K|6*L$pE3R!_O7-AJ_W~zp4mS{ z%~Xbf@$U(8L6|$K|8U6I7Xt{lKERu^w5aZm?72O>#g)?-kBmD!hJT>-GaTS0lKcst z{}C;~p5FxUpF({E_>FtZ6ak?B)&M^z1yBHx18$%A?#j<^+*tnd%Zr?;Bh%A=iSokK zMbg34WzqptLW4YN0>}!}q31s@XDED!ggs+}{U10rsE!G(4%W@F2(RT`xs#C^PM=h+ zORnN*bC2vV%zb9J5;wW=axwh9pTfc&2}45YT&(!j)lve`Pi7y;oFn>2h~GB*cC5U( zN8Nrc6l*5%6?b!3SK539Xo$NE~fUt8*EZ&Q^0nKsM*;7f^>B=S9O-BI&Ty0`B? zVuWd4hVp~~7QZQZk^3(rVh)X-vLl8nIyOQjY|TL#ry4W?WV4wGz-j1($$%3APbvz*6BXBgWf_PH_(g`WD|yy8kO9Qll#$ z!$F|N!TI^YIuae7che;)alQZmGLN6H%d;c}SxUqLopRc7nTd>2A|KTlDhG2kF2xjF ziGkTGYqR~}{LCc)DacjsK7m&9(yqiIxQr67OgJ?;QAkaP)Uc2U4ycbLD*UTUN64wx z3x~tbo?Oy9!cwQu(z;I-j-Du4~!&d>t9 zynYLZ6hIDu6#y-0>kB`8Vf~BmY*UCp48YV(0t2h05Jg6@XYCJmY~Hdn98-XWzf2&+ zP|yVh0MICm0|}t23K-GytAbrJ?xR;W0AbCa*FKvFs$73wc`O1V(yvHB1o#u(q5r>k z4f| zFFki!*&hjE$l1k4DEVO?XccX<4Hn2P{&%7au!QmbtSh4hfUKwDZ;%F11-^D*n6CC3 zRJ3baYok_AFVOUQb7UNLI$PiR6{eQ}c|?WOM0S?Ano*bL-{vZUG9U|Mx5k$knK`hQ zY}hX0h`?cJjpFpDb5b8-iLt(gaTNc6*!I*ThE!s^2ibbe#!ty|3;x~CzX8r3v*h%U zY=8-Xx__aSF#0n@e>PT0bSKh<6oTMsR7$Z0pxlZhjm}BIyz=7ilXkCjiL!|7$6Vt= zMU>OA9Z8^$yY$*Ft8pj!1img91+fLLP7eVEO-3 z-ux7|A+G=5{Jmd)L#olW`(y)D26QE>)z~y%#IuP0eU7mglGfU;=JLDg1dT!TkQAtJe2OweLsJNiE=nomuhThtPzL{%vPa z3&cd&_-6(v9Y}S+Gym|lLjV>7xKT+2-u_L`x&DtFVT?G<@bHLPAQA{#H`dmscqOAN)LyvwDd-x;G8r@i z+{g>w0Jt~?>q$M?W_s`TeCmM*z$qU!1pxD6i}Qq3fv?KlgreW$t6cIZgK>j*L=O;L zzdp;A`k)+gQ7-h502mzP*p01xBTLM*qNAv{iHMbUGoi1?DTZw?G6EsMbL}0dFHN$- z*$DBGgz~tSW5X_mcQ&8q^N<>BlWRjkJQ>1@hdE=tanjLgQfxQVI>6O zN#x_;2JM5=jPi$-Wb~1YdcEI^ii9JVU{Xrybng4CCFn>D;2F>M#mePuAH&;4yq)w( zW)Gu9kG7!FjPblUfg~NSc&>hC_wtp3qA)qE$^AWjM9l!EZFv7y|G)E7o<;v(vkGuB z@O!VlN$mgvpcv3!ANYMl5AjTN7lXnQ*KzCp-O8nENYLt>6!l<6tBZ^0x4iuP`scQ6 z-+AV1?)@#>Yy~!>|3g)<;W;-1wyFz2mrwXR20YJY2mFl`wT_H6v-pXt6A(6qH4jwg zojkK!ITywNHUNj@1D*fitmI#V&G-+pAChc*9d?Gkzx#cO4847h082o$zY;)o77($0 zVFS?cbAHYU07G%g00RG_V?nTh8$WXjNP^(Q_impc4cy}+74F=^zATl{>}&m{p%s7WdD!^l*-0rtS^k6z0)hu69F3CA<)$r$qtqNzVGorrv%ro{WI9|wS9GNK|>KI_We<*OL+dQ(n;;B_Nj#Lh+{lXfAakH z9qYNoH(=?rot3|6`IjWHemx_At-t##Kis5s;ZBJ@0RmLju|A_}qXH2_vw!||XaOuC z$Pwgt<|0%9RckP701-gHgHQledG*GBm13*Nuaq1h0FDDdf&K;LQk^`RrsvW^oFPP|kor#X3}8V94<^;LfN}i9hM3uwzlv z;nUG--`-J7RS4SaWCXo?)nw<%@CI(DCz}H>rdjS?P$eAhNqZJu|7wZvZFQ9Tk>Fc! zBSdt1L}i&uAL#%VqJ|!%n^X@n{dL|Za!rO+9_pxzEa8Va1tsWp5rc-{wTT=tLw ze&;p+lp_Mp{rgRW2Q`U3QCWqI&4{9l8Ecr4v|3oT)obY<>v5A(4hSyOIG+Fb%npv5 zJ|uw6aR12w76I!a1a1A{FIfSg1|-uj@PHaaiN?=^8qJN46MqR00LLIr&UOL;SmPa* z2Jqn_Y=XG`qaol~_VA*8z$&fY*RI-D`~M)`&pYb*Rl9uy-<#hPJOtVP2_X-{caH=> zwQuaN5!p@v9R=K2TPDnT%{YK|z)!q)g%`5GJ&S`+k~fwi2NU4LzLrqSyj-lOFQBxi z!e?i^-XQ@?1LlAaE-x-}|1T!lkKt3yM!{Nqot}OR3zbBLtD&v!@vV`X)43Wv0j=DT z118cTraLL@1;poJeYO+&-MoS`o(p$lV4-cCq#6%(Sr68?4AMl}D=c5<1-wL&X zxH|xUJy4XL`4|jLdQhK`!|B6{v!hepJD6r=I{JwYUtyuiJz!4|97gIWGkp#i)3td* z1pLuH12TOau`lR>mK1=L#hxN+@+fsd3>Y7a^$Zc+uIy=eI)r!dk=A= zp}$f(TM4U0Yy@SnB47X)S<}f7*q=;~wv3VhYJ-wjrz?5<%OkJkI~?x(W_Z4=L}1<) z>Jm%fq{xJj5_UOubA zTF00hv*peX#S`haEtNq}mDMzufhM|xlC2K!vy3o;w92L!y&{m*!RjRN3jH~7W9 z8y=BBUOdZdoPY}e4gRNS;EC?p6SuGMyUX+e{rwlo0CoWxc1}Hu;sOGH-1vw$^UDAf zOj869{K5sv1|;mN+sZBuuA8GA1mY0tedZ{xL5WF`+kXgg>Ix)*`x!>HaraXi><{ue5SqxVGcif96;gmU1lT1* zx2kWGgtV8TzkEM@1g^SWnCZ{AkN`T-5p-nNQD%w>`6uY8p5~V;i9Yx2fFcgE@}fOlllx}zz0R!is?H_8lP^$CleL>i%>IB7zE z0T=j>bMvznWC8#?aj#fGBJ~4na69d9Xn@>=88UkXG7Qd*oW9eS9U=8qMqPVRmukjuM$H0LhSnsFhYI6iG8?$_m%m8)zi!lNpg{4 zBje*~Opm(=|2<{^mJHpBU^!HzrniE%@l>>jdUFgsephEyV0Op+rev|;J zX2DSGtpwZ&;Kk7ANf3}Hj|rfP1DXI-;Zp2__-E%gXnlD51LnC6WN7m~!OfBSZxs;m zK+hT?@3VYtjqcwLVfOay%Izz#1J{;0-D}Gf|2)@ECG~>LOhE}C{P#~o6A=EJ;!pzE z2mnR%iOGw}BlT?rL{^T>50bI@B#!0!jFOusT+3$L_A(dwNM{0Qvzd#hYl_8dJpSIkQFLp zL^+vbNOpsd-qk-Xo76)=QaS#-N`ukzgDL5`>$pz}adu=x zRZ-k02>zY${E2<|0m@EyIr+RT*w!kToO!4dqL{4U4UZxy7ZXxswL^ zaDMXG^pOl4VLT64UgugJW)-*thYwf%gx8t1mFrck^%YX z{ND=w{j1U1KH*O<;C(OTx6hm3kL}}JVuP7a$IVpJae0O6fdKd@3xy_BZAZR8!++P~ zr7jKz1Zb~A#6_nP;wNxlAZsz3M(E5@OnP>u6DusUwS6ko>3UL>6;MX8kxnj0&72Fo;J<${Bzhdj3_NeLs z*zol6F<3K?A*^O*Vl_2T3V^-Untgp9P>%yEnOG>MyZf1AX#t(=oWCaOCk}ov_o=GW zHEB#YTz?A+&TW|yZH(aQpb^I(Thw}I0nT8V78p^qPS{zp7L~-wg1QE5XbM8cYBcU+INha>hpWFw~Q1U`t{TC^2mTwL<$M(tbjI zg)G{ifB?iOyh-k24MSNvU?o6gA(A-K3sF;%kyyT?t_6J`?&i}um{PfqKw650WcxY( z95AGamG;72Am9A=w(HIgXb}*5R(0Mz@b>Y#GWz~B4?aXg2t_*uY9B4x^^DAYR_;6rn*S~r zy$4E?;+*6G?|KH#<|Ph8qZ)j`bGouu^`JlO)up(gx&Rtqkq zvl+Y)StjDq*9x`4UBV@*|1M3Iz&j8pc?7Ld_sP@cW@ zlRh#42|&j08qw}+ssVrig1s9WfPDfKfi=!7gaFTMcd~!2o357&8xSCwSws`V(to;M zs$k#(P#FOIf&WR%<#Pxnk^qeZ3PU6TN|SYUcyD9xbG-%+u4Ef%7u#x*U{QiX4F*K1 zNMiVmmkFXOUBD@vPG6q6J*8wC6#`ai=z;OCU#DwC4Fj;QIfiIIP?cY+mz)0=m<9&j zjS^}VsENV*;eVS9g2Z69r=zT&a2^1QjT=C>>zUUFbru&hK7JGf!!XSVj~!muTyBM zeqcPuU1R+X*^x*55&2Nqp46hH`R*V6{@cz8sRLLGR#yO$;mUE^`gnkh*ZU8(r zyxtYmQyzfrepNca^mu{j{|Zrm?Gm3Ss7L?b6mbbD_=oi-A^Wym$dhb9Im|uRIuL4} z#GB{5gVXUgjudck)b=)hEv9M%L)t~1$~Ev#LcXPC;KT*=QUQmLpS*x2=m&@B^>^$5 z?nnO<6u{v3&;TF=xZ?--gyIwc57@$h3ST8x1lf3Y(X+2ISD8Ay9VdZzXm_x23C17- z05<++5|0VM20#ihdw>=I-u;o;7cU-vVf@nrc&a%-D9{Z6(*SY*r3}BnWEYT_b^ss* zc#l$Gjp{zs`%%qr8nECe{;zyj9TN1f-MB&uATP`o`e$d?x^4b1UY=^FvY(!&`kyB3 z$J-Y#Qvv||`FkARLPK9}pwR7KPA0(vg#la}T9=FA{L2DR0{dPg_d}Z@jcoYv0#l-b`dE(;d30%)uo#QYkjP=l_YREsn*d~+M)k9^t=nWWy8QBLCOit+ey}gfuKCmq(c+cIJRp^kbsOhc}J$H%ejv zeO&$){{zD32dm0HRMiv0}-RVMluD8(N-<=v6k6^xFCn4q{ zc@%7g+C0;q;f`QH)e|GP!;O|4rcDw8sJNPvKAJ~we0h2Q*+YBv`*H;M9GX7>dFp*w z11SG@W(x`6cYo=3U!cAh0oco7Tb0|%zku+k0%({zyJ0Kx!S65y2qXb*gN=VEgLV?( z>=glse8G%=6M$ZSJevpp@A|JEW50R`H^_iN<rEQ?M(N;`u7v{dbgqU4fa2EaapD?CBQ}J z1fgMQ06dU4!2K_Ocpdv&MXuYqTT7FT58B3%t`iQ=;Wby0o<>dWE#ryzC!Xr zg%B^p3M+du>a=j}kDEfFVa=~IuJ{05K% z?rH*Tz&qsc85}|YJRPr@TX;y3tji{Y+Ng)MBq_Iaa(}0>?FKxA0tyla!KZ$~T*M`j zX@{K>vmqJpGg#6DRm|C6p2Y!%*Y@T8%P80^77*feGJ5O+G$G+&g9Lk#4}O-#?n@En%z<>uCIaH~iev z+iTrVJEYWs;{&G$=nE8Gb||LP9Yk#I|k2-Wt*ATaW-@bVP_5$-9AdlIL(`Wba!vgF>@E7(kG5{~X zyqO?&)Groj1HXOq3%~J8C=03eahP>^&xTDeY{f5tOicm+pxONr zjReP6A_;z_-Jt;Zr+*46;KKiq0RPNm@CX0D|BHR+e}Zkk3bfzq3Q!;b*#xiyz|BuJ zAm`UvAhZ8~_dD_TkP5D>{oHbZbznY9xc~gb+S--xi2i#gJXgr{{oUOa?sv+5y8Y?a z4_d+e7sro~0H$OJO)XLyOd(AfU_(*>1R_5Ar7J|dtb@cege3ROH{bB1P}JKzn+Lr{ zp?^$q5a6`aJVvt%iux*^&($RgJ2nPyhNixUYSfyc1P`S`Xf#qavV+Qcku|}PYF{zt z<6TPY90tlooC=WcGl{{}o>2JJr(k3#aQOF>_Cv)6*K%p;CqdlX8x+)rXW;rgec#%$ zLR)s!Z1+Lxck^w*_~hEg${YbQex0GMW`s@s8%M5#vUs*opm~`ZZv(Yt`57vUTm5B? z37Jx`7P|l)JOxp=8*A}wA+5U~ulg#q4|e`M8vP6AVtgIS3vy48zj?^N2H8ZvJ$@o2 zfJ!!O`FZ<~w2x0N8P2$ggyrJ>tt_5#w?*?o_b)Wi;3*9)Fjq)UH@^SHJI~Or3yY{O zB?ZCdAZ8N2pEP>oo!se&`sxBJN&vA@tbXEj#zjzxRmN=-ud#G*B z>Jc!WEf#^5>fL~hfvzacN}Cm?H8D5~YtI0erpX3?j@fZKEvVRPFJAXsp zcMX1-hNCI{Mp(B`e8_<^l27ot60(_XwfZG?Y3=DJ& z0(5IplVm$cvLEmt9sd;cerZ8ce-_CO-2VjsvWTHrA^d?USwvO<7T#sOz2W;3{X;`B z4my7d0^9tb&c$Oh3vKlj?6F59T=?+yXYMLGkK{fk&)l7izpZSQgM@lE4r8!1l>6wY z2LOMlSYf5mbq&dRXj`#D1bdjs2P%Z(!+O_X6C(o!DtcV^J7<^CCb3;kI~1B?QA-~# z@b2W154wPbTD#~L{hI(2)%HsO=@Wm(Z?VZ=$WoRaU#L|dV2Ee$CQ#+_bNFMRsCVZaYiJ8e94@!z*ZoI#z49T+EqjA zsht~MM7m0yn#TQ>S;(%=X{DZnpfs8%0tZrDc|eyq_)}B2w5aIMvK=@WjXk2c>Dg>I zG-di`?$)UjYWL7-1FdrRt6jzfR2$}woY-oSb*8>s1fyXeb{F=NsNCjy`c!WueOYuk zBw6HRiBU?Zn)1Q>aPTERF?2Hw>B(HYk zZCtPL4Qd*2VkjJVcx zjlZ|fl-6?ZgAaeC&cXxKP=adDUQ&Qt02=@8j&Qpl$@<&A{`p^~19--ICIA4D4W`0F z2p%vrI^c%-{ZagHpjCTk8!P}O0iQdo+0Hq$6Ldj75rUyI{f&O(Uk_l=KpKGG{$C-W zY65Wi&v>`U_O1S*Rk=?OML(}$^b6#3{a25)ptTdgegDbjzef1e|9|qyy|vld69dHm z=)i^BCvHTQLC65Jv$sJ4(%J4=kU$y$Zhm4O{GZFep0U5&VCX;@4md$~clTNWqj^;2 zfeZffOMYJt(YjjPcJawM9X!5IcR=}rM&m-B+Wp9TWx0+Ca9FU#A@n{uFy(u~6f9Qh zTYdOIt-gncQQ44ani*+;asyJs$WWP(jubZ%;y5G-W`CAjiDpfLC=-Y8yTQkU$0Xk)9hK^kR44Z}(an8}&3_y9GWr4% z*ory$;|WI*=5#!r?FEuRr8lUAxu=ujPvEW}GJxb#5I-b2*c?EN z7y>gd4R6N(5%9!4a7c*1O7la1fS{4)02~jU%LAF=NQoQ;G|Gsq!@TZOiSel z@CQf$o?<>u)&a>UN_zVC0~UZ2Ts_=sWvZM}(FrJS#|-zO=amI9>*NJDIfq9T$1a{A z)A#4*g)99@KvzFOR80I6on<{$j> zFaPdVIsiGoXVmwC&V{-&8DP4;o%?_D#^>1>rvBfs!L<(^M7IYcVUG#Gb4C_`34qx5 z*!L^2Y2*hP0K;GZzi8zrh=ElJz~23yFT>3rO#vnN5ce+r+XeVqzTeu_DMF8PFiKPvFD))xfiG;zFE&CKi%<|tWlX3aw!%8578&)RKgQADe%6uRDypTx@_d;-A+(Y* zdK!_8QGx^yzzpH2-%JSrZ>&8c?isHSh+P4#Z%{k_hMgsdyA>*Vv+3X`*8 zqFwBpT_Dx<=IQxg$1JcGum2OX_xdj$kuQTJFJAkt_55AwLi5SD(ajx31OU$D(K_-5 zoF5>#5(r9Lqe#DzzgO9C&cnw~pV9odDod*8S+WP3pd5v%aU1tKSyR3~+T1old4u zdu^VdLT>0ffKfa!pyfdNWrrbw)*j>`GRz_PwF7}GvI{7a9-#T+WLMiQk~$9n|8HK} zyXEVD+e`_-9*~{Ek3QV{qeCxo&V2p+FaPonpS?o@*iHi2sX~j&tcw{QCg5dzSR!Ik zm`nd1m;xjKltD=X^c358-p1u`7TB=;kkY^b{_|C25F9dqw!8xW5#%3%e!fEj5DMV1 z0uYt|_Wz6k{+9mVcWeSe|4;l|1#rEq01$uxQ9+A;UIu1q`Y*T@{M{ND;jHg*P9PI7 zyPWLi=5jqy{Eq~{o9Wi6qyWJ52UGMo$xH+OP_xtva8xJ-Yy+nFe5!zTnXDXm?mj{S z&NWE5-TP-fa}S* zcq827sv_}oml_(WGO&2LLERbry$G^fnXv}G0Ri${O>aBpIh;P8b$mcZOgBP;_FF9w zL?ihP2MB(5LF_1z(po>il1isYsJe84i3J(R?}nKJ06c}-pAVdMzjr*Nzm6|RI{H@QYEomW zc<=X{2K_B8eK(Y-g@fRwF^DX)_@Zu*s;?zgePxS`hJ0~dPLqEdatC~j{&Rl{16D6s$%=*ueW^d?vHf7m&gD=vf6+z{rAFL4yc;S%X6PFZ2-|AR$9bp^mGAQJ!awbj3; z`ZiJnl>2Kn0P1G~;7JW|3I8g71j^5izyydYfGREMJ30Uk7yp2vevg1(86^!k6p#x* z^ivg#-kx=3Xx6AE+*$YoBLfX!0>DIxK>$b~KjCCU0tkS@fyE9!Gno_!Sfo1m%XQ~g zD~Ucy7p;?U0LCi{-%a!|@`LA#cikeCm0F%%kiD7k$-0!Qp(EOSIyL3fPp$6t+8p1&8#Zy!3Y_80z3MM;x16vJpG1+ z2AUZX7ZexNca+PgConTSLZYjgpa*4K0c+4+FD|Ipz!xxwIs5QoMiUd=<}-j3h!CfW zbRc^7EHz%;`T6H+&G5uM@`VsSOV8lgoUnZ}CMTFrhsP!X+4yG!kYdAq0JEPC(v~2> z0>q&1h6Vw`=Gt9S?eyFlHh2$$rxE*5aChn8HN3B`d0=t>o|jx|fit&%Pb10B#Rc}T z)!^^AaPnjm+}lBJPc|mF&PTg5(`R=hALi{AWRd8KaOwO8SHy^>$)rs}#xT(Z$%bCu zZ7g#D*aK+XM8lBNYa9~AL|C`%h+_*zUp{;lf1JL~FAHI&_KWqW}3Ox{} z{sw%M0emF|&`ki3)&Gy3{~Prd0g)w0nE&zC8X!>ri2Lv40)A%#2!|@)3}DSqApq__ zdVw2%&EdcRobHewOaOEX&;|R=01g1D0YC%z2`^tnDQJo@K(Y@B`by%)C*3591Xyf8 zzxn0GWkv&Ek^nNdmN@5hu+FB+81-FG^^I|>XSzYgifM~_YXW3thi35HRXdJ2;(1}g zpyj?c^d+xh^;nBU*g^chQsWPR+zdbNl~@KIJM>y?!|818VZ&0pnPQ@2BHx|7d<4ls z)%Mj6u(9<{DML}JEIn|IUG?7vr*rYsHP^YA=|80IJswB#A6{+==K+{ZFcDd~aX*h6 zR}X*gUuiuskEjAjLGJe{4yH_xCZnJooRQul;{q78loX?Ig-3(CFO%_cqMva%J%sX_ z1Sg$2IwC}MR);_6MZkXJlcy}E!>rFOldI7Ch481_#`$z$#IQBMiNPE~Xv?mG;Xf}n z0V!GyUdN$MB*sQ4EHeEK3D zI%9!XN!Qrh#~L5nGYK}A2a>=!Ba$WOV#P86E~f|95ZlBU6VJ060K|Bo-C&XB+*3+$RneoR6b`6N7qu6Vx@1t;TTq ztdpAyIaRj$FkhyWImQTWnq9$cfd!SarD;cO3`B0Sak3uvQz8w5PSFx5bUlz0GPf4E zm;o_LlnHa}A35G!Wlfm^#I@KiE+vWOZL3P*sCNR|XY`MTJ$U^!sRKaEQkC%s1W8vT zHOvBcKHwd|Bmu3uhc*N3`r@F)q|eB=mj>L@Z_pilP)b*Yx$vpyvq5BmF)4L9!-A5V z*CmKX==~NE8Pq^wR|814Lk^bNAbCF_IkDv{+N?IR03VIHg#C!YJJ9D z4f~zYIOPku>f7YyPcH}dM(J03hYwR1h(FX##APh8Zmwd&VMKEXvqsJm_m2qe0>=Rj zWKb$Z6c-m}cu3LO71dWQma_C%0}b5;1QLR;8smj)QUz2K3adlcc)3rhf6ZM1UheqY zWF1qF;v%&yBbTUk~>wnx?;vlB#tsnwYQveT!upF*BzVHlDKw%`%8X(QMBUI+VE zDbIwAG7E6Nr;UHY``J6x0OA1JhS>jaKwEg*hE2E&zwiPh0t^Nj3ljflq5&Yie^mtx zHPGKC8+iSPJmBcx_$TZ=!hkLT$N&skKu-W?e*q4N_Wof11{)BMuWA1&2)-i$a7L;3 zFRV}r&;yJPoERv~4q!PuFiYN`d*JeiSmkSG

5ef%OdjIsHld{01jZ5CG^B=oJbD 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%0D;t^VL%M@`Xd#RI!)$lD!^OZ8qb<2I z?SGkt987r=M0Xey97b6nk!Hr{k?E5Py*qWbmKsZqy8(#7nS(&cs;h#CtHbRp%{}5% zAr)8p`wL`%1arlN`vmm%E)_nK3CMAWvfedj$iiZXhcD)Hw8PCVE(`+mQGQW=AE$LE zfCJY?IL~O|pIJ+*Qee9SfpT_#E;P5?ym?b*aEp3k7go?32LoW~eo$i$#f9zw%d+mS zwKKhs6kq21ioIF3eWdM(+KaF6a+}wN0r544d$ldI%k|UD7rOTBaesNw;V!6rsFzPq zF3~};)%h*2=K_SFy&yk9y1Ej%nZ6!=IBg6+)x7>kdALsTAaTUb_x+~Y3m0Aws=&Z~ zDiQ=s0KEQ!o*;4}j2SM00A5o;SoESck!Y?NQyS=DqYKg`A*Q&n3xbk7Yal>PWSxfd zD1YGorwIsq|HSKAG#VyJ0Uxga!4K9M#V!Lu0-y=X0AfG^3J8E<{_NStXIq~!31C|Q z{0~Es4cjOHh<|nns1INRNGuBTe-*)bt_lHu4*+rk@E;)nks+W7z*}Fy-}sNhKm3Y8 zfMqzqzd%5c1ydx5D3%Pg_8sNFRRLn4D=WYN7dR~^ZytZ|Hh7@bz$hSq#exF_=7Ju} z{NxlgFnGWu0lLqm1Le-z73eJ@2Wd&}X}JIb0?XRdt~=@}&LwDUx&Lz5x4sU|1T^OL zTK?D#R(CC3@s)) z?C;B+gf{;QmV9q5LBpARu+-iM)Augyw2q+~zUc5Uxqf9}nLT(_3ECUdd89I^&WU~+ z+dLz3=?86vW^hMv=_a)E265;`5zG&;>S}G=$#Xd#PdAJsig^wdOu$I?y&SFwMl__q zG)a})$jzda7HkeVd0L=sJ2oFgKfJiEb{c-yhamQs=+n9ViEw1S>RC;OuB7~kQ*4(> zt@*R5L?J887b^iO%qAGLB>081URfCyx9pq=s1TtF<6g6p1Ou<$*+v^bkj4dE16 ziv%LFhX-Aga1{PJ&%5pu5i|kAEdLSDCs!Qivs%=Uzd)qlzD@j}ym|5@|7;*kA11T_ zfB=CZ{CW%BL?_eKsmY~=xjFNXTqzqj+P9#+Y=J;Qz6*vw_4g(483L6AkmJq`dovtc z?Xa8#us7m_1xSZxkYf+=QO^BL8bam<{!d5(1s3w#ozO&)|EmBf63%c5FAd}B0=~y$ z5i>sx0553^K&HFIY1(z)@}DU{beMtQelnGq8&gQ`fdYs605HR#5gdUM^2l8!8lc(a z`7=B40OCI1M5IgK+5V%mA%T$qe)U&>h#bJP=Yr{~#!rp_#>bEBX9Nfbc+*y4z-P{K zf+FCC7qt?5=NSp$hrdhwKil9+fX-mm`M)E;aQy=X@*cjNqW=y6a0sqKfZtmH@+!QR z1xzs@8<1z?-wwd@B`5%g_!kkpBIh3jz@=XdfxP@70j=ET|MmoTKiS|mPN<9p2>UC> zJ@-G|08N3=)I~d+WF~X~FoZMxiFikzyPuku&rMSa$aL?Ah1^|${6+=9H6VW`S0myt z@Y6)8*^-}KZeIj$$H=!L8=v{xke59j0uP+viq#RPAZB4>sQBd85+n66RAVI1_^qCW z8D=f}_qW*?m&#mK=r&;|%C`kj=E^5L-1bDiW5}vv!Z|gXR4t?MeKERpB*_|3N)jNF z9aJ#sSvdQX2zIdol*tq3P74`Dp=W0WGNGUi7o=XjIMT(Ni>xy-1O)7-FbA3HPBc{d zvn)>ZyRRRo)pr}XGfv?Q;}GH58;kWw;6PNhx@1h8Oc%V%Q!stB-DWwJUj7!`27VVJ zux{j{eKQsXvU~ZoLPH|WTT?}Ufxn4Yt_Em4=@{^llz{g7$wrLAxm-Y;C_8B*!e)A9 z0Gl&dzb@@T-lIaUJ17h`IC!TCK*RxW>F5GPCWG5bJoEG3B znk7#&fCgQY5@BfPBrl&wwPZ>JUZOUUD#Al(1>r{N#%%Gizz@rI{@~w%(*V5veSC3` z7MW7_34eJF?(1OLK4%*vR~X#*^ij>8CTd{~$?L|r2e_U9zH=>%?8NTK()Rqc82qc6 zR0dK*7*Y{?MEg(#MK(ZPV8lfk0MPf7Boq^QH6Q`}!%tqB>QbJ!k+^@KT}4P4OAvsS zIDAxsvaW4Y<>J)&L+gR-E$2Df18+bNZ1?#iWB`6^-MV$Z`@y*~PVr3!a7U@o z@6_F)^b_@^eN6SAgYHM`+?ORgNDLneP#+V2`M?-=H#h?Z{7SAaacg28I%6@@cemHAU@9JdU7-I-43J3z z&#!}1F1I-TqcCC=sZEyJHy&L)VOJvKiv>vF`jJfF9l!;YN&tPmy;u zL_ePf=JTlp+p5qI+Wu}EW14;%{_M<1J!32Heb9pqUjDqu$7!9eCw1gSI<^0h=Z=Lh z$2A_y<{;d~zio7FYKaoXeFSzI>$_od~^?3`#{%V!l*7HL74xDxJ+@jLNPr z;`T=%YJ=Fo3L;kP$n~7MJ9QIe;9;Wi&K>?j*7^v3J^bg(4PAhJQu$OAc5o^tQo^j% zI$wckN&7*$(6luVPlp$-d7_y2KrdARWhJPkqr=vZqp~x)pxp6p3h(kWGzB8wNdSxh z-5!p*|5x=sWqEn!$}$VTABG)%WAnMs4?CJ^RWXDgDk!S5rTei*+py7To#Fbs7=W-3 zM9t0u|DU@hCkXgMb^(z4(WO<$Hj`{WGWEgPGcQx(BNgHpFl|J(tp27iG6-2?bC()t$^P`Oa*o~(JNEK!aiP_nLIRjEqD!%S|iOuzzqmc3>=xzMi2XM;8x z6pZro2>%Ise-?Q^{q*jFJo+FU0BS&$zXQs4HTcpsLci<6DbE;sup~Zot8a#Omk^@1 z1}$!{Fh1w%2MRCNScEzebW`PYnJlE+&N-kr1IOTt)hPUwfTkpu{}{pFMP!2BQqmCr zisg$|G~0Nx9nU}zJ*`}^GTyj*@1EB7gegYgK4s!F?-3A$zG4S8vIdI9#99Ft^m;^S zuz$PihWMmLTth@)xn*-jz)C zbkq(Kkq50ODD;TiT}_=6?h9UPBK*T3U|69}Bk|yDeX9tHj44t#(7_`>;8Zh(pt7a8 z+IfW=Vdxt6aD(OSajhc+gzPVY%-WC%0B11-hnu)p=<@Xp@W!XuP(nlzQJn%=G3Hl# zi3Q&ft>$YXWUYOIq%9a8!dz|+ymaamyvJif%Cp@ei%g43`+j6Qwx9*@{L5Q8(UlF{ z2jCAE;LwNXFCW?e2Veiz4}ORyIIe&q089<)IY-bDdG2{W|2KYj>qf4B*CG`Z-nMNE z|3#m_^NiBp`QJw3-+rQs07Ow=tpHpD@)gJMHvtg)0ROyrf(|H8Jo^+D0fQU>%lQ%b z7cqeX3)q+sNkBV5j;~AxzOzF4e}U}2a$8!!D2(-iyy4H_J`WtgS(7Zph<}Rzc5na^ z01a=Y0lo!m!`(~?;GL&ZqzeP0JIDy|=w0^>%M@cdl(!M%ZVHPd80;1L7gyPu)fdx! z(1{kCJcR*t#0KEOQyf~2%Ot^zT0?D@>h@r>^;P3n(q`e=-E)1fGj6Ccy+6-%OF72bx^s)>VS6tt!QTYat797SMjUwp^=w z2-Rh|gIGcIs&@{`-@~h4Y@6a4V~isDl^kvAl1Q;2so3uVzm zBq6Nm?P)#K|88$@CQA!8zMAQ#;I+vJ%96JNIAZ2;o!=|xn>MU!oAq9G$)dfdgN7RDfzEk^TX<>^E=ZMl0E@ZKWCJDj|zpM z0m#(ylDfbO0~6Co25_VAyG9nHt(-pq$cRkvKJ33y65>7Hs4M0t?}PO`IKK}_XP{7F z{Mz0o`UvQVFi#^rKk45`z~p2*r7cY^nKs?fKu+AfbDJ3zB_JF?H*EyaV`RSp2yeTl z{wxHYpZ?$vzJB=!V1VyzBfwt*m2TC&dg=%Sp-nG<4RFUl3-K=ph%X13nE$31xBk$I zg8Bb8phFP=*8u)00s*7|Mf<-|{C{r(T;2TV<=cllFB?34>d zvdcbQ#4iA04LJBb7*9p2QEgA3K7=n1)rQ_`{}Kdi?F7;MVd{7NZmE~yVk<}-#WLng zh=Z9?%APu+-@i0ouE9-km4ZPXDDa#d9S0(L0$12^9FyKInV+wVeHRLSDJuZAye6pQ z`Q~y%N{=VZ6<<;7e!&!H&-u#@F>ZSo?gsJ@C-OBo-4;tF?(?zCxyh@C$x^V#nPL(vHl@p1S)W9&sUT*tVNTfqy<$7{nPK#hlu4pgOXQepKV(JVK?H=-WGI9Yk@|t=4&qN_&xd8{ zI@Cf7N%9B2B>E9JcOxc&6KEQ6-)Aj=&_~NZ&K^U)zXF$tdWKqA?P29@<;cN*ya5~qPRngj&Be1 zx@)@>3RDeL=Tb_6H{Rf0ClByZE_|qce2#6uN#ekLw8Ss~`pq}q)d!S6g@ITT<-$I5 z#p^e(+-N=9NXnl)55outAd;ls3~m%KF-C$84ZrX+16@fkA31wyCrdi-z~3jkZd||V z8Q`5~gBTvGJ1~TUjz0-NB;WYoBN9dOT+>u^s6U=03+m&C*KJ$fX|T$FbC=n@oy6#7T_MRpq~=qU;;`2hS)cf zuY@_@J@kJ}34sWNPa7EkUeF2{!5W30TmZ)bc|Uc4s-E%>#h(V2fKdd|#W4v>#9p5Y z!J9ut&%fqQ5aoj-0~_OYs@su05`(@j$aATtyug>Pid+Kpyo} zQC|F!GPFG~Q5X2Fj{B|qDWwXQU}?=XQxxz3h9m@aXKLp$9NfqiM>zuyvIpc|R7ZhnTAu-Wdyx}hOD z*c`QHZMj%NA_EavTJhEPkwmitBBk>tgzBO0ASODF_~@eLMP@8;N*2IN~<%gZ_plB-b}t3^Obw=8f2J+>ZyWdlNZ!Ki8Uds)aPkmb$Qr0MyCVOG%bJ11=6 zJ`g2kV;75aItENr4!VSkRtNXCBm2I-di}qy>Z2=V)C6WTuRd^v$rjp*oqpE&y?W&_kD#eFL00=*j09y2i(d3Qr zM~GuMpnL}?_Er9#XO_+%F+~ymkNCy#N3X2l*22M|1N>6#&(P zBN`9@x`}77ilqQnoxu%%Pjv~{Nnm)#;h!&tenbVR8UY#iUjLpD_shZgOGi+zeNQk6 zir!N08}htT4|q*15UhV?WtMkV;&l7GPXdr1d_r6xKX8wUfDgk0e3HxV`o8kx^C~$4QvHJyW5!7%E3AC3<`c6-(cv zlC-7hj6)@+G#EnP|MW);8kzx%%N-DN2bPnH-zW0L9C{#SC}}LW@q+s|;Vj6F##Ay{ zPkD%fu1Eh{MUA2FDyGA#eHtbrm45*@PHYQTAL!!`=8nd&E_qABBs?#+i|8?<*SL?I z1K6)vhHm>v9k_x#LSsVM;VRxsR`4b2?VTmwL zQfs-u$S*&8;q}4J7F%_|%f_Y4IH15$)VA1hNrd0GMP-_XCjA%tXVDQ=tcCvv(mM>{ zgWTXz55DXEE@HdxXztS?;*AB%xtb2fqqDWgYnw!B^@JF`Ie2j1l0j_{u76&U3%Jx@ zdyU2)Iu95izbm>=+>;V69nk92WI&#wEqLuWG5$A4kPF!Q6K$aNzpy+Fa{D<&%NK{2 z&?Nen4K5fyYRAf(bj*)_8_9fyWD7PFAJqqH@Al{X0n4P4OI4k!sAmpl17Be0&JCD= zzyyBj!yj$=!3*Bx#y>#sk@_P?_V3wA{ZE(A4gh~NsJ1Hvq`2@#>NVj%dH_g;5DL8i zJtlyF0tP9-KMpQnywCmr=LWrpx5@wj07SPx@oxg~J>Qr4uLg((fPwEUfESK$sR0B6 z$Ohzq(c4#G2SM`Xs<#_};>GqjNMH-W??{llKOYo9)CBzZNWF6uZo#v-9TzWiWY(q^D=XLscy7a%tyW4lBAZ(F0Q zUytSwV1XNTYy+xcnO6=Ug#d(((%JgMl>JQ4V1c{$3OwQ|-xYecXDNAZB5qw6xoUYkNS4@G^r4Z;2k-) zoU8*oq53O~JwUdA7^LH<56fmR^0ci*I)F!DLC+8AgS3O;b{%~#)j_uVE+0rP>-L(}npynW^3bl2|P zI}e>XbhQ!pA)?tidj)h@2y!8Q!u7v_cHi(<%+z_5FASwrprR;6 z^F=`D%LVB?Isx5m*Y4etXd(u!`F*|62qI`bA=Z{1OW=egC}ec~TP0DIirrVD!5R z6g0t60=hzld0Tqml~I_0T;E{-ZUjOLA`=?&2!gRCr-#{COf zF!6*4-4La`#UQV^v2#VRp(3AM%Sw*HuVly&3$Ms|>1zwf2Op%)@qw=2?f~97dVOw; znffGvdVAjtRj+^YGav!@BxQVQ+HWZv#9iJtGo(+x(ob%}Q}u)Um(ju|Aypa(|Go74 zwCLjELi3t(R$$w+%A>PAvgSQ{-sg~eO`&e%Z*L*|7iaKiQxb=ClMiS1Pm>g>EE%T322zJIR$Z03F=o+Y=@4W}h5k+vBn-HPm484~Bd@rN=jg0QqGyeSJsK~Mk1iwPr@G;L+KDwK| zbRLz=KivLX{0P-hn1%Ln~0-j+1KMVle3C8Hj4>Bpp^TwWm3rSHB>xe=(FyNw0rx5;- zaPldszzRzP1#o>;tYovGhPY>~6{Q!C?7zU`HTQL>_yo~WEG5l5l+YhCw0ommyp*a z;a^oBNZ&_4A_pT1JH&so8V}q8SdPakrCR2L0nNL_8~S*Hp5~vvo{td6y9jI)!UvmP zdF$k^SKoZ~KY;lNcH`d!aDd{U`<~=MkDzZDMZ=|C#|JNrDrb#4?UBhnxGJ8VoTOes zpTHf(M>DKSmtYu9C}YG1D7o%EHX5}5Q>iIcO7%~j-??F9*xj-8m%m5L7WTeWH#_$D zDc&=v9gIBzW<^|j>Vpr?{MK*L;9o@w3|`<%hy-IHNa@cp0|5L}=(7O8Gwy(_5`h42 zWH-PSK-B=l074-UHh~=jxD~`QAd;ZKfDQ>FZh*)ZynrkGo}6HJg86KzJ+J|!_}!DzYk$=nduRkhrEyL7Qe{(}5cOjnpPj`LL%ZLKD(WwaZ<32A77}!zJX<&dEdsyy+ea6qVRFYPZ=&M|WQbG?t*)=I7ag})=%&}AZ z$z~z+z4-}#BeK$O0thqNyqgySoYc6Mx!m$3-eRh6p?yqqQ4EIjDQ!dIlzPWck^zpt z&Qs@coIOYbwPiR%EuHiN7qSa%YCE_AvoMUPaKLF$HirBK)uX^5h$(T*ISi07)Jcc{ zyEsq*p3V*Po8wup_2l@1P~_^FJIoX*D;@S6=I+Gr2xRq#Gy0^0G&7D z|0((8+b6UVj{sX5rat`1Pqx4UWGrz0{7ZYcZQj9lADM$^b$|nrz$u^!U?T{?a~q#Q z)3ysR0i00=Kp~*27kHTbTLDDyf0YCjNdTGw(EsoR{+~o3jyJy)U|av_Xap9N089dS zfT9Q@{0ER15rVILN3g^9BLh&|H?I)iS8M`Y6j*^1fEFz8uTTt#5YQu#1sL`b)sz9r zc9)&&>lxw}2E5`@1q~=zu`SR66uLzKpbz#lB3PFu*I%C~f8;If__rm6!W$K++&vK_ zT>J@h7(hb!RsehwoPE{~f!unBCP48L%+~dEqvmMu>ruioDgY#wz&0bTlxs$A@gGEg zrhpSfA;kMJ5fonDRU7Z^JI85z48Qy7H@2%Y`oQIsrK=s}dYRW-PMOidcq8lVFavzN zC^n|7h7;Cc}k7FNjJW)Ut&Q zD!|=4bOE)sR2_13Nt&O^&*1V$=cUxG@(U6MtKUBAz>G;p*AOb{USy7X1rO0M$e2QwK-@unq9S|FIo_&%Dd*AL0*oAU6EG8-##}a@jv} zfxY_;b;*F<@u3kQs-*#~asOWt1psum=ZF21xA4DB$v?{btk@tEA{S6r(3YWpg} zR)|Kiq$a?}EKdGzyv70F)5qL4tI+)ret8PWER82IQ^CjSpbo$8ctS1)YLY)V)Vr$m zESR?Ub~`|03_4(kT>V;_vCqVw_ZcKeT3<^46CrgUoH7G9y-hXGlp}i3;tMQ=a zr_!KkMU7Ge9k3IPf06(uMwJXuzHNq$#uDK#!jIt@Dghiwz(F!#NM&9s2bc3#5k%D# zHJRbYq~_;t2A2oao3lQ5e-d*dWCSd%7uXq61TVtb9y)>a{&88w=c`{>Q+|A^jjqJ0 zVI@@I6<&DdNG3LrpCuKiMpYn9N#9Y3j1B_2w%!EF9XnWPf!Dt_z#BMz2zy|N_4{1> zo$_<&h&tVcA}zemY9K-mx487GBIPA&KDY8MG1NBihpq?4E#T1ocu8lVu}bL|6Z3DqrAUCveDM2uy03OeX!oJ}kpc77e{%#^kjRC19AsGf@8eH0%b1%61b92g3) zPqZ&%EP9e{r{=iA)femcN5ntiKMMFN@V`wSdUX5aYT_HcCyD4fd4@Scta=vNZ}q3fE}idEyn-XUkd|z41Eg$p@8ZF5C;lyQ2z@> z00CdU`+2TT|HK4k0P;@A0s-=K+NU^}bwA?1Rkh#S5fT6lz``FO;1$(D83q#j^l&E# zdI-T11M&l)y81!#G@wkN9lji&iiW|OP{sHdLndxSo>KI(ir&w<2)x$c51yUqUl)@E zB;XfoNRE%Iri<2IhRJ~|Y}pEV86c(;3_4ZdejmXg=NkvK%xWTL4A3_EJPH1mp6f$3 zshToPB~u>8#XMxdn}G`MD$2>_G=@^Z821yLa=XpbME}@=7zAIhua~WRMQNc4fk47Q zI@mP7>y>KIp`{J;{P;UwcOCB=!N#__3^&&VAo`D z^N%Te?q%-;F&CJ_@f<}%$-!E1^c!(|!qQ(gUIrlPG~+`mbi_v!FnbDq&Kz39aE2)J zVXEw8&VC44P2_07B3PEw^Niw#W%ZAVf5}K~N`mPwD05&;b`e@6l30QRQ32MDs~K7e zD(2YUx^(PFrn@_qc5_2@vK5-%wW&;=Ex+1Y%75*DkOSA%leUOg9drYHl<-KgXUGqi zv0^q29+Bo>$(Ypta8@E+f=bW{0_g!cq*gk~sYtGY0q7Ih%yfuP8=@xSbGw&e$4qun zs8LU#<=*@0={mapYao2rB6ZH;`}F_U=4JF8Aoi&PsP$hV1%!1zR!JcJVBPg*fc|ay z=P!lBWq?6jcRdsM{&;b&FOiDDkyeIgkjhbA2(naX?da{35a6lB z`lt5e|Fv`5JDb-l)V+=P_k7OVt6v4^&qM7`7Qoif^DVMxAd3DcU;fe8IQ)R{oIM1p z4@rpp!QgKLCnNxjTNMHDH#lqo(*wNlJo=&{fM;j`Kml+WiG~2Q08IX>TY#QO0o>y( zfSdpSz;=N5I`>CYzbEw65)8vaKxg&X7+H^vGFbE7_ zfMdW^M|mh-(DPsHSXbss!M%3=jLJ65_tT>*1$W9&23WGWiU@%8SL6bkbu76{N`J9b z=41fnHWR=;CPX-cFcP%2d&G^+nc=2c7LE0dXwX$VHwi!cg-7~Mp4*3S^1d3(tPxDKA# z0QBS}5G#PFANE~_e`D~CR^k2=py`C1oV>4^G3{a)p6G5!!aPOcizS0o?mTIr879vJ z`B&)QcG1R8^1>5q5XTOG>@1pmP#?4ylFVUwhmB}E0Fpo==!#_vd6=7g0y$St3#im= z_bs}g?S|&4(xf-xeRo$mrIiTXxNjJW%$_zl_MWGMQ_LFt*$e<<>A z@lW+{{PP}l;x?*7-B*_!!6c_b8!kwCb+KI$(yZ@r0Wg&^T8Pqd`sk8Qkd{;e7aLIh7cL4m79b;HOv*YCiZ2;@2q? zg55+vLT;{?m%R0b7>s15{+%7N{ISBS!!xH7jQ&_FjeHhpSoeGv6WtT6wK8B09fRw# zjKxy>lJdeDk&HWZQ^(h}RW=74Q#3P4^YYgW*zkt{smFn%qhsyae&iCkIwhH<@;p29 zqdTayvya01Z(HyO@XB9;m5(KY6yYw%?J)FdA2?m283di(9B18@EQ zE(MUd_`UfdM;QN|ErfhcDXcnC#A_K&UK|bhBynHp9kKUtq5&`3Q96GF@gLfWP9Xu( zgBhxQe18{YDru6^XiNZf=HEU#Juy!z2Lyz_!IH0!$;YC0mhD*s^^a-hGjcX(0dk3v)6PUXTPUf2V)c08eTEzxkFT)fxb=qwClE z|L_dOJQyGi01>YXz!w%0&jA8h2e3Bb$2YS}1O_MrkoVSaer`KPBgVf8;EWhx)ep!) zASW^dF#`Yw$_6z4o&U@H4Qs$v5fGaI0>3%|Q~>OXFBJId{+J8&|ANay7jTajk=ow* z{wSxJU+|0`XrAiug3^^?o=Zw-f#zCnrVtoK2uiRI7(A{3o5QM@o~}~=;zja%I`6iS znt}1FSW0^4>LdeQg zQe*Yz(DzvolC1AOH5v!FfM=GwL3N;e@xP10$IX*CU^9L}0dyDuVBfB{aN1QWgctn) z2kYn!P97)PFXTNdacK!Fof2i>QpRH%7AZppYUwn%W+FHXF*gKzX|5&y=>=~<8`nXN z)lNd6XUZoAv~uv_hCF%n+0mw5uUwkHe}}jXgx-Cx;tl>5mSc5ym;$`<(_epsRDbOf zNT6##qyQ=lNCVIXST=6XYgf_Nexw!V4QGF&bjXF6O4BX1@%kO4wF;59$1d)Klz^@)hzC&EYM zTmFj`#q%vI`Sd9N!>R%>3Z&YjL>{H^YZh0AFS0^sS)#yHsDiz8hfT=E7Bs$NWayxf zr2ybzZX4r&dEMz;5gvL7!(z_JVP{LCHH~5=wPjFV>kl`VVMav zE8HrdH+V_otQ62)a`~U`p+R7MD~7?VL?4d+qo3ZLG4iDj!-E4;bd0F}WtA!nS7<0# zTUOF{WMRBs9=^`L^opgLvc0>!^xBR{&8KdH3-4$|7G&&~WT#KWpVX%WC4tV2O++Lh zX?Pr{c?9YBdMOK!06Blxzt|0FoSZ78gyr3qw)f``)BDK(9a}o(&G1D8G(98(EC4L? zz?uR2pvRa+rbv ziz^XK^^Ze}(h69WS7t%~*)gJXfK^?lP&{!Z$u=C?-ViFCftmoI5F?<c;75 zs5sMY(@@i#Q&vi9kpgP~69Fp1Bs_q4h~dvl^6<5$+LoJx$DtkP``b=^?B#qF7N;nk zh@V+Ae77G*@5-~3W{Iz%SHJ!LV+Q~L0MUsOz+B=iu}lKk>z*)6!|;MhCfxlI_n!nn zWw3$i;HFLN6mk0l0zwJIY@D;@-!>cnRYMSteLzG6avdnZfV@Ww@M9A|SOcWnj~swV z1EK0C-oXHPZTQ>sb5sRczBk()VSofYO26&^>G>t7j|SL^ao)^hg3F%=#0mvKm=~?k z0I(qhEy{H_b3dB6LdSV9By9Af(G~X}EOp!3W**^s2^2^HoWk<*touOO!n#~B7g8|? z>F7cFu6#B_q08e!jy=)OLfcg;-p~O*l=nXq9Hh!z|6JShN3Gm=?SFsy4nrf5zd0$d zi6ISzBo0awWJL%NfR;a~(dR^cfTQQB5-DtXPeGcxI^f<)MNw|uJnOw}+ZVWvYg`~k zT5dw70vA{ZycvkT6aadX#1kW|9ZTJv0rQI&Xx!_#2wR!W>W2>>9_)xrIzs2;sT zm=biU51*vUEZ8;2Qn{K0=|iNU7sz5^>6A7)hr`9lj2|QFa|_>mvnsYoBFpajk9?-D zeOy{`24*ofLHim1yN8ANs9M6RgEv&904`+OX@b4>l>r2}ieLm*1D6Q6EE2i{?0Ta5 z$p0}Cs36}bn`>DOo+yL%Hwpgf3Bb}^-5~LQlJ=-Gt|TZ4pyh&dpxM#9yGf(;|3iSD zX{Q$MF!4C3NvB!{-#&if!b$6efg8(Q`_t$BX14ub7&%76H$f{UDELplU3l{7$$vk( zesoybzIlTG68!!H!&DCRm%vVS!+`y?|NDsh;2oms>x9ZIHLP7`bb_5$JA885jjPEc zi*4g$Gkp#iOQeY5lgD?t2|)Bi9RfQbh0r%rJj@?IX`*5k`3TC(j_06fR@DID?IQ)WNIt8!=_%A`Hp8dLe?{s;1+vhKCd~qY1Ouj1uzzIIU zYKm;N0FVQ0k+5MsMx)9MTL5r)6{&$%p+Fh|Q-CEv)CuSYF6+J7TmA zGr_)>kAC!bXpkTfO}?OExR2zu1*$8%-mPq}wGsT-`aPL*wB_8Mt6urCvXKvE#t9%V z`Fw)mKyd$58x~$KG+-tJH1aB$0qDW8adsImGUpig>WBBO6Fx2YLvn^l+6F+fZ}!D1Osew&_~)Ed=EYZk#O)`ll9KmepBh>H!xec%3;;ovCMK{MlkJU|bv0$0Bj)&ZA>=>mGL zLt?kfbsr$b+-bR+QGfz=h1qq38b0d6OdO)C72_@))28wqhjO+0b5{Buq&!{I0Zzxc zqkNRgO(F8B^6#)55UVF++!{$0Mf#jYyEWKHfYd`Q=;;OQr{AaU2Q-j`$}X7?A^V46 z*AWYSnOm!*a36>ZLG%F%jW`~`&H&#k9=04X9uobygrl^ZfvqIp=PTlQX(3U(4$Y?- zTTMZ5PLU&^m+OcZQ1@s>Gl0rb(ir-dwior3GaCIRIB;hIK@jDC`jlH_Ksug}{imO! z9bSTEXT$Fe*$$@|P9cXMCKRKr`Wy`zJ%AbhkKhG{9+zZ3bbJ!v z(@Ln#Zz9Xz90$ZP=JQr~d0-wuK)QkBH(P4Uh#iCc+Xs*bsNu_TDg*_9PjvTBO}DjI z5?Y&#+WV!6e=C_HlLoZddSO51Pw>SsJSr#>%8>no0Hp(@>(fQ2k%XhsZW! zS5~0FuB=e>A^S)0(-@Nl_}yAM9*VwMmcC*BBlXkTPE1`OAFw>)ZZb(vqB1lmU5rw* zYe3A)nek&?r~+OMgL48PkmprFwSflm)8& z5Rd|BA#fvbeDFF&!SPOXoqa5?Ep}a1o(EL}G8TB9tbN1ZjbW33$JQm-&sXRF3;^3? z02uqevi|qXkpX(9ezau^wZulbekg<405}SC1RzOpyGwwCzVrW$Apxx4v1!NVuN?$d zj|srY4=2(DWEdz2Se*w(Du6~lc|iIf13=k7zL5v)98moa?f+Z}CKvEuBl}0vPgwj} z(*VeW=0BlNiMQ500PE)hG4CGmP1!%AEjMq20Dub!5`_5g1S?(%D`TKWEdvAbo>-u+ z#R!~&lPzulzEB=XP&QiN3P;MoYUQp2Q-ixh|6fX!;Kha^8zLcuL+`>dlPfM?dH{P9 zL$$A)f1R)o_kO?)Y9aL2*wr7LCYsT`E}OKS``4h?hYk@PgK<(~(>E;gXGa#;GNQ%g zZ`gemoj|q%X%eJ4sspAYxVepVfy^myq#|Ng+{;x9_`pjA>`(R|v-b^uWeK#I?k1wep0PhRMx+;{J9h08JCN#l07D+sFi_e!LX4>b1U-RTe#F`ZG6Io54q8ev zQLN$Wsc6E9iF3m;eeMOc&i37xBj^V4s|UFJ884Us}`y(l~(G~z0aL3Ef?*Ib$+BslY1dOi#51j(i{WAq*6{rdUaIgY$51>i| zBI-!5Gqvv^k&+~o0&-eQV2>|XN ztsU6TC?f$w9W;=TZxsZ1m+c^xL5P0F0(_45M|tDp$1ze%`Sm$YcZ$JqBn}vVLAd~@Z^q0h@>szo}DbH0x6eg5SY zAwZKreWH3{Vq%fsg4#{qsl9mddb5BcFi>ioh*n{{iRcb6e-P!tL(GVfAFbrWVP+56 z=Cf6o34KV%UO>0`FPF!8Da>)5t{}m0%!pMV--$`Zdvt-^`nz-#Z9`%Q+A29_WhPe6 z=(gQ$ zO*HVrmaZJlSk$`*m_Q@XpQ^m6v+Tkg%T(}w!TZuvQ)T0sYMK|Yzj-zT8TS2{%R!Gi zZDigjbAV@jw%D97TO^pz#*bA1-3a!EU*~Ax0g>!Sg@2D`U!@0ZZD|O2pXqz5QJG&X zx-p_E?c7W zzdSDo8|*Xzx7w-!H2^kpzP>#-=WiofU&qx9#qR~;f6r_32>0&AqKHWif0r`H8k4Xh za8j6@yoy>J(2dBA>m23T4lb95tm3MuGt|HO>%Z=t@X$q;1mF-T9s2SfMFsW*{=kkp z27{yRY{)SD)54Fxr~iJc^EtK`v>=Ch0+L$9F2f>Jkpk%JkO97;_a^~R=>5-+endS( z1+Z^32>_Zu7dzd(Lw^p_Ki=Xe2t>Hb?O!$kEC4uLw{8vSU+@DY_C10B`>+Hc_M`ac z?VmaeF#ItDg9jXGe{mcDr~&?VQTEIG9t6+@;N^$RKUx6Ff7So|z)b+xMY!r|`KAAf z0}|}V-o*}pi9gT}A7w>;j#adfFY9@wtDRk;BSX$fcIMgWVh~6PEGZPJ8aB(=W)lJg z7~r9ifrF}qbx8#?G#5ZG$*7kq+tFIXWh!eX&mBjcs8<`L`dTg5srf$t-sj(|>2k%W zx|#5UI;zKbuOSSChk`%iwUSA>TwG116NU}IBZf(zGowluilBNp2yQ5p7Z^EV4F>Ga zh0UlRnr^(S+j%*m|M7d)5vXXYr5Eb{p!Q%8xj#f>zQTc0fNBY0kCPBOByda={*DJ9 ze)s)Hn$9Yti*Y9iH*z=b8VsR)#kG%r_lgo=D98j3k3wJs2PWua(^ISlsOjB_?VWvs z3qdlf3dsVmH5N8gCY1J(@2@&pRapAfWeqWYAE!3{%1yW(>|a_v5_n3@3(X_|Wrg`` zgZ4A|^HBRUk32~p2#p>(LnnzJroSx>wESgE)ImJdEMR#F+7aosX)LWm9{8`_19QxJ zP`C|*K;nDOC@jQ?0TsH;3Ul)G@|41`}K{%ioO^th`@00BLb2uR<~`@T2|D3TW4 z0W$uLeot}!2Zmw%g8)EnS=hH}m3}{4OM6HF0RwpI$2L1<*t~f&6@W_sKmdfl9)hK% zrH_IClNbKa4~!%G-vkf|f)V!b3;#O_kO&a10$Kq?$21T+|67jxc*7C<3r67b0NwYQ zH3jJI1KXc>`QY?yQ2OUye@9e^_-E*^DHyV#v;uLjeU)PX1c`fKijH%#^_5`H zgudljaPav2=AArO+)7Xne}E8^?}V z4CJeu1hI^uF0IGZAH;sFfHEO@{R0BTQ zJ_=$$=?R*ToJlv)uU8jWocH%!CPRog4(#9zbxHM8H-D$p5GLE?EEh3w>vHz4?Jr%u0)@1!D-N zra?r?lt1zz(I5l3)o~D$Abvo5)goJtPR-@Zd~lrlCa7+(OKHEAZnm?*xLutR35t|A zR{z)e-(pg)g9xw?E|EJN0*KGoRj`u*Njj6Jt8ZS~y$cm0syz0){G~XC4aSF;Zr`Ci z?7*=EsDJPnGyPx!w#PCe$PT+h_-)>Q?xmL$3It=o4yXdXhw8R{@b&nA+7NIf-?4MBn7cc&AK8q>6 z+ymfsh)Q?!dF&?i{NaBPrjQkYs(jqZG4c+WCM@?(9L&`2ZD@f) z!Tbu41IMf(zCJhMg(@R3Ky&-$z*#9U2_ZnHmMx1DDLEA;0E}2v8!ON9f-E(kFUpAx zT`Cktm`U_1#XQl3;MD%5#rOUQ+|LIpSR1{uO0;4B4L%V6Jk$yOu(3FVZ=AwyK!zuM zysN=USzSQ_HOVYt`TT`8>~KTu?>l~c_wF`j?G_*yCE)o~_aBWOBCYjMBnRur;DCte z=B0WkYWMHb?|?}eZPet_B=j3R{C9m6vNFw$Qrd+qjssUoC{9vy7 zP)2a||JXYNVUsDB`Ed^=W#F7oZ%Cr9emlu z*(n6GbkMHQt71^V2hBfygzF{Jq1oy};C-rotpI@=#?uOfPgM1eBu41v>6y7u0+!&$ z9vd(?iy}$A*V#Yw{zRG|BFljJT~)eQ@DV!TjygcamV9FV@@GgB2RXa@WD)Q2ZUB4h z`50XdjR`e)!EF~ZnH!FDJ)@~O7zK&%k_xi?jrhN38OL~R>L$kGkO$c=(^ZgRL7eFV z_ucrS@wt;yQZUI?WAj-CuO#g}Ve^nd5=nQdq*M@+LiT|%0Ch;yYipO7_+e+dv9E1% zKUeO)eXs|gZJRt!`5K76i=l*}>Q<$Ru!}s!2wriRoPy;v{&A{(u77m_Jo$2kUKw6a zpWC&|8(n^?Bn zF|AmcnMu)OTqt8o!SBg`UV1>>+bsa_48Dmrgd^Tcq(|f-uya6hc#^+OSvv4T!F2iU zQzr$$ozl+@ZkS(%AsvSiedRWJeNUaD;JSpVI*bp9!5{nr+#+I?7zl6&{@}_)24~3f z>NaBkS?~l+4cyQZ>{C`*-L7xmni7ihOL1NA{~`tu@qeOo&(i<;e}Bzk&$v7`eQgtM z{?;w)OE+u)Q560Au}ypm2SHK+b-<%Q0FZ!i0wMlaZ4F-w*dG|+Uy}p=`XT?9^U7O@ zfI0vu#y^Mfk1~Kh0L3jpmw>{o9};j;0+d|9Gd+OZzaaeW&TlXT3^a}``@a}vRb!@cV>Aga}Dtaz9HkYMF zD+bYjfOUEifdH;9%KK=U$4#!Ga;MI&t*g_vtkge--DhK*rGpc)+( z_c7eeZs%vA)FM!u1O@=kmGqg7Gv=F`o~+%q^8TucfY`mI9#(zDpP-he-=Q=lmTIcAn#~lcy5UkeyRd zRiPVH$C=p2d61ut^jv)w?*$G+UEmQmIsVlxe)4}1?gYBIsm4l>g_a!3i1>9y!<7Ei zaa7F^eg8RNf3Jc5LH>aZqz$bkLa3YrxpdzyvK&AUwa%L)fe)g6pD6Bm3E}TRg;Pub z#Tj6r0*3^^q59`o`dORW-4MaDSeiHy=fTyeh+yIpA%M3sWaKpxp zfCM0behvhX@OJ|EjUx~fe~~|me_uiupdPrdtO3*lD%t>n{yPOw>`(H~EiHh87pOWQ zk@}}A|Fe$%-#E>#&%h|Bo#G$Xj*|cA0Y^MQhky+YUS2@_?;NW@9f}v$M7a4;lC*Uc z3UprNr@2PD=BSN>Sq-RIDkmsFnCKx!GRf+^B}Jakwa=Dl&;zcM%q^c!H{l6Dl^E=O zRyt2sU{EJCfV>`;W}dFHnhSOQK8>E*Eki@sM`+fa&1rMhSe@farpShzPquKQ*K#7o zFl-4yVRVJS7LHa|F&X2=_3T~*H|kB4$60sbZ^7!=!4ryN!8K-A-_e7r;%@!5VlQcd z&q-zpVBRCq5RjZymd=E`A4xMDj79ZjAplT>N!ji;Ty&wrA%vmk_Q96XIBX&cfc#LQ z8?GNS%Fy<6SW$q2;MH&S6LXH5AVoSpky#yrACqCOURJFTu>iZ6=^d|{+z*DpjUrwB z$9%DS-GwJ#n;|QKN~^{+XJM+DpR6)aj`E)$lNu0}bv%9x_8Z z`|#v+4f7JvL#}=b_f*39&49drC}P-vSI`H-8K~esfJj?GG6Z`^8+M^hg;RMC8WGUg zU~pVk&EL8j7XikY-1!{pc6M@ySdqrCL6Xk_esxAA1Ad%f%5a;n)+K(CBiu{n$8?RJ z?_M{gff{jJGKQ+K6$cK{&&%)lP=1eGABuhb|4pMrb!^wwy^{nh)U=t1nKyc|&q(@&h;y+?98~(h6*{(wXgFgZRE`LSy|077uU;p== zJ9lugB>uZ%iL0kJZ(6lMqi-Mp#{io}_`=!5eZTpU($e*`0FQ0lxW1={20#KZi$ErT zKmrB`;P(HWzy9mLjzj=V032U={XfJ2K8g|ml0aI13;Z$Yixz>2`&+>hz&!xfKhgdb z_`?%a8&Gz7QwiGg(*xn|55iG;{y78S0P;}0!u=x^)cah4NDn1|MM3LHKmZ3p7+up9 z2m|Ii$@0|x!c_XzX_xKnLd}#M5b_%LiWw zfo6SqWN*@Kx3NMuL9nIr6#v`=WFcHaYi1_yHpRF~&?u<{lmy+-08_h6Sg7UTTd7n{ zyundlHqJj)j_}N5RW1=VD1c{1nrTomC;!4b!rfs0W=a8=KwBu2C?RM{3S4|7fEArV z?fND1XaMdOb&efWW_et0BvBlK^T7jkmf%Eo&U2H;taa6ixOj0|95b}A)G!I$*e*$hkI_27MzwL9N@mY=t8pW6Zo z4v6>#8S}CPt%o~ zT8ANY%IgHze}VO2Rf2lvn}Z6Fqy#5p(6_RcL>to?A!fmEIMj;?Sk@S{2#|z z8hd_V0PcSg0Hr&q0yw@SAn*+W=*Y$*z%n4h|78F}{*}h?^(AWm+dT7F1rVRU_F!wz zV;dC$CjqRY^Tz-vWPr`G{Sg8vYkN4ZvSV_kYn5C_?=#06gJc z6F}qzq8S*k5e38jM;QRc{?!0j4%Q#bAtMlW@_hmRQ1hKt6o8z-s;)-=27eA#x1m2v zg(y4lXlU|LhH(UV^qmIT*hGF!OEAcT=&oowMY8x2AfPBU*p#?*&K^0%eN{+wQDqYI zv$>>N$d=%96aIHD zb&XUZ<250&&4^9*D;^$H=Ro!wOD1=nymH zVh6E~LeMEh9OU}$e5{6vpY=`FudYUB*!-!c zsyUN?Q6M?ZJZ3oK6HCYUNligdp0ke{{Y~KnW|qNAzmYLQRnuA0z!>-E%t#1-#snsS zehPq2ZG<~B{34)br@Euv?L@|GT*wBG9i7F(u2$z?_(Qfhwfiyh71|&5ViltzpcL;0C#wH2Ng@Z=^GDvN-+W<0kGnyNk|a^ zd)#gVwY!w|u1tequqkk1o;r>?zB*=jm9RZ`*_m$6)7Wl)XA4o6;j3xkT0WP!e z&lnI5P$E4V|GNVG7jl3Jcvhw@0d)Hw^%GJ8S=+V5ODXiHEbvL@z#;+lOab_5J+Fjx zfxb9KKggqj=DsUN=yS(8|1sg)&c^*sa!1FSwDkDapb^W_9{4Il$9VI4I|DTE}95fDM=0WKK^hdjNq z;l$TF(g@@RrhIgFU4`z#96LMA+@PcA=o3i6_J&r5i!u>{Vi6rctL_pt+Jv|!jA8NA z7+0$OG*>>ok;76#%blQX4Coz@&ko;23g_2jF@Q(_X9N=gv;=^q4St4vJbF1?{NcK8 zTbx89Anj?k?oZ1PuI|;OfkfHfPX<7>vDz%0N%$%Q9@xz!lR~(Wb7p54*R!YW&*5)5 z@5msN;Sq7#CDBoq zp&INLbx8Vtbrj)4kjt6=EIH${B#WjcY#?ouCMhK3_-MUyovE?l zPQ{DD!+TleLM)t79_vEh_&kT0-$UR3(?9;x-2B}52ndk^Ob!?c?l|(D^NrKzh=z}e-0LAfgmz)aFh4_yYy74hYN&K#4ft^;6 zMO=Z{pIp)+OlG3;fr+I5B_Is{2gz{>JhvdzeSojPVpDG>mq}$&TN3Su9>u?)8cIni zad^)&V1F6m zSB*wDuVuwA^U@wZ<4_YI5qGS8Ho?R`hS;_bI9R4R7VOS(^mV2~gz*nXgg*4E^{yX1T-M92QXc-ZCM$!$8?$fjmCy-S+Fk&&4-cQo!d1-cgTq+ zc*OuOu5&X5z;vww?JD-R;m3dWtkYOIUA=Ig3-J>wNbhYu@>(6g9HzQ;FSyJPvajL^h6?yalmCMRomn=fn4F#f(K7D)j725El+IfuTg zxB?&)Xa)$hs%V)?J8C#UFp!G$Kb`~kU*-?b|NU=50ML=h0^pOe+^4TR*b3K=46tDX zq3s?3#Xkwa0sxPZ9>2oZH?Ch#B>*n?NzW(I{m-%T_sIP>@_kjr|2#!N;6MJuT%aGI za-c$hRsr7qzR>*B@&`9?nSVC_0s|D}Fn_@UF#Ey`(52v9?sSeK(F;14nhm_5I4FP{ zfA6pZ#JiCGsffV@S`~I|WfvnD)FBG&C@BljPtytpINq7!gryj8?CdCCPuW2#8z!Em zMMX&*lpVA_f^r;`0o%t`K6!`as3PO>n0&wH1`!V6QXoj`RU2P!o<9~8Msg)-^lYz5I3 zIT=vLmqV;;68kPJuxcXfC6o4qZPXM7z_t8;KB)LwT<+8yW?{ zxFJlTW#ob~Y($nx0n5B6gK+u`V)cmK@pGw?Ch@Ng za|6Dg^uWZx;I~jbO%A6a(xtOjA?heq0k*nW64T z!%Hu#27*-h4kDk+pDW)npZ7o0{%=30S+MaR#KZ9g;LA7z9vdB~ZsakwNPVI3kN~B# zmzAa3-AqxT0qW`p35Wu~o1e1Z_CJgRcv}NM8|Ix5t zk^fgmkID@|v6uT9fk?tL8L&WZ$N`P6l>^|n@B40#4OUh#^8pE@x+O7i^P@LkA0Ht8 zGZ|545H?|Rf~{*PpA3uKW(V|d2>y0jQJH`p{OTd#L7jEqo=O`lE!F^`-xB<7)}h!d z;WxRcbpuymcLL&@WKZkIgUo$x8&A|E^MHa{3oqj92P?zh>^R>#H*@xjoltovapG+v z_T|avk{Wlrxs+m3KY_2m5VKT$ij28(<;CC&^K3q<^e+dbfOHwxH8q+v!xq5wuH%yblS{eLtFOS`GeN-0uE|+$1vk9jfi8zi#GV$YfXzDo>`!cyng> zn18{rmQPpl(EvmJ-Y0C&=t?q|m0@mtiBjZahGKx5|j{egfad zyR&^cL_fyL)z#+}2_+x!%r|7H0jkG#f=HoB1?s<_TVg>D zM#2DUv5fw>_8Z=D96(}F4Inv?iyU8;m#3ILlo4BGp7T{@@RLP2`NwD9Lx~0%qqaj` zLkIB%VpdKw!0r@aBt=<*^`5XfahCY^Q~@G@gUYvTkrM85@@q`0Fsh2hp&bJXC>T}2_)qb_p**2LBj?6pa2AX9Sg9PzINVft@W5)-t$f6DPsX6b z$~nNTSa%}F<$X8QAr>{!_;`_0ouPOpn<8Aim4W8li8t?kvGRW3@1*2SBObVuMFECW zk$^lmzr9Qw@H!Vh7AnSvmV^5L@Y2~yj4z)j{;NZr2ZOc{@Q51|*Xi@Ycnm^7KTjkD zI+C3NiBNqouIQT{t4dR~_W}^)e5UQUEmRQJ*57{6M*@>#f**fUq~5YFg48q3AqQk= zISBc&=}9ItHXvp6A!301z*v$;fZD1WCNl|*lL0_`$^JCUFeuq>0tIPh?n^G0?z3VS z0c&_-Ugjlq(CTqGJAI%qV-40$ML>HKOlhM{*8Lj9(98?46rlceeZu&|0<~O z4jzahD}DWI#CjOj)2vg66vpuh+8q)AC;&J=B%p}*hY1ku{m8{Hk}u)`S_J<%#=qF# zJzM|3^BDK6tB?UmYc#brg;@V@U3;*qbiI8(34kBS2msb02;iEYRjVEa0w~?!3_zF= z5HJh^w*HU=VDv`_KyU#x{`LQW@fQh#8~nvYU|t{tQ0o)|F4eNh2K@*g)l%|9RL z0iXb=*gr5Is^8jz(+#j1DDHQw%nKaQ-a%K6^_HV%cYepN>a?);cZBPcnQx^7U}eGy z^K5yR${zG;R*Gq&S|fWaMsIy-^7$-6f@T$+0;HcX4B$d2X@UAh8R+7yAlb|hxWmx^ zH`|i_YNQo*5yDrupBW^e1&)*%0pp8 z0BT+E0k;?7$p@&cAz1q~{6O~<2|Swst{1xW9hq1{2}IpH5qiK(MYw>tCdvTkaxsl_ zh!btntOLTe>!_X*fVa&|GPsEpkJwDlFFYroUQ-(=IflK5xaNP-PMgz7_`Tgk&xg1Vi+=X{ z&#~?c)ra?9=6g&6Klf+9_={=)A_B!O`OUfUIaOji4&FQ{o`{}Absvk%&H|Z!S_2@} zgIJ%V|1biy39tbO*S`xWA^08sgZ>HrdHMg_-~H|9pMQ@NUSBzIlj25;(Ny*O4<0;P zdUzv#pxAiV1)x6#?!0AqD6O$TMzWrhxVwSwQ5!-{JsFkk4Th zp=>APKrp+M40N(RHGseMcoNps=sOGqsl?n6&#D?hJ*Nf^g*~CI^RG6w0#CERTgfE; za-f4ga##Qq1ITl0ICKOPDrupHM_CKP2aik)Y1LC#{-ay`49;k>o8SM$7}y+eivC~{ zhy5M8$~zRpmgejV)_aiuW70w{A^dsWNkSKN(-h@8^yALsxmn{pr7-z# zFsq~_#r2*kzstO>wEgT0sttd!qQ$}8^^qUQ_qOkI6z%SbTm>T^oDa@|JB#JqU_s3< z##BPAUfip>Nnula{=f+UZVClM8s}sJ!ZJv=j{a~{(~1+N8500cy(roA2FM^-{pdf3 zHt=y~@&v!TFjMI=gPteyn^S)3dqV&G^7)0bWpRE!2oNA&@>_{TKA@sVvr03CQ063C zufpB5{kus30r#7}P?a_t@kCvsVhh+`iX5&GJaKP2AK<|dOvx1iw4%#`W7@xvwO~F6 z2!>sMdX$fw7;@73^Iuk887MJZ?k)px2(uL4K;DtZb3!Cmf#zhE;D1qhrqmp*R&%q! zUywM8fgA!Oxpa1f3XP+rpWJ>`!m)tapyc z5e&mP0XhYq!Lq!?;F(o7G7;R#u?gU))nU;u1Q6H&90~&s=Dhlg7eA}NXD`$Ji~IfE z-~H`BhS2}Z&p-e0%P+5d$zuo`Yw}y7Y}yK7_4;VmPk3Vns_D zzorzRkdI5z1Na0n&<8*K&+q@IdSKC!6u^_mH{la-1fT~M69j#NErF6iNCAWzfM;)i z%Km>R0r&xj4+(yq0B@atTmFAL_deGC0}z9VOEW)1AF{jA%c#)D0NVjH`V4B`eMjz> zmYgkw%*la!1*2P;~C2DxF>^)l&ykOD~%`h%f9%wV~MUw^EcG8fEV z#jEk)&a6;|9C7IPyyB*qL(WG5aMCwVctlm=FkcZL8BY_1W{X0BGLo$#Zx_z>LJb`S zd=eT2lzr$lVF_eYoz-~HG)_0!Qj%N@+ZKCf06bflBfz*P!LW5R>PzM4K(`$j&o zmf3w`Tv&GPhF9X)Pi?-utYIIKZnH{%1oxey~Gvz+;sx1-KhbVxTvmK>$3= zWtsN#tIFrv#hEfOz<;(0oB{w3`Ctk|fgC>{i}knZ6yO0c*Q2ZkQ(HQl<{~dpz8k{% zF7H(&^f|zUx4UW%Tw&&`a(2V^u80^v6gTL3_X(fPFL84N<>x-+>PY_X_=ERi9zWsClwYDc&DY@jKU7 zUtdX&IqrG~kmZU9C=v?4ANB&Hx)qeSHSzf%2!M&y8TwSR%9ZFU#*&bmJU06BbNadS ztpITE3gtYpy#fReq|f?XSa}abe2u@_g5OYxY8=>`%ND5DspBs!OjVIDroy?v^Mx^9 z{J{U)F?t2QXd^^gYKz+3ePQRFNfas_JKOsDc)q~5Fgzf6SR}y-3Jsi|k4%R&hnkQL2Wxm& z;x@nI0~hOd*v(_${l;h<))Nq=MyZaJ!I8OZq@%ImKd3(ePu%N-Iib#m7qYHLDqsR3 z63lvU9l?(M5I>6t3tSGEs$gAMpF{qsx_{|lMcpAb1}h=5%Ok-5g_~@_w7AoMms3)K zRRpK?mk8`LJfLU;AkhD#uHTX0bvcRHi+TP3ZK(Y%0>0$_--~Pwy(+%99o%`hUU|^d zvtHG2wZAs>e6orZ04lg_01{;HrtMOgV}!|#RX6J)BM%PA+^4We;}60?AlNrC?J!JQ#sr^jpnage}z9)9wFVGJXig8?`Fci!-4Sv+>2x+bnH9TRMu@su`I!FnCX1{A~(@eH6NM+Zp zq`s#0z;J~mG9^ie**B)6VXX0DYhKMeHyEm$`uh47VchK8wRHc!cQ+`Xg1rgQDpQoz ztK}=#SDX?chPFo&vH#u&&qnkdnsq3L`U6riAa+IQqm>Mo){}}+{WniV4Z0OPH3_c2 z#JytneM`do0RNMds*ZkKTMO}_PXP{PJc-m0>b`AWxNu=%fk5XIV_f0{|JXD@9c$fFBeGyf>lnDQn|N(??K@BqX$H0Sw)ERQ4cde&SOMB8UPgkJe6|50{Di~PkMTO z7|sLY-(%cI)&LJRLd*aU-}h}G3_Row;~|QI2=B-ma5=~<0RQ*j-+&kp8G~>?8~?I{ z0HGv_b&>$MnxSkl9SCkdvnkiZa!2Fr2EN-soE)Q>2NR}TM*7lq!7S(xH##g))V`BI zd`tnz0hnpHKj)v0GY0dqL$R3XB6^Zi0658P$$UW4l*%HN#$@xy0MbBcw2DVKhh2Br zFe91S3g~)uAoFBwD!BS&2CXnc9r6ez zrWi+Ok~NsW^WBC$SIJ-ui;8z|dSx#Lh!LBhnT$JcD5Pru z8MQOt=l_9cxZOyxjZL1DM!Szxa=g~ihkGTfhTvaPD^0^wJqoC61Ogo772*~EC)7)d z>nf11fk}=?Gob#My_i@}8Gpbi6n!f;2!&iK}Rq0+L%J>-OOw1hhKO zi^_x?Lfs8Rmy3HlDgcJa09+1W!}TmRl%NR{ub>2 zZ~qp=@1`w)FH`^*y{70Rv5m;x`n4bSJim1nx<8w=04ZI!dQIt?b*m4rC+H*WpJ(6U z-Hofk0(+h>?Xdv(AfJ6KfvP*am!R z4tQ4uaGC&KzpEseRv`8OWgL+CoD->>HvZo^owG1#4|n%TiC(WF3W8~bJ`k@`{o533 zCYRD_b~b_ex1e!02_OWX-RTy-GeqS;x%fZ?K)uN5x&&wxOt~;yk{O}x?Y=(~qha8B zP4aPOu5kSh>GXD20%bb}T|CUIM+b+6=7ES{9VR+motrCA{zI_$hz?o71{+OaPx3$i&cad7z@7Nq4S1z#`jxl2qF`T>TvAIdE&etqp&l3HoV* z+&|k#+&5mu0jup?8;|&NXLygAp^yIwpS z^Jp-dVLSlBq;g40vBCx|%JC<{se}}+zEf*!5Q?g#Z)<^>fQxnirypHCclD!>6bUEA z@f@Q6!iB1)>8V*AUj)=p634%2>?2EK8jB-#rYaE zdY;h-G!p)vLG2gqa?OPS|2ZUqI}HAu0Tc%T8cmUlTJbXJ|E=$T^2zf(n;w0HXDmQ^ zR!Sj52u&0cBY`+{-x{VhH)zQmq~&4{De) zpBixG;{<-C#$qIa7K(iwH9*lA=wa2V%Oro=X&J_s$$SgH3m88Go%w5iE|?VggAdE zm=V*QF=Icx7d~tF^Y}LQk+eHQIZkx%4kW5Jp@Tl(o^j;`6)krKWerOu*lm&{J(^9X z>>7XzNG?-V9(#x5B`%dj6*_kTaCM{5!`if-=XlHaT zrBKYU@&jw7$VO!&Y2+OHou)-XU>S&s@u za2n)sC&S1$NRMFi&0Z#dauBOA?gP|6SVM-MhRXD<Km2;piwN%Gq<}k8|1RA$@=XFE2kZ^CNCQ;)_T{%8d_qOPDJXz$ zg8su^pn-pwh=20QpM3I3-}t^CZQs6e7M~@RGO2_kBMg0TA~bU(?l* z3W)b_62LB=UOB_r{T7Kp9bt38M;GY)-_`rW^DqBS0zeXw1aRwCnZKq;>;-z@EP+lg zFaa>_iQDhf)RQ~1Cz<`y9f0do_Gd@mkp5{4kUKhS4bK?c&dZ@Yx4%$vj5>id5%y10 zG?vsD9DD&>c#CuS&xxjWbvB{wje9#dI7a)mJo-`W7{6C2Z@{4j=~1G8dhG2-JD#I^jd!;(343DbFc1Op$%N zcDE6~eca&CbEm!MuSV59)&4oIHDb;Rm(<0PmPqI;!@XXC4M@BHlw)U>`+26d zvAwy$2|Yk`&T(j!$OjZmSEub9u3inuAZR?5l+I&~*!u98+sC7A-?g1*&JbSK8Dt>S zhxG_aYJ#UK$cdN^IB;!Vj;4{;lpuN_A;3J+%8%eUQpbF*?xoit^}hDbYk&BwFQd}8 z1NSeMOJtj>P_71TBqPI1c2wc%)(%K0t@;(9_y-R)l`Eqmq8;JUy?ft{Q#bZ;3+{Y& zH`?B3&Naf6YLeBgO>rVAaue@hZ}{aGKCF8Uk?|w2SsFm;&R@N8?WtAVx?4fxxb8vZ z>BrZtdwT7+wSbd1?je@OqkNda*1pp2CU zvJ(m572{v*f5TH`fUTug``e#9e0aOj&&AK%Cqn!`!OPm!t4#q&gM6|oas{#m@XEKx zFSY>1&fmZ?5P6!g!;7}?fXWMnA( z??Lqm>-toEjC2+G)e{b@K*oeML%9EqvkmZr&%OYPo@`2Y#*&)x(YCXPy*%|V#)@jpwUi*5eoljJlB9Y18){+przM#SC94eEu^b3ts?pZ*1zZ>azFSW z;`_PmgPk-H)@OM@FAs02Z;}g}OPltDz-x1OXVoAECn=e^wQ&z-wEO+uKa}}b8X^i zi>`kEW)>xu5v=~1(tK9bC4HXTpV$NCsJ4-0*HQfhaO5KlT~H4Kf_ZK5*I(nkx*e~P z244DgO-D!j%i2d9g6kw1K&4V(N5ND20R8HzAC4V)wE-2{H{5PxiLn;e!6_IG%z(D>rRS~$g zyet-w#N5kb-Qf53pM4k1P<;RnVJ}w5H4l0S7c2ySr1M9O#WFx3rOEwr_@7K=Alv1g z0mc#_?6@|QG-&SB1T1H6nCCeR|B%z<^6Xw&nduHJn-StD*0gKkI9p#wL7?jfv(u3IoxcJbHL3E(&7&cM=K?WIai07an zKw6{OmU_I>YzV{R0hSnvp|)F4t5)(lp+3MAx*Why)cPP?&(>x=6Rmhm3Y>zURTP)YW2Ex zYj|2+vJN0Bd}U4X_`T8TdA>(cs?dHaP0#bS8psFTV8Ji@*NTOTYf>uhqdV9i1&3 z>sy}H{!hW#tPb`4@FrrJejHB*m_~Eij4D}C#;xyTo7A@xK!Oag>kJ8Uv2U!2MiPd1 zRTcL?;!1ODag>vH^pyiB;90*7e)!c7*rjP}Bt2^b`J^V$_HG|Au{k!1~@I zU+<>GKl6U*_lbXE-mELcl74uU-WfT&Dh4^Pf`SUHAdI2>}Q%zikfi zR%aW?3Sc0vQ~*apq3c7-^o2QJFSn0!os$fZ5a}oQ$pqZ(K{uB+ogF~uZn17!VYdQe zN% zTFz#%m_bS;D9bA-Hi(!AhgZM;S4@_1hoc#%%qdI8E%QkNC)GmjcW6W^nE20V`3;c6u;IfnQJW*g!VL+Aq!4Z|Pc_WwmV!aO2| zDF9|~{nj=9FDd&sJ*pqa78$u)aOe6KdSD>``w=w_X20|Qx^j7G|1PSdeYA-&nw$Hp5Nz{`M^ZrE6M>{!*d=S zMK651Obzf3I^YU}EUPF!lv|{B!*Ixx!_}qh)hxQKcfYuH8&!9hS11+)i=ccyK8cXBdbNM*GZAFYK0sAiw z(=cA#fweUpk&BFqKD>CP1AazX(?Zo0O2Q-{#rM^z>Q3SZsKf|?JXeseM}kT^x2_qw zbQ`__H9(h!lgNJUM2%3J3|ubvwKb=@3VAMkp1AR;8gvP=d0^AT8X$>ClB@S4YT?o< z^r|NSQn{ZI8rWg&zDRdh_pU|$HNt|i@qeb)sOR8%>5XsTpL*&M3U>O~F`DQ*#^iv1;LP$LR zJlHiK@#Gsmq;=pun+N191Su`L&xlb-Tx#N|MzEj;fRe~l4iqFNVqy*)m^(`QsGnRA zl-7p)$U9_%QD`OajMmdF&sH~1o;f$xRFFD+wE41IjPfS&6k@N{WqFi zRK(*=;&ah_aMtbK=J6G!N1I3PUB8XSCvB>@XFVI;LY!O%V&yuyZftu>RVu(zCPYK6}}p>0NKDd;ZQZb z#cQJ9kN8v5Q&SWIj01rL14;1HPd~z2KthN}9W#Chg#2Et-!Rp;^#}-G1S~(AP(=Z15@dt#Nk}_eB zPVi+7AQe-pt*o_MxA2#>1_+Cn3(}D-0LFN~nP*S|$^gM5mWmLMVX;UtaV$tVM~@;e zG>|PM${Htm1gy*el-83Tjx-!);ymE!fZ~yb>d(CQKT{hx7&7uf?HAw_orPc1Ul+%} z8ynr-j1mw9l$O{CC8QJul^UXw(w(D0Nr4|I9V#6vN;4WH1qsQ~-8IE?qczy6aYdx~8=sW59LZbni)0so5e>H;s2I_F<%Nlj>?H}`!M zp6G8Z2dWjoDxv53pw51(xm^eSSIk6;%L~N2&VUaRFa+fJ^b!OJzctLbHuctZ%|~GB zsgjPVVwz~`*!Ri5v*r8_NhDF*BrEa*OCp&(eD1D{!B->ISx6@prtITvad;h-H{sT` z^-GDVeZG=Y{~(bvB65fpe4#ZVf2@6k{^t0iBuW~v(h-mxX2puUsQY2T zEt=SN*E2>Q(#dubMN2)GV?TSoLw2&^yvm5zd^-}+o=JKguP(MjwDLA-px6n%S7hhun9Ii^;^b2ID{s7Zm26p|>gF+cE>ep@iS(d8V{iC=Ph$ENj8_HhV^$QjO1(!y9hh7@b zGt2)ht9wc10v0q^$@b^oUV<7qc04x;nGC88ognEqTHKaEWWg@qCy-Q>UmZCvI8#4= zvIaULh$OSgd-v~GEAGk=yY2F&;$QSLxI}nQt{?SVf>!Hkj}t-aZ++7%i;f?WNc*h1 zkp0%huX>buTT8p zb#?YN8y|4Nb;;$|k@e?ytEL>l3|Oe~V)2-05TJ0M7Ab=DsfLJ+2CkejGXb;2U*#a` zVAfF8%Z;rc-!rM4qY#X?|B1hi2t4?s^(M^TEX;niO~}Rm=MA+C@(=?1tB=R8w<2p> zL;W|}{k}!hE=Ty`%J*KDQXGh> zOkzdDc%_w4&Op)l5V`}2K5y-+~*=p%0NVxpXIpdDxgG;mA29n= zgaerAAVbWz{5UqqAdAmd5lwa{38WI?&td&UR$`aR1_lGUjMnwK_g6;`_q!B!#@dXM%H)pY7n2{Oq0J8=VUK<)?)6 z%4qM$-#(H4ijX|KleEd z5hUAslt#9|p$n*b>@ynhjgQ$5WnJl#paT2>W*RgK)Jov|`T+@KuGbjbiLn0uGB5I> z*O&Kf65Q8aT#WhjO zAGi*2F&nJY+ZTpv$9mA}CbUf-g!E$E18KPvnhB}=1PqYdOi(`V#~Qo|<$h6TW}Ita z0Z?M3*Gp(qnQ6kpFv0UzzwGvWs~XBxI<%4hF-=2z)fCZc9ub~lipri(vLY>0x4xZ= z;Kt}-d?W$`rH^7UjdB52YS|1p!H|w0TnMtja_F)2~qgVKAK&P<$gJvVna39EbY;GkJjS$ zsb8Co>BMW+Hw^)76sE$ICFJSsKGITIligG;E1{9l7_ zL4`jI;Wx=UA<1Xlp`S*`uVWcHIyZ;vO^UOKw#pJ=JLM{s{;9X;&MpL!nWo(?YTgBtc0}mLR}b0C*keZLz}CH?cRd1XKO7uIS&BKz z8Aj?~KSMPnL7b*}_K10mos|?5V_D^>gAJ#>lZu6}(9=)_+2+h5|Nov$-}Z|bw4#Er zJ;Ll^yt29_H(TC4)D-gNHuHUPL$D!FP46bf8K+3NmiC|frJCIE=eHk254?_A|3jQ4 z*hRotFB=Ge~Ms zo6fxc1SBJ#0xruSe=-0Cadi<|L9vTI4wyR4@LvTHVFTX1KqlZ^+!5m+;$aBkMt_UD zC@lOKfAUlghzjz2g<~ts{cM$^+^NC_zs&0*1w{7?6X{8amBz{mrLg|a)&dtQQU_T_ z-Y+PqT-#N)-g&(2G!)I*^Z4D+;9G_kiMA|xB-kZaqLGCVlZHOgGC$WNWpDTZx`cwT zeU%9faF|cb6knPWZwbY(F}_>Nd`$1X{((eqEG!t-X;66bE8Lgdpic@##1^Vnr&Vt6 z5y_jy5|wC&3cnC$Lp40#Ih9`RQdFZ=xE$*gd8etc%k(duIIHk@Q%&z9B$)h1ZV zA_v>@L)|lStGd?>3}lNp?9B}q&ptvW(?kl{S~fNbkA9=vai$mr0V8)tIACX zxphnQRLIii2S7R*n5g*No&Kg}&9CxO0RL+m!6t%fek%idRFC-U5|H)G8_Ns;3zZfj40r(g zZ>Z5Vis)BN`<6a!1`}9A6t9(0LV=rk3%_6Wo4=_8{U?04a{lHlv(Ti_QrFs^y?7_7 zLaWjz0%h<*ImxrFZQ;1v^eE!+vlF7g{_&;PT@anHO1QVHAd0NTW1Dn|n_l9)@G#)4 zEaY`PH?2sNcm}j5aCsE0p(3ocfk)4AY&VT6{HdZX*UbpW zeU;5>)B4i=rrBt>m>nnQOD6_3iKl{dV`SOSt>bwE2U&t7=G=i-kxhPs4YcK5mYjcN z8sCm@UT_dRC~@9-S6qrZC0BRi?0eLt1cmKoFCP6$5E1vKmbkH3U@~G7;GNTulDW`@ zIPtT+tJ!L-j&CFNoD#rOp`-jtfob&Afc6{ZAPzG1i7>y%0Wbc<%caDaSiaoA#RXv# zF!+>ql(MEA{F~SI<%hTYd%3x4?XRGdixQJ1 z;*SD4$s0ILm+oVJ^-Xtq21J#CpDS?luRk&Sen)=$y($KAKZ1V6e*#|qys<6e z7fU|7nmI+1^GLs_(PiN~ZkVv0*bCf;Bw;7%vAj>M7!AdXvHLx~F+=H=BSv4{Fm zVh2qfYMf+7{9^EI4nkjF(;deceqrB80r&FRA>*6A^UES-tjhC|Z-5wH&GtN8Wc3J( zCc;dZ9eQjH)fKxu_M6h9r&4X~j|+o0Pmzm1+Lw3|kth0!`?3(|%z5|a>O|HL8hJDz zLDf(&FlTkR7CpUxRYP2sbEP#Hd&ZYmxicHBwUOI#gA*!1s7b64yVC-_3>-9bCN2fE zd|O&@uG8R>260=m!9c%39^uj7&{K3?pvYz<`avbJr$m{;$MWradUOX@5Kk|dK@2U9 zzvvl+Mm4w@=%eG32@9<4%r!ZQxgkxRZ5q84O|nydyG%8Pqt$*fQjjONRY7-)3L;fK zZG2`<*c%^UI?6dg(pGhp>_KY-CcYl&HPZxe3|@vpRw^|w%W9O*4$7Z!F}jX4MzPE)invH{vGlzgfB0mso?5>=ai!V_@ifvCezoTtrWi2nOkP4 zg-vb}71KaQSA4QeqT}Z(4OMA~YLxqgnQ*J&iz>>zCEOm_ZBmzqd51dDmy8=-__sJk zsoLBi2l4Z-_f>mHbG5(LtoJ4Q$+cV+v<;fZgv{R|O#VBgqIhtJuNHg;{t10T*EHSo zbH)B)Fjd$o zw5&}-^)ed-<46Ftbl4XtcG6`t9E^6<(Lb1Z^FzydCoG7yVPMl^y{_R)&oftq90*9u zrm7^%9N+YaA4~bGErp+U_$YDn)@VRgrq)A~e9;22xNQVd z*k=1#TL~d?X*JGfE;%AfanR`}VMie#8V2wE>O^-y4^qyVY@}ynzHjzP6m$|BkA_f*Y;(_<3UX zdDf_`>CuOVVLLd93KhPORP1c`*l#gVF|#o8*>VPGk|wbY#ae;(kg5OpBw)9OeY;lY?30Eq(u8>6o~}lyl<1sEU(#$GgI_o z*YgFfxj0-*V4WkpM-jXQev4PqFVebabB&>pi?P^(eEGbPzSNd9>3f69Xq$xIo*FHp zO|ozU)O7GR8&m-aCiOw9Tt?PF3w)=xC7YU-S9$ht=P1c9ihBy!d%V*9n%s-sYr_xT8_6W~%nYzfQz`UkF zo?%e+({#$zA9z!Clo;Bh)whQB@&3p_9h(v{DZ%*Bv0K|ZJ=R=9_34D@#6If{h*N=; zqjD&C_Q#*;-$mH^%j&e@_~qJvYoBSvVRri=x5~LTe;$imeP1P$yvtC;t$Bl1=lKoD z8JKN6`CsF&yE*JGs_&P-yj23T;el^GjKt5LV5MQ@tejW8sFBD#_q1*x=bw>;?OJr+ zZKX2huTviCdCb2OBHN)|A8-^=3WDiKEj=x>Dc_ol+=P-c-KT;c= z@AGBXpZ)!K5%43C!_HAEDkX#Ae0L#Qlw79Dg;p2&pmo-oSEDadq9&;5Rz3PyZ@yq`SAVs3N zxWBYu#YDsqTaUARifAeYs8YtVE?rGug1JC5c zLlk`PhPzeEw>_~{_&xshIj!!im#F=EbZm@P5iFLaHEB+w&Gr zqw|;xcfqsrGMsD1TY-TUZ*H&+H}`*1q;2_j(=FKJU`L{48;bKV9>G&{N6cSi?V}J=y0#=}!g(~p>cm{F{D4zvP z8Ho@sXkkhh2oThj66r<{tuKZR0!h5DG{n@bpKH4#?ts|$csmxHkg2#^N-$5T%7;iO z{T1T^EJWcIkj}u;@cnP3%#UsAq>U$^XlVCxgug!eW1X(GDoyy87MP7P%ld+f=YL!x zVt-%bK94tZd*BY;-W?M9IrFWmLox3_PwKpbkPGNu;L_C$JNOQOIyYQ^WAEjP+((n} zet{~Sov&Y>@m0O_Ny*ojP>g7PO?vKI!dCvlqny|U;s$FP0FSWS;9kB@j+Zu!h$kSU zpPxX0=ral%ZIG&LGHtC4;Y(Gu(fzcZTOyn@_W=4qijxpU^b-Hn*qC5mrd ziJop!jKdj-d`(H97@W}s6F~^z1ctR*t2o$e=n~q!7s}^b-?_9e=6ZS2ZH#Z^u0oA~ zr_%%E7XF;sFN8~Cyqo2;010vn3FkK%f%a=Tp3j69Y;jEZI0N-xN@`tPl2TXhH00S_ zH-J8gmo?_~xuL((m9)yGeq)_o$G6;s2)IqCIvI_+c9n?QP@R|`&x5LGv}>;P#SR%z zU?gG7dmX3JzTtne>?R6Hp~UJYU3nZ*3M6U-;Rska zPHd+jNs@jUyMsDWDPM_qw^4Q2r0{9aj0!G{Z2XL7nkmFw!&SMCB0~9s2I&T=^4+AY zGHAeBSbnkUS2NRSG70|fRkFL}-#%D#|p*gf~LRo0Wab6T%2d=MniICoPs;AF~MpTgI{R*w>u6ZUXjof zxU+%1?;J_N`-uxpxNH9Xk8(E*YN~J#`1r#fmrFml zNRTH{pd+u+2i^cufHk_9%oV_ZlY~$UKY4e$t_JgJlh&VL6c>94MQH&-Jw9QvlSzf!>4hEffu$!20RFL`jc`&M#vgNS*^ z^@e|aFDbB7?R(^Gx51riDg$BVHuP`Jl2EGhWUki;-E@ny))9dLifaPQ9u%EeCQ))@ z$tmz(8k3*&23zNpJH>w!q}ixW{05{LDD@RgHH1^e_^C|FiJqxUe=XA4Yy0Lzu>G|e znO{F@iWS>GGJd|d^HGXb)Ti&6B5%5{qpyV|d`B_W#vc1o!o2OWu|(LFN8!NVMAaN_R=W8I;~z-Dfs5K^ZB`w_ZXUx1+Gebj{qsR^E@gq%B-ju@?b?j z@dI&cHn@zI5xZrvNUq6L=UIu4=nWYon)PUw0@1uX`!92Ab{J~Rz6U9V4yT*A$mm31 zB?xWN=r#>J(OP_QT~F)`x0ZMy_?ps)I`vvX3xesdf0BCUnS5r;T_w-Oez`r;{*UzT zMtKt2=O#UCT*v$$&O~z1vg3iHrQJ#M1_>)nDJjjng;j{$IbsVI+b)`ZR&ktBs^f$w)~exYzLwj_=#zR~%h>$OZroxwLNEohW}=02bNFZAq~X-5D(Ji;>7pZnl| zB$090o5))kGISMi&JP-T%0m>?1wNvg0R!OFdH*#6s!{%K^tDr15!~%`I-Cq=pz`?U zp2PZnkRd++!Duzn=8iII-%HyZg&=8+n8CM816vrkh{PCOU@wT)64P?_&jI0xC_(fe zUA#VP{Fm~$eg@EYJN?Rt0*1=IslAE6ll@NK{1kEGS|a`8&rqM)*XbORxqc&s9#qRx z0yf4Q5uvBP^j;)$wZ(LpER$@!Bc&Qgnui}cbJSm46PIZV_NVwJpl9&5IIZJ-+u7^F zOh%N@6Ze&;uX0jPg+ZP>Pj@R8Hy-_pi|N=3>jyp&AF;chem}EH;j^aYo-LXK3(-b9 zWKE?HpFFl@Y7)=#)AVhuEE`KaY7AX4@*5eyl>uk*nx%n%UoW`kMDsTIVvvpkLC+=B zq{zP8!InNBti)fbCmo7!{H@?*v5eUO$?jDAWzOaW9?|d7$n-2vNZa*`2qhI=@Y{DC z2wP{9{d0%&-jPJ>M1I{QolHlMeLa^p|Me)7tGJBDMrR4tAr)ADVoF7$O2Y#Cw8792 z9BuVOL`qnBPzpV&hCuHhr9=fHuGCHLjFdp}=zh!DB=|1YMa9w9Y~mnj>Cp|+QXr=Q zYsK_Cpnzu!aDh?8k^`Id;`EQg`lwh*hPeo-)Maum4{QR*3$-m(Z_W(v!JNc!O|nwZ ze=+YL17xBo4a#a&R7?OVeZ#wc%gxtOw#g#-i8bIzVHbMreZC^_wQO1gMFhW(#^tUK ztqYXG>*lFY0g_EWG|B)BJPEaKnRlgivi*`L6E)H!UV`WUuTUbIQ}b#ct}vz?&f=t{ z^5(qcZs$0CEPRW*!JPWcZk&j&1;1-6IPtEz;6pLi>qc8G;YUL4@c^ zWK%@lIkOVzmii$@AqPOFu>YHP_Da6U4Bhk>BmcSX*ZYg-v#OC5%TnHvOYT_)y_ed) z?rPEtXNSW_DjfQ}0c?(L?N%haDrU zJZLFte0klru0LEJV(BM_fJBDx%s+f!ya7)JirmyY%Ggy@KL2LH&!_3dVSnrjct}Z_6oi}gSuzU5<`>6~5qcx|d`(S<3T%3Gs zOi98re-QOJFz<7~>&?CA{E@vof)8xM2+_fWrH>_b#Ppi_c5??rLmyI@cn5}}6%A(v z0U!r33!u{*9qUlsX785bm#y-AqKNZ^7>gIMnWSl9F0a^E3xcfGGwJIX!D_I%> z;q878OVV0vn{#J)Empa?BBi!*eP92<-n)n08H&rj{v^EBy<*!m7RiDSq-fjCm8Ckc z$S*o5i`)9?#IG&S`it~eANAv?FWQI8ig6I`s{r+#Yc1tu@b8gyAv=Ha@8HWzt@aOh zB|BS>)xxdpM!veLQoL3#b}zhz<|inYFJ)Vc*Va$=gEfw_%t)k2J8rTR9Q`A`t$hvV z>^<*&-^Pw;&z0nB-zrSO(Tk$|bC2KR!(_;qciDI}DZAVGHc)V7<#*?XwLpN>+w?fDO0vC~F5C93rD9b?T4}BC@65)-KblB}MlW>oFW7a}8 zG`)_$L)6Y%95+PEU$lN=aw7(wMzA9>ps2~{a3J=k{$*4j^aR3!$Zk^xkxGFg`dfVU zpdcn+xE|RnND$=&(OcmKaEC0yZ&NxJmGuH+#q{no-sGqh$!*i#31qvn;)B1ZNtRwPo63+x@Tb#i-#Y#Rt@ex ztgRB|1G_|34l(Av`oUYF+H)0md0ft5aO)fSvOu3^$WQ2IMtEAO>VEU{`MJ21uZ{l( zJw(uCe?a6)m*xA6VKN|{-tM%t8U}$n^F#h>zasQfU7y#6NSSXj@ngd8W*{z)5bbSV zbS3H2WpiIIA8lc5wDhY0nh;xXG?4XR`}?6f$e#{glau>6KUh@tE(8t^-H5$QUPP;% z=bs3Rr6k(d>U(zhltL>QuO~tQM6jVsLu9wviiDd*(r{I>eKlr6YI)YQ+TVm7Kd_?cBh_rHU91)r z75AO#KE> zj)%bPc0MpIp#;SfjQRd=4vU3uk#r z^acre+zVLF%5I$`ESg@#NHz^s>|e^azWfhv6`_A`fUl&?i6`*Usqd22^Nszc+O2L+ z{~{&7AJ#t_pIg*q$CNbdV2w{4Av*ykKtrhO-ZT(FfKwAB`AJ-ZR`%H7SUw$K`e`_5 zU{KA3lTJp~9Sw2c`4PO%t4!BowX{F7CqYe^Al;FCMIs$~I4oJ>$#M{*OZA}D6K3do zi7^kS)}ugDVh3H8h=%p}K|qx(z=#o{^eRXh;RXs^R+E}#O|M;hHr}eDbt_K}gu4`4o3czVgBpRdRXH=) zRE{`8JF~0#Q+8^BP?oXRA34H`qzfNNq_sxN3!xU@TR%)U(%r}}`^Ah=5jDDz0OlLB z{46N0M0p%Y^YdpRKBHfe4E+2YQqI$?D75>=okqz!YoKg_jMw^uMpDUx<&sAeREyY0 z_%<#KcI}~ ziK@7rh&TvX@~7=PvQDHv?3+jSP{h8>|Gj$?RpHTi{W)6i(C;t#^y1uP?F2{#W=y#* z!GUz40E1%q+{ik%r8zJHg8faW4`u+gGpK}oBS>#U)YJIlS$H^gNimnUW$3j9BwYbw zk6eLaR(lNzwi9mergvx4Jx&;=jK?O_HfObM`?6W(&Sxu(8syA|NQdsEsQmfMS-Cc#oElN8 z@?Nm{lOAJ8(|JA7uj^35vKRQEeM(Y5g?;zQia&|Nnb1DkqX5aEZ4d&BTDm3bLkkwh z%)Qk9XfdLHGOm3yac_BsXF5a!dn5t)r0r{OgKF4a3Mfh+_#;VqaQuScn|e+L!S1;P zo;QZL;D-5EXo6KbKwxX=9dcx*&p$Bcht6LO+6=D?@PO*}jiDVxbGrO+;U}e1^OT4l z5=An9M*+isxrVPCi$iLgHhUA?TMO;GQfb31Rre?Jn`%ziUKsx8rwZ4z9dx&~?yfxv zd7iW>%IWJ8oMq~3EHJ8bB)Y=rOt}n8y$OoxKnUlEX3p+c3p<^NmH*SX?|!TQ>jCTw zW3%}C+#0zUR>6Wiby8$VCxM3$eEUWG-%gL3**=?|EhX9?GCyARYuJoD8eZVD10{i& zwsbEd0?v7x`a};9JqNTyp-kr)O!G{)5L;RS^yTYfEQuTJ6H?Pj`jlXE$}aj%gjQ!b zBKB07;mVT&Em$AfeSb1A!DcoZ=N&SF&U&{J{8c3F&u9Gi$)9_q@1(yq|16}sCj*)x zXWA!|ykPA3#|D>K3JSqBFs=s$o4=y$#YGbwiO5*Kpz9Pmn&xD~R+Oqm3iqQ;g-ghFn^oYyPSoR06V?`dg zc&1HX)trqZyks{dnPb7xE5I^%r~rK~k@qqOjFw-(@v437{7SeC;?vA^PCDo`Hs~J<}5s>WoE@ zIGr67!;G;q1;W-vkdt}_^Hb$(U}(B&a}sfj{;=#e5J3b>i{_z<9wgjDU4~`=?H6A| z1VFu{$kTPh3J_r#9)Z^oM4r2V(PDVP`4kp>i8(A`E2x3MaJ4v&5IW415k{tQncVIe zz2|XD;($BWq)=-M1Um~`4Q=)!+Zy1kFNwDn@?jN3Khwmss_;H+D;nS>#3) zcW7=&#j@v@s;}#C-N3gC9Xs~lr*UmJN1G7}pO4?LBR5K&Xo&95otdr@6B}773+IeE zpe=64I7+t4JbOMCnHE9~?gia$Ym41HiY~cT12-Lf`V;(|-o(8Ls=yHO;pStKuy^5U zESO>nr2Z@6zw3KJGYoPe9gK!{MaB140J` ziGK*Dq=1D1dsoB6z#!Z{VRu`R37x&xc)oe%7vb3lksq_It9P#mIEcW?gRXVl4Z)wlV6e6{jIB3-?lpfjlyLqDi)9oOZkGs?4?-f;WZ?ouLA*}=>CK!F3-ze99GnQw5 zwI_?y9XX?`PheOXs`g(RrtNxq**`-jMAhkS2Tq-S`2BPL&`wM_*dT${B0O3%pGz@s zZ@`HnrB}~u%=v1{A^+lhNXm0^^x>6BouBEw571vetEO5H!~L(?7+G1LT!lKpO#Y;4 zkN5oHyH7dbt~u`fG++RL>H3M+V^F+^4f42^oZDvDlLslWlUJ7`CK=%N+ynmKzcNLpXrIu}oKx7euD%Zn-84$WjG+|HL1hisqA)9dgwZY`U3yGf~IYLwvl)xvIc~)>iF*Ys;a=gh)U!L$7=q~j9)-)XtXJb_fM}dFCzTQkv1bn9zo)ETxJ?{l4h$3F1 z1W*Qm1jB_8s&Ynt|3qSLl2`8z`eB>6=bH$Bw(cz~r5UX&7lx(kh0VFgmbu{i)8oclK6xS! zsdBYHP@KfjNFz#}Ad{Oye@C}oIxkxS0Y=W=Qq&%zqnR8Z`E0>x4{P$*uG6vX6sYsZ z!VgoBulzSc0)Oeq({`UTGfpgPia*G%KOCy$@5u5G$9^M?-#{*zJ0s70Cg59+<(b zmTBa-)73y3h>4)y;7a&0s+aJl#Lhw?_f>Ck!o3avzYGvO4oG@v zpRpsv008*W<^*~ITwD(E6tyN38ZIu*8=g=+A!GOPPYrN1c1ErD}`S zazt7hU47pd!CKqkZeT*1PHm?12Vcmr6Ox6r-T0oZyBH)v{JR)o2LQ3-I=)yCGU^1` z1LkDyo2@+LC{AR3`5ZNH2c?GAkqDF|7Jk<ZH4>4Fq|x~NA+)-&)sVJMAqVnBRx1$_o)tH41)DA8y-@}w)1zm1?EWs zDan=Q$6EW=cM5-86UR zcog15;_sNpBCfBYT1w4!vpgA6Y2o^ z{orA4G>%VcM2ZHBV0HB*nq7mry>a_{NYc(O;zfF{RclDL&z>lmE~zV{ZwN%ZJN+q`Q?fUbm zw_=82>-HhhX^bwu(B;U#IyBU$3G2SRv+)gi;t#L(g=!GaeI(TMbv3h2VaWmk8q}Sp zqTr41wkw>hYqK-kA7qVt8~%&1u#Gd_77@{w%y2XwkWaY~8(UC|Q(m3+>(Qt0(Z z{Cho*t6D6|jm^$d_BU~$E2+S>HEL^mw_S@Fx@`rO0(|@N1BrCW^Dv**lX;QOsP^y` zoia^T+k7<6oR1Y62qUnK1bvUR2f@YQMz=K8b3^yZ{yXhdd@8I0ZfdgwF#3F!Rg#OK z80p$=L{no_G1~>zXsQWF2z8r5*iNu$eq_aEuDXrIL=3GSup(2I`8d;0JG|>P&@!o5 zfQNZW;!}p`c2E8ZCdA3(VG&I4I|aA@f`LFuIQJjEL9a zHxetzu!E?7+NA9-2mYXyL_iLSJdPDbg-Q?ZOisVsI4E1Vmi#E}fg4EtMSqsOmlWLY zG1zq~e9M)~p_LmOw+Aly440h0V&E`hlv=d%guJF8A0W2w&pv=;aiGsz@J-u<*^n`i zIi&;I!m0zRE+C=(6b9;}wma;;vB)fzOJ|A#V*{pq2B9~@V`+{Qi#b%-qmw{WWC9@N z(+~<^J2wd^IHq)my#dTna9OH|QDWmy0HnCs2SF@WtxL*f{g!;4xJ9C@`*#dL|LcFP zm?!SN0)##PbPI<%V#VJ62|As&etg)`nX*ZFRv~T6ZG?Kk=^FntcQn`Ty%Jg3kXIUs z5OS{s!=}!?0erhfO;*kEfEst7A`--Sr-kAKs9mUh(i)u7zbpJ&dd|+S4NfY={sHk^e1f6ocKS=I9dsza%~UopR?Jt)O?RV!p*-53psnT&OdO&=6pS zv-idRoCr5SF5bBlnoN_q@ILoXS^royhP|Gz$L>W6@(GAETC#yL;$uH3Hu@+|$uq0M z&aRKd7_%69kNN!~>b?_~b@XrpEI~k1g)qw&=(s?n_GxYB(A}ddTqNkF23V&6Sw~eq z$B9gv-W4{qP1D;de*v`vLwfXut2UKD_XxoWzq$ zBI=n`;nqHUoe_m_=R7u_>ni+tkBn>SjJQYM$BQ|MD8eX={0TcoqavSEDsTn@YRF5x zyV3^JFE%yW7(+%obX7_I2UcWt?FZ|_WJ-AEaEr9U)Vt;b`_vtmw(i!30C<={4;&Fp zv1n*c9VSJfvExYGDf{fRT$6{eK?15wFG9i>4#4+9`E z$N6h~Y2x?FkjF2&oIb>HM!&Mq2%-CE$iX9_QX=h^bWfG9`0Vy-U_n}!e5rMmlx(_*>-ivT9eR!pLCK zyGYhB4R7XyT+JKSU%v@IZyp^`m8i^g!b%4J>`V|N%^BdhPvm#`p-odsBKseAaO5E3 zCiNxhEd)DtSP(}2KA&_=1fl7OmogAJPlPTes;dN)-Yk3KcO|_~SDgDok~GZydFXP_ zsRNZ=@qHl`=U0k~4ESNKrk-R>q1lhm0waXt_!xAi650(z z_jwXVd%a1nmztN(AHr!|3x&=&TPBNf~^}JkU_Ren=m~c2 zy;OPj3BSDQi%76&_d=wd43gi8~m$6Pn4GC4prJS-O7*P2Lf@#673^j z=w0&E8hQ07Y`IQEejiI4i=U>TCOqzT0r6g37)eJsb=pT^I?`+ywbt1vl-gc5)u}26 zH6Omu+>THkJCbym1)1{il-MY*)4pce!bl{me#`IaPiFgg_Lng9_x@A?m)xg4=pVXJ zgdzHDLXk1XE<{ys>9Z4*db1O=o@0s$}xeZJ!v;C1{pZRll`@Afw!$Voe z0J5a+=4W}0nuI&2AUxOon6Rsu-WCydV;APccZ}`J*8DKc1*t0v)JckG{yRQ`XPPVs z{t$R|BY@9KQbUfsIW#voj<2`T5*sY>nC|Q;J$=jwz>Vdb`yKG-$W!?&*zNNe@<3-HP!rEY)bP;U+81Uj)W>wHiDX~-GfFjVSWO&55WC^bs8oDN z(H#)+dz>-JZS02qQfu}MZbdXmI$SY_BFCijPCnINlFy$%JhtTg=9c9%KCxpL@A#nt z8((hKq|#W_*%eA1tNexdmCW7$tmoE&bT#1z&X^g3lpIThA~U2j1ef$CsdTQu&q*8l z!K4&Z2U{I4bo~s`sD~Qq4pCDCFAdD^F1F|@sYw;rl5K)1d}J@Te0Pz?r(R>y`MR;A z0IFP=&_=3?PmFU1Uz^`ZXz`o(=@#aw-hjOIsO)zmDMfb#(2di40B(*z^XU|j+ewM1 zUnJvBd5GYWDYQQIb<99Vs`}S@E2Wg;W};n}Px;p60CI*{|1NXMDsqZM z+(;A*1Y?CLCMBKQPcz6ZviKB-ebxltL@*?WZcJ3h&ggM)6;-b2RlixvKNr6BQxrssPsemiEilGpS zB1C1g=;Q}oID(u5D;;8QWOYinfH_h{Dl{~|;!t(5T6Z)@{`>Kd!z`W!UdTv#a|hjD z!~CNfdEm>euD+-8Z4CO}`a~dnTS&C@jTNbdm-MaJnu@01MvWKE1^Lg& zv^IPtq;E1HriAxq=od*S?gnO&GY#nQR3V__oZy1wjIj>^v4a*h>OCq<(0}%PP^ET* zW>|!tT?b9`q17_^CoSvO;ITXMNR~saZDywpckK&MX4!>8fT^eH?OE^3eJ336nz7dm zqP?%5ew)`-^j%nrE*aA)7AaHHiBdJBC?OCq@`|7B(3K|=JUbj?v1S%vC2kCC$S{~jojR&d0A9Zu4VpsnTYAgV0 ztOovl`RDO7BWm_+BdP)4GsF;dYY}(SJxPitv8kmNEbsOEpkVlIPhk=(zKSgyD$?Qf zF|76qi%XlzNo%>?E{B_{fqh7gX3PIc)y|>I3J2%j1PsKCz9*9F$y@qkKriq*sUNK9 zYm)(zXknn}KYH5p(WS=XgL9wf+Y` z>;Y!(=i>J#3pwqf8y$4`bJG4X(f}IxnoxjR{n-F)y>Xe+buV188P;@C!#db@`8E5|sMuQ+#K+NxWO zt@1vOTI%5Y(w~sZAK7t4hlWdzVOwQ2Y%ujRS%PbX>u-|Cwc;Rr zSW|qO%2_)8j3esxEa%pTTmJ&%EW8UCo6}zD8uSN~_j#1WdOLnz85*gt-`0NIQns|? z4Z7}Pj7N2P|9#A41)@PX-809jkLu*KbKpToE}(s5rG_Vl(#^Is13-{De|0M8nIZm2 ziHjLw-GT`VkQ%F@HL=cJ9QK|Mfrm`o_o0S@UH_ihFRsXNI7*SXw@GIHUg4@bG`mXp zJ26Mm&|Bv2x|#4Cl?UyB-OpmwT@fGZCFH%y`IVtT4T%ow2jX_mI%r{+?9*KvwYFWq z4nMpOCtjzpaVbSUhj)+(H*hCXo&EHt1co7h`$>M&h|tX7_YidDa#2}s4$A6pDNZWb zSRk4F6H*aRQygI{37tZ2MgK>=%K-i!L;8xOZR!K%Al*`jQT+VJfsBBmHWF4uN zz5SOpMA1`bH1#}B*`2t_(R{lJ$v@Uim0Rg{x6IfSdy$+d;`96d0AAczWS43g0GI&H zo*z~X92S44A@zCDdKH`DvB3=}j&s7bJWyyK??qzy7;U`Zf5I!(3m5S%72u+a%fKAq zC*p*-jS+}l(K(gv(#~2BhJ04T3?j1%2OY0*-2edMX(=AEC$?ZtV8>Z zJZrO#kH??C9b)mzG7dMV>EPKwzKhHRzD1p5{aeZi<8L)UZ-C)eSLV02$))~QU*%ex zT;n1Xvat_t=a{!ev;Tus^ZCrTOpyCkP2+!Y^DN&>=>0d6o&4cAtk|;l0LeuCCYa@E zc(vd8bx7kRir!WV@QoDC#br5?Pi)h67du?)GD6sCfB{AL>-Skh% z55K|sCMfgV=@azJ&+XP-q!e|?2fG-un(7O;qi*y^Bi_8Sx*&V7#4)&Z{xmK!lDXYw ze;Gs7TR1E5iUIWptQIu8(1{|m-D zJpXV{p;<`7L$}OFg%U}Q3{Jm;IjDSbLf`42F}@kr05E-QI{G$=`4M;+%2>foj=I-R z_CT6IcZWV1iQ~b6A*(9mq2DGXHZb$bcP7X9KjQ`QdHR}??^^=`@T)^1XOC!bWnT%aK z19YJ~8e=G&&yeo19cm@&Ok8J=(1-%^ShX6Lw#i(2W~Ay4idd=n=>|5}JE6ted`l!0 zK!m#6=BH}6JAciwP9uBl6CpHnF&1O zO0k^p`iM8YJn(Q-vt=FZT!Bbp*OrT@P)V)FvN>~V5MZx{Paf3CIW2%3J$=iou$hrR$t>$~k|SOF@tE#kPQ9L?LP zPNW}?JbqVT;0Ps|Zu_>TfcOLdM{MRS~g}iA8&Yl_YP)9`I#F zF%J=|AsGKb7w{Ik`|w8N^X)uQ!zZ5ofEW(yO+IpDwzd1ZYwzU>1%+b6@d=!l_beer zz6liM3i-=vlYlifB_+rc)7!mK!+{voa#v7VP_3ckKk+&UpuahQdc5@Z6eOe8b$L$q zapu&dV(V}jUibDZQB((n2IEE#biXigyyP)3delT4^U6wn)FTOOBhs7*23c-+sbtIieW_vzLD6-2dKqnJ5LZH%}DCfBtj% zeY1EUFm!ZTdVQQjWSVz1#f&1?bdIvdU+$QPqUr}Bk?Abxh!x}$>3cmj$1FcaAIHzY zr^^uknzsRqJYYNaJUiQj2v_2#<1bm-&3#vlQnKiY!OJeB%?Hyuk}@gDrKAfA448b; z0pXIH35OYqhP_a9(GGFZV03ev`z;f2pYy4LL%HBKcm8j63iuxmbVE~jMjK2DVb39{ zhH8ju&U%xw+ZZcgz-wVOh03lQ`DacC-*Y&MsE_8YU{;GnNH3!6@tD~D)Jh%}r=u(` z?r6jn#_4vON5aU+>`PM?;hlaEvtgd@q&fr=FvQ%xS!AEqw8OWic;XYi*)QZQerG>&s{&af)~8NQbIf9WxM};8jhD@N^H?z- z=dC6=s|?4a@FXsYD6zwp{2;EHI-3HjkTz@WVxYvxkuirv#WCiQ4MlXP4(_xfue-kh z9IFEm5Kvq%mq_ljSg*>eQI2EZ^xeP-y+YI^Us+?8nYHn<>?f3I{5ICS>`yGjUc8gu|93P}zhx=wzFS3XMXZ+>MXZ2#uT~;g5ObK7 zjxdPo(#ddKkzxkcCjRv7r%=Zn!zl5*$ZdyTI{8vbe5Ri!s8#(`U$^5}MapPbALn6- z_-CLGI{n8QYtJAdcRp<|Z%B&fpFlxOxx*Au+hAPzQ~*L*+?g0y0Vp(a9YU-yr(Z*{ zQ`WaV*0*BJ=<7iJk53d~^Cr%~4q;*U+Qy?2n zwP|Que*Q*(b?5=-^e^_xXd+g=lBg0?CCe;;lvvmL*NE0fvkp%D8Ffe(QB(1LCmw~Q zw}w7MzL04BH^%mcO{a;Q9#V-Lk@ zhcfCLZVG&~5qT#!vQAs0LHDm~mNt>V(W?(9@{1?(w>edtw~Z8!WRU3@9?yZJ^2v^( zZ`3$=ClQ2{oL#IKPcCfMd_vY2c&Y=cnG+@zkl6Ax=R|x)m-Y3i?cx!53s5>MEf_V* zX;_$-!v@EbHC(qj62yK`un40K8|(OvW3~_9Rs0$1>1PljRSSk&Ou~qMzRsFV)NRrq z>~($eB*S$t7#y;ck?yqciPMtoRRzcAVd?^IV#MoKzmV@Ir*a@5Y!y*A00+h18iF-1 zJ3OD4KyvxQ(y(91?>7{O9Z1aS`AmKEvu*6^YBTD~s(#-=J(YZOXH)F$J)r3u3j^JI zP?2>2!3hrV8n|z;>XwukYUkz~dIpiq(d8nbBw^jVv49Y)Im{d8*Ero;n9il*%>9Af zo?16|f#G2lEa10QA zOkx=%4Mn{{(9m|Q9}=jY6uwymj%(yc&%^dNyfzbRZGG29)>Ti(u=STIcz6;*7=4ji zGXnS!i8w}AS7Ch>r6Xayt|gLz22#bnEoCG zK@;}F3^6Pw9Pp+m}ss_o&OiEK8DZH%v zbHdsNjxAn1Cv{mFxosdAD7WI1VEa^B^cXT9OGtWT%o`psYK{yw8UEbaE+1`>o!AS< z)&YtI6*&|AE%VG5uZbT(v)9v^<%C=}ORdjXbf%@4SN}l{Ty);~i%Wq!YSNbk2#lTtV+m$1fa7gss`OP z!skfpcFK0B!qQ(*=X+#5(gpkr6$koN>rqp^g9zQ+N1puC$Euy^%jM(9fcpXQtBWm& z2R;AA_7*<*sgc`H;fW2smo6jJ;68qdB5bo+JS6waD z&I#?8<}U~(-9H~#D~@!**0p#eilMpQLvhKhkAN+jUuMq468V4XE)7<))z`-WbYA@E zfAsvhG7t_Z6+Su%I^y2#?q|~gd37`-wfGTui~A6p<+hagFEVRM>;VaF9U_Q2!8ZF5+QN4u%j z4LrwrnDehfHs)#P*@IEkNN!qa??%lS-$VHdwF%zPi1SDF-A7Hw1fnR$nSgal3i=l? z%S(I^z?k6(zK1kY;j#Y93q(~%tuuf(Ayr@*JOlZBQumJ6;Ml|@?D=?&;aeJIna39K znYr(6x365R*$XZ;z{T~IO*vYi7S*&VWo>cnHFAbt#`B$ouvnMY`@;-bvrqW_?8Jx1 z>m8n7+(-^*14XfCXNovj%t8v{$2kon%y*(?Ni>WSr`35D+gm5Ofb8kDdGQ~8b-TM1 zDp4bX6Yz0e#*wbrsm}(ILb`!7uG0LATI^qwKDtY$aNzOtilJb%`-6z#lqju#Mn==) z7G?M{Y*|_m)sb7ekw)b{k#BwQb$p3U_x)~h`KC#NLzO!mLo=&CmVbk_^3`MEWebwJ zxuDk0B>P+1_5(ILUW3-J3iVc`f`F5P|IMqPv{rR*8;TvR^if47-n2CQ@z=>SR5BT%PQ$J0FqxV$nzx zcVPRXWq#kjZfa!cYt{;Kz|wDW9z8ED5c`t_NUfdl$cr!C_ed>9(9ldm&}7!8&8!zD($0sLXjp#5ie+QPupXq4C+z^7{g zvt)9CycUw=ATqsoDh^mHDf$ILuyofbUN8X;_{K66XVd_365nl>9ufHOT4teRn)=?M zA`FQ--_gqH*^7q|8N7_R%@bB)8()57bDMg);Q=&Yf(U-*Gb|%)?!1YC$On!MM_$J- z-`pjWsN;uqr$naG;t~*;lGqC!>=rv*$GxPF-eANV0PY6ez?aHv2xpPGA_ z`qO*y=QqY)y=^~sc!$nz{W^cNi>fqAWVc6oAx zeC;!o1cnw&C zz_~cxSE@UlHohg$%PpXDh=|4rX>oSnG8sRcAl|m<3iiGEqjBlVOFvN(T188Gw9#r9U!y{)X_&s~36TT$&zNR+FFw71P>T-Spn4$>qBvEOgUB3*`SE>Jy zH4(d+#-|Bd>}4#Q;*qy_-G&q}VP3wM;XKoy><&JsM$nO3rB%AV`)IMh7uomUcT-Dw zGo!9p#qBd%jH5M5>l-A!-DuYGs#hEun(pK?&?Rw`E!V|mJ?zh0L z=j`kW`!>xu;m$uT{RZy{FvqPXZd-|JC;v#~*LYdpAdfdoYCjp@jBA^BLb7r z%Lev2(&YiwJ^_s}Frtwyj(4Rjn{qgqLIO9@;eNf47I;4WPGb{7%PC5o$ z)p@j%Rq;3#8y+{BD0YY!{@#CFKHjSB6`=_RGq3q=$eqm(fNCSbj{*-IkLmE>7uQ*M zux`*oJ66aC+j>$ncVBFmUP*$N!v>%(VNl*pmj96Kq2b`94(5TsLkRx}**;;7fi`Mv z@5{cZP4Q7kZ`3l(B=x@m$pOVGcQp-3yf1PWApC zTfg(s^XqGZtB8inSwblhlYt_#)Luu4+W!86W0T|8OacT8-|-CwT63r3A*? zLa4Z3SRT~PyF6C?`(9e(^CJT}v75*_?cZA8f}nQ`+=u!`|Ei;agE|lVx%X^`&nfXf zGi~Z!KIMo-(~3&|c(Y!3Qc2Py;CH;qSUPCwn$O7zyJwEu9?r`i1&Og{JFXyQE$NKREKU@P^YeFFVw48NcIP zAe$V?s2P{KS?S>8N_IGEkVK<+_HZ$raJ_6UXc$hav=52y`s3^^(Sqlb%?wEThq^H| zOJub3BHI(JK=I?2PXvm%0Dh|AlDw`pr7#6(m>l2-s6T&>;RDmv`|qO1d|un+|y^2Q*J? zk0H+$f?KlxMF>4~@W@l@_!_$4%1BISWeBLjjHm%mlclHO|j~TzY&zehEn|&Y_Y5ZVZ(P*ZGJWuG*q^C zV7 z!~0V&D;&}j^y|I96jHxsw>Kd;HeQ7t<70HE9P)hU6mgo0o`zKV;@;8+sNbCY_Uab< z*2oWEZDQN})_|k6JMcdCw%U>C-_Tnnj&Qt7G3UAw*5n!Z<185vXa0jHttb5#F+7y< zYXQA5Ws3Z_qEQi|*)nfK_O!n(T|5Czel2&QMCW7OO^UAGRtjH-5tBa*mUENxa*;E! z%;@-Q$VMAVd1OektUjU`2pLv5?KxsqyP9s$RQ!cEz!||L*_O!@Jib33#*|Z*Y~-Ms z3XW7}?$1!^o-ib1d|$EkQLu{X=)~{pkeN~i?~cMPvOtyYZmZ=N-=@k&C3hAl5mx{W z6Kb=ApB^B`VSNTz(&4cZqR;p$H})rL5P?Vil{NOaylG zAr9SH0YRO>hO~B6$KPA(fJ`!xBmesu<4M%Hi)*tCoB|9gVRF?qFoslSlrf-V3}lhA z!VfF+oUwzbw$ZdY)Wv6jy?_!AO$4xXhV~`yWUCX!_OULGjr){JuJw%wVwT}3Q6&Nq zj6}qh#B1dzyki#BRLG;Z1Xa*-{VJp_?bTEDB;{{XGtV%9>-Qbj*B7EW-cGkl9Feh~ zIK?~H*QDfB&5K_Nr&xdejB6vKS|B}6hZCxP`4`U6x~dy zI6)Uv7?~}ArsQH&Bp$A-pe|d5TnUG&)wuc)e$l6#$TxTpE^{zX9Z1w{SU9e~pT>wv&JP@aPOEnLVO zXZ}ywlPvo;EaX1*fln%JZcX{P&ju&D;39Pj1YrseYR(hYdy97*+EH=DL_r_Qs8+i% z)BA)=8(p0TyM8Y$+9tHg>)uM$-8|Jdu>Af-ao_zEgoefYAiUX}Zdscb(VJ*Jxo*Gu zBm^U1&6-dZ2@Ox!(-Z|Qvq}{i)rZ$2|BP&p4E$b>E_(W!09L!PX~h|dJzl!dW8-Tt z4Rw3%X6r^EX;3aV{QO(e0v^Z~Gu?q_gSkN9!S4M^cBbTl@YKYYZ`Sc7i{I^a-Ysxj83Bs`ddae5umd>VVnUf8v!m~z z=|bD*v&~bRL`1-;o+U+uDt*y9i*z_O&pqzE&{v>~9UKK~_@SYiJ_7Z!Z6Y(c8ESlU zN|p6xOw_p8IFKAGXNvl+%)yaoCB2tX%1)s3yO2iq=+XNQrjL3apUm=8YP>zD%gMD( zn=Hex9V8$3b`vMv70T*9M&P2U`$QS2XFIv)nCp-P@^jDhIDQ*FUGx{?^L}PFG5P4A z|N9q`1?VJFvh#I$oaAd+=q1W1bRWIa}uy3lvo_=RNen07rccj?SyjyR6*mCQ*Zq{(# zIH1u|-!}e|mUoDKO&*UFPsRtX(7{8aykjI1L9hKwCtVG}A zY4wR0ttPG;uy^NPrd!_0qm6CEH^R zK)dgUhTaHr!wZx!8#PEDb2_@#rx(m~kY`rNv%S1>?+mnoH+Nf!tgNf!O=+ij*?D%>2dT-5W^g|AG**lmV78GzgCWl=^V+e!BAJSq^*a+O zE47_3Nq4s}MDsA6sG>O39EV6eAB4-&N3slnLK&`?fIsO9EZr>G)CP6l_BaMTTmaPS zV|m{fyZ_2SwRl&*uV@`I9~y+S@i`W|A;Pg-1SStS9b~&eZ#i_p2GNksezm<+eg)8= z!NTY4VqIPZgg%c30vtq9eAXkrUGQ_<_b}6lIO0EDooytMNa}))z3+^I$=p{%>yn6O z)xD8yG7G|PMNw3)^?N#DuT|W9YnR2$KKG|4_P5a$G*0B`*Gtmkp$s^5JEpYYp3jux zw{t+uO0gFuNA@ntMw?_J(rl+)~Qi)*=3jD($pm0+C<^~{XmkKzRJwj3 z7DspgQSd+IJ>xY4#0U*gAH84trJ3NLt8L<$KJL%cV1V!D!4q{zfb9P&s599tAYnAN z#IJzrmtKm@&9kJg(A24D$Wpr`YZw3$OUC&Rp8JU6!<`Q{Vmp0;pF~kD6%SQ!4iV`j zDw1PF(Jy9c9lxXYnl_W-JjmjIetjafjua@rWHm!&0L?1N%N1zv;Ud}qQ$+1pYT-jv z=frOg1=7iCrbo3qkRkgtDyI^U0pSm*`dHziH=^09VgX;@(+gRkyBYrKf7Nf2ESM7h zZ^D4++OOcV$00#z>wZIOqE~1-O^M-g;Wxy61kw7o`~(1SpYSL#NMngZooDYZyXYv% zC-ueW-}n7o1r);WbbpO?2i1m0`R}1D)A1dUA7x<9hlUIW zyd(#Ny%NWJu}d!T-(nH4g@^SEKyu!8N-LJny?jka^jn~kJyo3m*sVEp!CDF8s8fw; z*IyG6tOd_^ql}jp{<<$po{}=b)sz|i(W&?3hdx`<8{@Qdfza}^r_bKVre0-Yn zX3FbBV^c@EQxjiRZQE=GCcq#pfp90TUKj6@J3f?nf2sHpleAv`W&LlH$*<#sD%wKs z&s#oG@%;K>HCm>$f^{VV6c z!{a4#X9TbN(F3lM4S0fA^4hn_jO>_}BruhPnXAi*G;&9*%UMtqRk;5H@SBdKtH#t{IwrfNMds+}bF|+&LEVU%~mids0LKndty|CXlb!LyZ#*jQ-h5Ul}cbf`)@Lb-ub6;mA z0`i5I)Jbs-bxD^uuUGa*&OpIWg+M6(;)Gv9c(s&@PgSuU$rAZQ;6!QGhY%DosSO@n zRdE)%j9CR}6&_rEP2p4X_)YT1C^!5`V9Ha4ptl6R(|m3-lkj^H;!vLZlO3i$Usaj$ zYyF;dJl7JVxpA~nrSwWT^5$2U^#j1I`#L^My(#YsU{>XdEQXkFjPD@Y{0^8uD;*4B zdSFuQ!44%l_hcQGu9PF}Z#qeA;I_G#!_=H0*$fRUs!HP$IVjOtKSN$FMg|_EPL6sg}|3@qd%KT%x1S*M^n?H8LK;m zzjcc)sS>_w+AL!)y51_~DL&0iVp$g)v1n0mL#|H=;av*$Pd72Kt>WFvszO2Oj|h zSI)pbfIC^Y2d)Lx4 zA`d6Mw8Z7wzhE^A&tuac7X zFYJdqvmKacBW1L7|I_LDU?=flHLbLyw^!$rytsW0?@-J8~$Xz34FIjr&q}{lSm~eo)a}4(QXE7c2i(sT!5e1Es1yxR z&0HAz4@J2a_za(-)(Y$zsvq*gc>Oojm97dDE;~T=j)jMRL_+0nm6Qg6q2SD+tJkck zPJGFaAL0n0RHVq-_Q+L*veBxy@}DF6ePVaL-XTvN0BQ6c;}%>ModqwzUq$@c7&Y%Y z^+v^dZE&nssNm_Ei)`>(u@#XjmjxBsWuP{2mFd*eyI7dn`3%0LwFL0Rw_@}6OLwv|5+#94d&;S*$p zYWs1g-OGa~^T%sz2Ao$eL#|i5=Pu1*BD;(#HBaj5qIgNpl3L3Ddv?BYIi$0UZ)b1} z4}c_P1Yais?6bPD0DlDLb~cs1Z_jQzN4Ru8;rEBajO$?Ad_YOb%d@L|=p}%^PHyzH zn>4Y0eut<`o%Xhn=9l!?6?YxFN9J^m1);uaM(v>t$dQ9L0gDujHIc{8tWZ1N1NeI{ z7IM%MpdbSk!3jAIR5-Pi0w@m{o=zb!UN6r8z3BDlnsuviBjS5~h}saXciJuxyAAjr z(h~}pUvd<;5E*NqS|^kWXqQT`xKW|J zNFRtMn71?VB`?-eO|KXnqj+jn?J8U%MB+~{C1CKuwK-52D^c9FModpinT{`fqoE{m zKG2>X;@X;ReD5yR9{y=7yWd6e{>Hu}8Q+AT76#7^#Er1GYQSrp#Y>$9Gf`x~;zwOJ z=f3?AIwxVW&{>u|vu&BPrQ<&bl|-uAibL)UI!sDXX=(B#f+_$g8{;4_Be!So=z#sWf6g`#bcZB;$jCxu6lO_ryx!xFm zTpw*B9IS)DWGY9%yz`vC@{`X;; zbj=(~Ab9DrfmD&sPMj_WbGcCMIgBLjlGnIs`0!nRWEz{+ZjLDb(y%xZmeX3MZT9bL zgOs$-a(uRJ9_M&h{Ku&o;M$HPoQ!lXEU*i-6t_%oisWQ}DU99LEjNmSXp|rPvi^|< zBSsq?>-p9ilfqdgk5Yv7$qk^BA|*o2cXR)N`tub^Zqx9fFA)Sc@0cxg{uaDM{du=c z2yvXQk7|pj*;IP+EKMY$&do#}n(D7vgD;ima1X)jA@P5P%Dectq9c3}Xiwer!GJ!K zJ~yM=^BKvb6&)-1(EAuY(!+!K*At#IR0)1XbD}yM#*5PJH>K0l1QH;q=KYUT zrYs9c7QKeBqy{63!nArvTx#k=Np*qcJIHiN>F=Fcm0>fRnl>Fb3yfYFN{=M=h@Cy^ zGR21Sy7b7VhxG*{gD8U-zxJ1iq7>@P@U3QuzLcU=0=*U=nBy+>0&N`GT3 z?7XzX>=4&M6Ebmr&FD?v z6PmVP9AbXss)J)7{w#vR3Wq=7Kr;&w>wi}a`2^$?hhkawYmcK;1M&nuslK+xzlW>$ zHR{b0YO;_T#>SMX{T!lK)YVe6W_dujcBHw}IVz7HR&T}47R1e%3eNw?t1=)Yx@V!G z62~-4ER3UV1K$`-Cia6fXG6u;MEWEhd?%_GEoUbCO9|)vs}FjM)4s7J`+QPu+8`%J z#{BL5W;e)lm*w$|X<36qICNvz-+PEt{sT&;ikqw_uH=DDM8oEi)4+cXp^ww^iNoD4 z%RZ_YR%=X{N{Gio{{zgit=`6%)LW-#&*st^%hmJZzdf0Qz$e&PFPC#;1otJmef54B zv$JektGql}2xAdfRya$VDk zY8<5j)G<`>@UI5^kll~{>*cBku|g_t>dgWE?W?TYJ{@7Cu@RE0yofaxw%NREC?b}c z%l#DoxziUk_UUQE0)xsU`nsT3ET``8b6F-d6g$z2hj}9SB$4u?_=2s)f|+xLT7A9* z5Bb-?Mv5mY-y!*x%W{a3_prhRq1Wn03z^TNIl){+W8yZROxy;ViMMGJQJYHs%#(h@^2HI+;o|-_A*$m(98SxO-qf6jN2{S#-K{Bcv|=n00yqh<`L$J zU1e(}LIJ+~T8en#0KuC+l$G62Ht@sgyeAl9dV}1mZxYjCfhZ{mN%KhH4MtcR$ieFP zfSxQvE?u%k>R(3m!8{hi91UR0&96?3LH-~)L_LJyHWE+uHtP9N_gyUJFV>{9(^p{# z|HqGPw}^=&*)gDeF(Od&ua!ol;++wzut~VnxNT$&N_{RYY+OjE8zsprG&x<1Jd}JE zaBr)){XII_U>r91?1@8AdBwI{NiD2Jc|P>n!;}fI$EmA%Kvr7EJX4j#S$GfQwOi+o z=e7o+ft_}7`V22M-i7C{EQa0c8u$I3-Vs6fw-dAmEEO?*WufFp8Sm2J`xz*rTn4#- z(1H-3m)Db5h7(I)EW}WrqxK;e7mXy!>j>rRyj#-}x#<3>I6ekL{;xFu0Wra)&GPGq zbN=1p>CIVw-`BN-aLhgdG5K02SaW%MBy4nbW6rsKSti~&HntuZ9t_W&2fDOC0Y z%`y(jhnHC=gW!OFFL-3?ksp$VS@8k-vj@;`EZ01=VkEWD^fLHh84?T+`!&Em8WLDv zOcK~Bd;Lb^wBvbp#X!z7io6*ntpR;}X zkz7n3DepUeD+r_9EftON=PnC1QsJm#LjPtk(IKw4lHIOG)=8?qlAAntOqMI68>qXA zu4j(9X%!>~m(9Mgw;>@i;D#@R-Aa{wvG38_>G@B}BIB2tB!h#7Uo{A35PhSP3xgS? zEa#}4c3wp#Zup0UN9Q)nlz#ripvZ?ZcRuYyFTdwMzWn@?xN)cA?;q3~O~z58k++45 zsJ)J;=aRRB--9m54k79pm9Y3dnv|ioV^pxsBAvs@gQZgP*e5Cm*Zwy%Gvu<73_h;4 zSyzN%=b#%t&Fa$t;g1Zl$tX-(%9et+p5R9BLa-8dfuYftSKEIzvIp#&I&lvAM)UAc zfV2d~up%F}vjd-1F@oIv&x)`z)4c>6=KFp*e4tllZs?_`R-P+h(2Fk|F%5upZ(4db zL*9skQ&BWg)HV|A;$(z%TT(~tEHg!bw_dm6J_P|><1)&`%dTah6x89V$?eQ;p;jzo ziu#s%3(ef$b0=NBSebT7_)76hG6|iS*l_>^=!-8Z8m@Ha%DO z!Z;It-;nlT{xczE75c`Z8;1*JoN))!T6;gCv)7qF3ZJD~fs$#7apIF_bIB-^)_^MR z(6}(emPqok5dq;X^$*_+?7-)!* zVZA|+EGaTv9V}NrL+H+5qC4Me$rE;eVHb^0NU6EwZ_DH###4!z=EmNQm_|W$$`#93 zSvAHNBP00pr5rg}t-lR_C9geLen(&CgK+)8il#&_A>N?SSm|{IjeUW64nkUAZmA!@ z-o7U*Z}a>ik?#IB^vll)9@kk?@6G`Q)32T&C2SpeGAcME+m%jB?EFb^1zn;6DXT_v zw!ueHu>F+_9>gCfc5(STbgKvD*Ir&2CII}p9KEqio1c%;!~IaqmRkIn^<_x%i8?9D zj>R0&T_y71)Gz-+a2I1oOK%;nT36EZ_uuD?uS#}i#>^dr@}-7PHx zjto@@BX1!xW5r}qitp{w%Y2<7nR>H>_~$LZl!@?cDOJPLu{`;$W*5h*U+)=JCrrEp_6<&Bu3O0g@H)0u5GMzy3|Urn>kFxv03(9jV%bU+Ln7 z1{sLB-*7CCK-Uu2HLfPM4$~hbAyUC|neylrV6(B-eWmoc>C3|-b8%5O|NpuNX88R~ zb!@vBh3=SokNv2R=`Zg={y2N5q95#MT72usLYqB*6O1`_!J)e#D58qR4xuYur13 z?WUD)4sa8WQs^0^;4szyN6;4xt#TnNJWt~)exYw5oPZwe^KcnP^5LkYtbI3R!GrB% zj?2%1bFRwyDeodPlzDkXfWiemnmZA(jMQ+sFqK+%>)8#{BV+9o`p8;WA+xg}N350FWe#BNqnr*g&=hKnb4*5CWv6n|1iY zDRM~>xNzdTw65nS*b+n+Rv2(`~K6)8;i_J5m$9DvfupXzPg z0Hqkykn2=#J3&~Gu>DhrcbD}1{u9G9`83R-cQWtU!vl}xaMEih2Bp)EMsY|3%c{zV0|BG3 zWkbr1^G6_Y&nA@IpaYIJqgJTX@Ohy+P<>-A3hzcqfT`XuGgRC~CNcD8 zC_PXc1~H{34v4)f!OI_Bp{3`(|L3Dk0&5KyypES854-pp{ax9HO_)_7@KCP4M7m(Q zCP(p>nQK+sA;hMB-!6zH#srM&11VQAmKS=E*;W#vwh4M@%uB=W9A*FRxhV`_P5j0W zKwa!kj6geLjZpE~(@eXV{1X8Mua{euux*)#M%?n^B6<2mzdnH2Cw%n_tQ@h4Ho=X%tty{UX4kZM2r>>$u#oC~FNM8~kc9aj>4W3doOjGU zrl_PAejD>!1${)2<2ZR@Fhe6eW}@LPme*nb;Ld!bLMB?lf;zep176DPu}%Dtlb9Y5 zp||=^T=ZLYWoV(zZ=O>KkK$zUb_5Lz1%BK1W)HAFQ|C-sY{?fE0O-az1yF^ ziAutju=z&HOBIEn%Vh0;-U~>=#a>PF(9VgG@LGV*qEUt}zd23@Y1BwWyZH~4V&nA6 zb7)%;1j;($I&V&%^hWSi9|sY=$g*|EZE0}%+VdjVf^iFtFDC;6$2>URzX7R$MV<)p zP6747RoM{`GkdfTASSOQ2mRM@{kveYCWeOH+Wa?RWoK3+e6_4q=C<3N zf472eX?2VI&!cWvz;-eDN$Pf#*a+i0lsixg6n!yE4g-nex_SHRi4$<2BFV&7rtlDc z@A0d8Xoi`ZuH8b6v%a7ytyIhK-UAo^b+2jR)aetuh4`jnatADO-7>R##8z_iRCZb< z{idq(hHR$b(@T#9=d8s5aOL|sK_9Q}v=)L7p48z{o!TiE~tolPPgqS;sYI!qLb zW(c6!M-!a8+R)xAo^aSDvJx8=eDEx!b#3f<@Rgnt(Z`2v+-lpFC_cwnwdQh~qkaUl zO;a6{Y{Ne&6gsx}l78zj)pW@_JX{{z4J;YjP*I0X>;D=#@h0cO63@7Ihpt4$*E+@C~%B%Q7%LASdLlf?AEQ|e@lL{YiE$;S!l zR=|o_Kn}htdW@_`*dA>k`tG=w11|Pmx)H?ZrqGPL30Pdw%L49?v{9j&Cs832h~`C} zz|i@e(`hMtFLc{cY+vV0+iN^OcM4W*!{gh9JR2dz_la-nc*tp;{aS#5=-PJv*VZl* zehInLK9bg(4ZDTjBx;C}yTKlK?I&%jcw>eZ*w-UjLhsntH(tl($i|qNV1U^PEG1L- zBPdwNa6Z>8K<|oN-hMRW41NA;VL|RG#A0QL9%p&5PAY=U?Xj`&g5t#AlZ@e%y5%Nl z=cgaeRJ6*Sso@Z|*7gTOOJw>_Kg80eBcGg1!s}#^B#9U|@EIhDK|UL8MxPh#74?T@ zo17pQ-%WBStVCnZx0f~pwBJ@h&$+MUdWYPiENfxm-y#e~w<%G(WW7*?o6Zi5ejo$M zz$9C;?D6#bg9T7dq;YWTC$KF`F=iq&+5aBs_n1%95(<%W_*gYJw!>X#@T*|t(q0$A zfG#WqxYmnC=iO*#7Yu6vxh|g^KrqKlvDj;DE#|!TI}xw#bSz~Vr|GLx@_)%N54yN~Yb}`^XJk7P-%VpEz%ls~u zMl8*ng7NFRAX`=F9dY2-QC?HY{L!^orH7liV_E%(On=ker+uh+tf}gmIagIM%a;35 z@7Vl*!VF!(t$Ysf8OffX)sJeZWJt|{hXCZ0SuyF5RWI)F)fa9&i3kjy^+H-!;{Pz7 zJ-t)0Nbvm65pTu+0kSa{=@x-6w^rpJrn+hyT&);gF=#AnDe%GQ?lxhXk*FU zhh5wrbO0Ui^o1;k^-B0H66GDbpL!rmD^v10l#Ime84E7Dmp$0-BJnMrZ13Nfi%<>B zH)utIcjbEIFqQNj>bO0H@sd9n3ZCS67<*)s=zal7F#J(x`lnZTNAAiw_t!}KGUOL{ z_UG@rj>rLd)!?sTU-{_yr?wnEWD@Kh{)&cVGa(;#jDfE9WHPX>t~+q3mRJd*5n8Pr zH;dcmyng;nRTdpUd*nD_QX8G6qC50Lo{JQNunivjD<-CsYMYGeWbKvTx$(RkV08O0 zr-PmZU{bs66U4LUg>?C_L?Sl~N?>ycfs_j2{V70fof6rIgp&!eKJ7NP!0hLm` z*Vds3g#QZwmwbg%W*h&6Y{p;}Si6kD9KZ{DPku=_8N!01MY9vdG-8>Smrq%8a9o)#8n8x@x6ov88B@o^`jZ- zBGl5+i*t_33oSOvf_wPrc82muxO4k|H>`Fw! zz=opc^x4|y^rk2EV5S_bjE~3N?IX1OZNTWWJ8TytIw*+id~{#%pc~YBb2iuyJ@ls~ zhB-v!e`A6*5R+l8<|->k-BwDeP91qF!AY-hE52Agqy-mGLHo}LF&m^l5S+cVLlVtaB-Ckx|%etw;!AR%Qx4)5JZ5d^&aAxBH{bn3fn<3XD z!PQhQ$z0algp&^e4-FRupzZf4OEa;YwkGscOI3Nf4+;pimOEsnDbpw!Vj=c77a=UWJs zkRcO!weSvSB?3~d!JpasAJuP+OEamrq4|UeIoSHe3g|za%<&Ip2=OEmFa|a^J|rCA z$BY8^z84L4+dUr{nf~`8o41lKV}r50e0c&Dg0`YMj47g?#p_X~a~q7FPV060yZ1u? zklyAkhC{F%P=7FKAUK2qE|NgS1MRBoHO=hl=9&7mxJATN6ivgR}IJ4R+%ZGqTkks)Gvr>Z4_;ybo_3Vun zVbJO(d7r6V&-JnNr%PtJn*rX`va|VJTRk>Hf(v$b?73MYPgt-%_q$0_y!P9ID4|6! z0$71kmf=1UF!$Z7?|HAkM3q`{l&V7b#n3e7JyEeWlUX`rNWI-7Tw`>J^FFl3I)RT^ zWj14;VsvnTF#XZlhIJuVEUd&CT)>-{E58g|JJ#OqrKAkQ;f-UpI{S|0B&eDGN|2a^ z6A&kCslXZlaNHAb#!-h>1YTi8=A!W!piS1SZrg z&1{@FQY`GNS>-7O4uyPZtuCqQeb=@R^!*GkZx$#5L5^0Y#}*hAbibVEHmL~b5wB29 z@l>D*zx)JsQHn(toPTBVc69{#VxHWY9tz+_Or@U`{bz*g8?7TP&`39X)KUJJ(p4-g zgaw<$2od!BLV0!*qP85Jhroq+Y7wcE!kJ(MfJ#lWZU0U*XeYp&q<2BzQrg4bxV`AK zM;8otZeasvFCA+EywLM)LPi=s)N$R{c;vA_P}790(wmdlRR-xfq3{!M?7x79%E-4F zD8bs4SJ$FCLU|YgyZD~KD%YKCZ*zNtP=TA^La?q%1{WT7(eQ4PJ&C2o1>KA=)C0cW# z-=*yY!T#_Z??M2fB(renGhjH^HhkNR&U>>BR)fAXRGPgr4MbK_!_bch&Uwe{FB3tS zoXHLq@s#}P4{z{r)^NuI11lTp{tUbUQ#8!a{hrbb_zNs(I1PSuerl*`;Jb={T}1j#={EteB7` z5@vTYXN17n-Gl%*Y2GKrarr`PLINu6@Xfq88;m*h);yTL`3tnOsQ$++{+JKN=5pfb$&{n;?|zx3_mx zS#tNOw+rYK)0e1A!J6n+oxupbIgBXvy+nI_iqlV+;mCK;-s3CR&!h&3k_ez8`=~bP z3@O-|74jq(MRQhCpREXb37GdnTVJCD|D_5CrluYnQ=grG(hWYYM}TBDPYK0IaM*se zyZC_!81An_fyB1*ym|xH}_)+RBI>Ho82=(%Hp$Dg!qJEQ@+U@51!V4OCXvt4gT$FdYu!<=C1EjmO)Zv-l|Dd3@%_BLrB1prq90AsRWA{hNJ*h=Bnlu_ z>zsWX-gYz-BXT5xK_%)A$8{G$9)++Y!3b4)|VOa*lyUkRO-S|rJNbj8wQ1OH;B)d#2keJEv#+%)Ahjz zL`CNv%r3!$UX({CS9EcOTaaY~(TUYy#`jauV6O+|TeM;HYy4G#+Qi$hMb7{w<$+Xd3%$6@b_me%3Q?J;&Za=eu3k{6lK zZMb#tEMXpjHs9E&BZ;vSQ=ZT{uq=y1tcr6MIcQi?Ua%M=82bTYIj1L{!*5?FroWT* z(N@+1%pBop{f^t znz_CWky;2MdrF1yCp2sU!wd^`+ipv&4nfuJW+W$F<&h?Gr(lKH>W-j`2nGjd<{!B1 za;`W2-M9-d>?i~Cfa>%Sf91;rre$h;hLiqSe@aqGepQQbyw)gnfDJ{LS)q2 zp(N+fF4bQo{CwvH!`BH^9fh}C=xYt(i_!NNw06S!Ee4!|F?hb72Flf09@sm1AlDd0m)4n2AJ-x_EYf-p#3e@#(%_7A8(h_4vVk1 z34TMD`5Vv`H@~J1b2gR_mhHQvElY=XsCRJBRG#n&e0$fcPTdLqEvN%Ri(@Ej%3as5 z;2dy*eI*`C2lRf~vqZ(JtvW$#@T9=#{O7>0re7JCJNxWTRimSRh<$8f1 z9pYRbV?&N4nil)H$BD^oI7&GUtO_@6g4=B;+tr~!>&buqX#5@^06AA){b~8paoF>r zzfrZMz{dDLi=B32~s;I-`) zI&n8s@nn5TjUDkbOv3uo5_xd;xuJtb=eNoSMF-#}gzjBg)@Wa?A_HvkshqWQeS`lr zbq)e`9eR-{$~N_^UX?LdG{Khpd?Hs2gZW85TAKqvE;CYVsUz}M2V((cQ2Iu;`Epsam{rZ3$GXUo z`JAz4)PMx#Rs+OUTbl9pqLiJO8bs-Te& z<0De)xW7CYbr}C@KY$xk|wd3`*=Zg`OY!b8`fycWYM%FPn;Quks#IU;M+v z4{aRi4C9phzmoAx>s@7)JWjGZD~_!5WXOfZ-w(?S-@;bmT?nk|^k+|l2?!u;s%E^s zX*l>+SPnnv%D)8`k8;vV(%Fm)*>tBuyQ(I=RVWh%mQ)ivcxg!q%9gI|jSQKz>e!s^~mUjC9R z7wEc>t_@jy`P-_u8U7{C_vaKg?b#xCzy>+kia|bEl!(?t-$=+Qs3lJQ?XPEFshpT< z`56$vf52$4)N_8-L5&E8Fp97e=^2!+D{G_d%;D#SntF z6$_e9uS0p#O!p1qs-T*mHYDi9nfMo2n88J&{77$d@EXl<;JevwCR2fZ@>sq5SYxIg z*YKW>E2oU_D?V_VzB#(dmG&cr&k=Z@^6yDL@?oZUAI?o++HxKD%a!nw_K;&Auv7S~ zUOtZ`zKD}4RotG~MXn3^Hy*HVFjHSxnvI?YD)}6m%tF|L?^%eH8G29V%u2xse3{y9 zn7d1j!&w%nr;sE$1Vdcst&~J%%+n5*sQjsn1%4d5qL@OHAG0qS;J&1HeZ_2Y&L$d_ z0|SkJ73m3G%tVQ?_NcKiE|)}F-d@Jd?6))jWN@DHSq^7N4pe#lX@(!i z_pI=^Q}ww5S@m)wLI5@_LcsdismD2gY0H{|m_Fvukft_PzaY!tvLfu0fhVk~yAey9 zoBL(dqq&h`Sa?2NV3a_#LUySUnT$eV(6$BI=+!R}T)Gt(9^b*&;M)i-#0*<}*RBYw zT8P2eQk_*_iD}3xnpUUgTAL&AYat9l!Jco*~&P^IcOdR41Qwbgf@__F1djGULpP0t z{tSJdh@E&MXG$WW^??7qjubwR-f1pRKCW>R@;axvIYZWA?e!7k(vNof(Qz z0T~H^@x_)rNm5Z!JdjuqLU57BPNa>Ga{l%F@YT0$P0)xc3 zjboso+qNtu_3a@G{bYywTT(E>PX4o1-^1n?H>Db0_%=VTdDqg%@4IWgvxxj98s3H;h)aT#pi7jmB9?P z&Y#LKCCTwFzIpD2#MZXk2UB2*1z)4=aIQ#P6u8%lXb4Kj6PBD)vGOr4Q&upQ#^gtG zAwp*o$n7%VIS-;%gYfHTP$3~IlO{JFg#h|X_Hk8j`-eh+s&EK*RyQP*`QV7Vw&M%S z(IcW4ES+n@co9u~o&CMc;SHbdiiP02;1+A2vc+|Uw)??g{9)I8q@qy6)Pb3IJ)xWo zC9Z;>)M=gi3Ww8=AuO`sLHgWIV^3^*H1+chOD3XGsQGpT>QN;~(?$co_c4R35OMf{ zs;r4tbR~flRzyjKhi!Y~B7L#}rH=xFGmwMQ0K=@&JPx zAV%@n@`OSN(3QBb{XE&xVxLHiQ^ht0>`}K5J~YoUw^N7TYVSE1@cbqp?fZBy7_w7_ zFX@eKXdB7Z+dHgchu4Dd?*@o~Vg;G)L|1qVgloQ`*{WGSV?zsx%pws^yoZ=1`TZ2V zi_6y=$luZIlYlXEmgK1Bzpl}g-KkIFLyWx3=OSgYjdDC4S6M#oeb<4W8#mb47d(BwyBp-23PwT8+lYF;CFloBTd!QHcx4+4|M|C9r$&Em zNq)L^PT(9x>vMX#P1!PZYJQ7)Xp)sRxU4+7W48Vse^$OqHof2~;7x-4B5BzuvY+ASj)2I)CIbs#`h9DPPnc`8@YfF=hJr9QDwqNSg z$e$A*>_6GxmCnxC=U0rr=F9kf^F&M) z$7x}E>*0v+>33Ca!W3>fx3a=e$kJ6?{VzwsCfR?G$%oFnkEm!O$<+(VH75dM=+o98 zKQt$_{z*pb6uDCUXP$g$M|fHAOo$_+t=0wpll+``KEXkYNy&={tIlsZ;nT~{cMl=p zXEp>Y{Bx}Ya8X(Cwo8_Zk)e_Cd$-KUPE1w=|I7f;KrE1Hnia_XTFuUB=2^W*$dZ0Z z6>Aj?9+QfAGN1qP<+kffYO<#*Vw-4rwzDQHBnDL(NYcbXfQ1P$Ru=Xq`ohrVx+V0S z>!?~T4cV8;ZMBiS@=XAYSsNb(wu^``NNC&~SrTBh3NAT}CyvyFK7+2WxPwE(x}Sc< zCs?w+*izsTfMJq!&?P4R@XdHO^7*Ix$~|CTx-5Qe{Eb<$%<@%WlDf4Kd_ON;er9>` zR%Ts4Q{hOUgZBn$bdap_GzbLKEku8#(iaI5Ak8XD>g{zKIb3e&D80*?aOfSwI-$8w z=rN{)y?b=(p2AQ2jwZixYffV!%nR@kV}nl>aAg~&Mf>-hS>m=zO! z+J`GL$M*s;+u{zynWg?_zs*6mm0-6o$LoUNTjJuu4qz-`(=7N>*Z|QBm|T>NTYkqL zB}t1-VgHYmQk{N4`fTkqtVBwcw|usai@roJi(FGufvxj(LdXP{+rmN6NURd^x$s)u zMw;21uz6*X_r2OUSpC>A@zO?5yB&qW+X3EJ)afpHVqGUEjzVZD!3Js19R+Q*iLb@Q z&3mEFWG}QAX}VMA*7*ot&&@SGy?k2x@M$zVhg-e#K+>RX=#D?{CNdf46`-3vQFnp$ zNLe-32R-W)KjLohs}iK@oPLgj$4tocS92!as+q{cVQi>6EC)lWbE|M70y0IaJ^r}_ zA7?E%^qlNTqD`#t5eOUMBshFYM$@Lz8&1*$68wfL2e67mw(}QA?l^;XO?t#aIa=rZ z;6@>dPbtnG!R#o%MNIM?8YZmU#Xv z;Y9@oV&F5}Hh;y)XbY`E6(yZuo@mn&+zKjAIJFgkMV>{E>}ia*SPLr-(V?rCxw%fB zG1jCC&_XIqk4=NUCB7n@s8;H9O`C!RN}*pcwNsZryS(R72#Z%WP4kNYYqD?@3f&+7 zDn*37c4qiK(1}xJSD8qV+pbjC)s_$X?RME3b}%DEfpDP{ZyF+fuucDz@?k`CZr28c zpVA)^u(BX)h=|^>|M78dbiivlLh?V$kQ>8-&&ri`?L)Xsm7KCW-Te$lyR{&6w=N!D z*h<_QQ6YgZ_J0wzs$cH4(p^8l_QTrh>^JaC(KPH{Sf#aJk4!jP^Z?eknQ69FI6@|I z4)dSD$#^`#0;bva{aKH$IF~hYKkNKmypO2fV*DC0e^z*&@tl5<(N2$*>!mnUHqW!} zW7zDHcOxNmMK}XVy+?s}*I1p#K#oOo(dV3&bk{WBy<+`W>2z7N9m=`z36mf$LFyu* zCXmmNp|m!M+4A2W1`-EnDlCwn)Ax||#kGDC-Z{a-2QEESdRDp$e0dMh-%?Xmn3dzT z#5WsSqC*>!T+p3wxykd1g2#!;(r^A)7By{rPHD0vW0`n%NB=NJDP=Q<{q}D!I54o! z<5-}2xc(-ZWk+vI)Ydok4Iv{s;M2w3U&^4~{-;`5a4*%>TNdYUteav%7uIeW9jMr_ zl6LPKJ418p7T~K$w_29i(Scy@(1<`|P@wZ3YGChd%_C`iusdatS`{j7AriHfP&>E~ zhhT9K8=#PUW?_OZ9^aHQc*2Tp6=q6&KwK3ofLtX)R#BTp_ z!}+W3!7w|Fd&A-!SiR{*H!-;<$WyYcv^ z#k#D<(!F8SPwuhLCi)cPG6WTZhQ$JWTy=W*IEsdIj1iD!TuS~^`0T+s*+SU@N2(pe@g_LFC?^TG8hWmk(u_O=c~&r~cw zq~Fp>gXO8-5J@% z9KKw&_P5E`+g{31?fe|TvlGc}zWu#1W2nmgQR>LCzG+4mh#>t^^!GQ$+}3YaR0EMY zxQ+wrX_v{Dzo2l-E4-YiUbIhMd$DE{I)?Qe3Vu3%CggFc|EIFKd6N|c-z*w?CHU-V ztid~x_2o8{o69Py+j}Aa*yHtm7=2WU0%>)jV!Aj(;6j2URU!CPX3zcUW5|zYul?2; zJpWG#5jKqIb@oR{4H!OWOSCel$-;<;I4wEM$uNW1J-AdxsaD6A zCU2WZdtISaC}+v>lwb zI2lJn?K%(t^h6puV{@3q$p_>#P*HCi-~Yi(7S%`nKqWcA=UMJw zng0A+q3IQsR4^$_3Y+3~4oF(x{;mH;Mkwq{<8f7g-E1UdT@v#kW?VKR5y!kw{`~fK zyOxiAM!7j!07p*cdVZVhR=)WA_n%0u;$F$D;ly~z{fgi5=p^0_ZH({3onFuQyGH=e zhsU})l=;?uF#%sWtJ9#v4UGK*w@V82Nsdut080pu1VNX!f1xwjqTrJ#l9S<8H#0-G4p zrGqPYUL+990m|_I;Bhw}9|R3b6keT(oCHS)@oI0O_}>mMw~vcFWu5bPtNP)ui$UW1 z>%BhX<_$pO>xC>^d%A{>7|&6G@jonAy(7i?7yD%uY@NuiImq_7QeKm!pT+V7cQA4w z>{ZD3>M$rRGBUV^v1ushWLD@AM${zaukL86^Ru_B^v>0eV>im0-7Q7`LV00+S7Dw` zEMw}zB+xmwa#ZY22f2ChT0XZdDk`6_8)dN*o-?SKO95==tP+>?H+C`e{HavxlvL6bkj z(K-cx_H1@hpE5GsW9~Ym-l_>a$e}6m{MaOA$?@_qE0(*LIzZ&)i89^f&?L7!rO)~DLRw94M0vE;a`I((~!hwT8hO*kU3m;JmLt80VK*r zh$(@I$JJ0)*0L z|6TwKStF*LFyOXbtMcnvhDkAgl+NotU8571JNJGpYC)0mNOQBoqgg1oS?G~5gM>1o zmdP3Z!N}tGD^*m1kQuIUKAPgrc7U1wyG8AN$t3g;+5XJ|{P^bJHo`$2e(;AWg9Ee+ z^`E1m?tLi5mLY)=l=o2O;l zuM+>jMtN4Ym+-#r+E;kuNo)D1be^eY;gnen#A+&jmzKuH%DuSEaq;di>@GHb6L1qR zsn`15;f+{jRfp8n*D)gdob2>>Wd{1A*-!SRVz0DssP4R7>pu2KXMeM@7&u@s(*Dn0 zZu8RX`ucmfgRSE11=B_iZnOSwnyl|ss#Jq*1=6)~d)IED7Lz;6!8`jdeKVAb zgqZWu14o{4KjpCb26FeSIQUIyoIWc(sOy*7JBs<5udEKKbnT~b1mHsW;G7J(B6zEl zrbAFeN(~zre`fq{9P)fZmjsw~znD3}7~5ri<0+yvX&0ZHaipC-s*%O!XIR;SPEB!2 zVCItvK76O0jD9LtbY>M3rcZug(`zP9MAF2_DIqfxrw!Dg&mL^Ih{}H#r*tKc<%^ zAAW(rGUPzRyB|papTV_hoO&neBg$-ec53T{@Z2%YF8-1_XnYjJ=V8n~nW-jFW?mcN%$3Y-RFFfx`&&LZwpV$dDHtD*kG?7Kl3s~q zv(s`RF32j;NZ6t#U|H?tQGT{}!2!i8Ea~gk;6#rRrmLD`*uRPu^OV>kcLLZLL0i02R&8EVYkt zZ-2bkI9l%T)#h?T%URuWwc5Qc*e`NvvWM@n2q4-+U5moasXYp;BPA1mLg&!%5OA;; zY4;u`OW|k4lxv<;CxwGcXos0Q`+xMo;e_8wonEiV^TW5jh4jLxM4_J?ubF~s?nd3> zafc^$(YGnZJD>yg~l&yG}9*l?jjVirajG6d}#Ak7M zFh{Ap!Z;ZTU`q-zP{pgQL4Q$_apWN2C-r#|D3K-*>bQBKuCQYW60` z`Q}5}StWK9AH4eq{ufDgL+1Y`1wpeE@bqBl7V}4#0ACYGGQ$2UFP*6CIHJ6-VoaK< zhTw@=pa-k%Fxa7H$@{ar%~$@4XhR=Bu9aZMVga(TJg4C2F}qH>QzU_%SY1M+WY(SI z2lrZ<^vm(;ddGrj*qWnn{Z2y&nDn`^Dgnw8#jHM(Go$iEqp}O|D(~kegSW*m#Z|+o zq3`&hl#PIRK!fMDq@ej_iTULeDR9!7yj(6nOn;wsH4H&orr+qZ?D{0fnoTF4gTW9a z4A72PevY(4*S^6q>$iKdg7x{k<&winKw!OU++r}7vO1ZWw0=CD)0@Vx>)7vT@13xVd zl*fn>MAb%7qBxua{g4JKk^>gS&R6dljBA~@^EnGOzcpT;9R7mdtQ^)yIy&C)QNM^C zHxg}u3czBXxBxr9_JQEtt!#@sXa*xuSR@x?zm}Rn`6iLo7fO`$1Q7*Y#`L)XFcbB+ z@6Gws(Cqy|Vt2qaC^Oz#mA#w7(FV41o?^kKI1#9JemYZ~kc-W=C05+O4D~DojdX$^ zUc?LjcYUxtyQ%o<%OKZs7g`dIIQa27u;+lD7S;>Fxz|&XNkYA1CfjgbJ8mhIRM1U5 zwmccJX7@ii!F2AqfomK-d7Cu3@%;2+ueP`e$|Jg1et>Bb!tw2kPy*OXZxHjoR)xz7TB$ zz1AHUn4eq&9|{AS0s9OOvI;11U|Apo?YO!(h$Rzn1LyQdQKvQ!I1-b*{fQ}GL*)Y) zQ;APG;iz5;j(_e45LDm6Ls0nV9}Z`Bel^dU z%ow!!Q$&txY>bEwdG9mQH7h$4z6(A+V<@cq-mBsZO$-76Em8+);24HEZFe~x{oX>; z(?D)8MDt2QPIRQ{6^8)8^%C_a_#5nx0S9((-vOrrMy80{L^128oRHtI*YcpU3wey6 zCW!8N32Ge(?6s+>>46cQVVOC*5o@(A;RKFrF*M$!AYgc?v#z^}z?*M@p}iS?xjad_ z0(M!$b_292aMGiEz2wkYS?ZZLD1k3ne{aLs)GL^qyRvOPuzvc6KzslCCrf?&{D~#O zsyhaP(Q{(JYCmMpl8O3n7IhkeIaRQ)<#AQga|gX&S?!K=g>vA*7}7*+CP*-6Lk-66YyBH>f9|n2hJv&B zJe`n&fYPd^|57nm!ld98%=)oGT>RPA(SX(Cg(RB6+9x@==*i3SL;tCZ^+2&e^v4y3 zCp7Y`@^jsGF_yPCz^9H|{tqy`kpAKV-dS;`PQ;Ky<3IHwOCE#!I!Ha#MSsRx-IfWU zASmzbi9T&RK6&Z(#M0vUdXI)podpwCX8i&0pdox^xh##+2jB@&zHv_>2XwkP`(|!& z(W)MAUk5<&!*~C{xn5-Xhu_jdhF2K(flo`No5z9Q{?nl6l;*s5+zeD8WMEawjn3Io zc;;$`v{Ae%doINm9+6LW@#w7Sf_X1VyM7`fxEQwlReTBFnCiKE@h~ltcV^k=a6`cI zSuDo-+Urb&b;PS!=%N81`$DiXX@CFa-riQ(xh9d>~TG-NExG&D|2*0jM+hc!$q&W>w4Mw^+%0tnn#fs2~J>D zbFke9r}z$y#s7+-MhZa*Y;ul`UuJ`SZcyp3jj50S{!mYN>vex`emNJi|2M-2PAqo= zcM6f0Ih-Pn0hN4xgt;ZP^0 zf781Tg=L)8EI*RGldKVudbWN&M{b0$l>%yk85+~KZ-*>~D92|iA@ITYjS<60RNORy z-8pDPM#fBM9-I2`+NX&XoE~kAKSWNXLSYU_aOf6RK0S*5SSp-8X`Lm1^fv~XZw4Vvm_|GPy&fx$dQ1KM{nE)PdgS=0F z?=b!0{5pLLoPM?sSUoJ_ZJ6#r3y?*lQ0=>D78YO9jJWlMn$cY&RT|&R|ws)j-hl=&}l)&{$N038Urs zJ>^e{T|7^WekYAv9Ma&^)wQ)Mfvkrj5(-@p{78km$c3$>X%UOS^6qg$TFEFENe(lK zpa>#v!onAJeJlmd-!v+EG?kY8J<~Ucg_b0|y#KcN0WTL617OC2^hm|70Hz)g-_?)m zf&)2Xxmh_KMI)%}(~$4zjnOxZ0dKNB_Kyp9UfL`M{WBdFsbSkebK{8`D05R|(R*o> z^c3Be3o)~ZaLbKwt(tnMQDtUBZ?Z`67P{;;L&SnAgZ-bRB~fCtT`uP%w-a7hy?!Mc zSsdtux7$)%F4|orw64q17kX;y+~|j1P5)PAHAC`C$ERzvTDWg@>0dXooX^=t^lFDM z&9T5-uN05h8_VT;B@W#iT z*W(vRgYzF%W;UQfXXW1{#o&BIOB&XzL6y_z*H_8Ja{f280%qM1O^TXOVXQo@Ng7XF zk?Ga-IQQS6R+z1X3A~+uOOypNBda_FJ=7#+M5HYrF}(zGu^MjEUegYy5Y@~BW=E?Q zy%&Ecp_QPXhhSV7LRikJZ2Wtb;6Jja&k=}?d1lwOgfP*A4*58*6 z^0`F~^f2wGc=TflTD^(~Ct}i)7&W&lLWkdSt)q2cJ!qxC`q@9-bW%pR&Y}$kI>tZ` z5to$v=dL7b-hOy`7VtYR$LkyA7tSLjh`lxs00WgOa@SD{Fn_BTaxVy?4^m0)26=vF z6azCPtb%sJP7RU)h7d2{d*rL?;jdl{ypcq?Y`J)71^2+HNAK~+bN@4SHS4!uc@Q*? zRc8c5OjJO0CnIU_Q`Emly`t^h-LXmNpfs2%w}vD?*SSF=iBVJ6Oj(_`5K=K86LuaM z4ayYl7lRwb%V9q(8~@XsjuTPNUVm5g$Gnx|HX7{?G={{wY?j>5r)ogZ_jo3e#iAJx zBxCG~L7K?nqN_i+DBFpk^t}ssTSfC9?wY^9G(?6%O_kSo9lZS1)H}JOx0oA<-C?T; zox8g4)i0hBCt8l{uC*Uu35+R`$=ZZgG~AiyW%3fU;CRrG_dnZygK03LcdhMdbacS@ zt_d2!{;r6e{Oo@Worgcve;miZx17C2=GikVWxGQm6iQ|NGBdKtNVr2nWt4>Ml$}*( z#u2jjo@ej9oxA(_A3l%w_x*jn-|y%1E@VJ;T0xia!Lg>#U!S!MiSC0A>%O}g4snBf zq4B&Q&LVgsDHrIu7ix3czIuStMglNDpX^lF=@TYI)MZKAjd)?wSp<6TAsEm(jsmcx zo3}GBd1C86GcoAh`|!*HoN_7u0h&IQukqhIBSwj62o}Nyw3y*7o%Jo^9c9!+aerD8 z?2<}Bh%eGG8;>|ThJC#eF<%3-FLt zy<*#UY*wF~@)!i}#7e{VgZKpgGJgrb7{vMAB# z>3vU?TwXshvcxuWR3vhOj03-j0DTV_O23Fd03_Rg=*a;dqK($`jnXT74CV?(#;#U_ z(Vw1tg^+{dR+sWEdNhh1u2E;6@|7vs25(5R{ZJ3{TNUE6`oO^pO#5@bxydz^C^_21 z)np(cwa+M{HV}KSN(-kc2K92jhmU5&&GbWupUDqDoba#J8A$kvX;DAd>-Ts|N=BNI zV)wZRpor2wRh-&?VPqi=I2j4G1X3x}gJGb5Nk*8ql31QZSMHsew*7OinaC!jRtbix zd9&OSI(sUwxQ)u(-iRmP)9EqEzu45;jM-$|?s*V0o4y9jKKmi7);8x+&I3QskpX;E z4lj0f$#kTx)*zispGjThi1&aR<@NJF zpMI9RTVO0mc?MMX#5Hi~H*+N#AO-+4Qh8$>0xx55zHmT0s4#tEdhFfS1y4subHbYz5p13ueR1%Mi=o#ylU=9LYo&8|Dxo$9kh<2iT#M zik2>cX;YpPHHyNGaEInyAe{w>b9~K7=x(OV8yzHf88iqG!Ug@s{dNy_qXu|!B7U@% zLH@hXNS|OqDwrJZ_j#D`qKFKtWUTLfN#`RQ`_1xy!u4Yhl%`E?nLHd{I2-lXX~d>? zx+%RXd;vD#dO0-s*8WUB_rYU1EJ?I!?c-VReidVCq2katxQHu=tf=TadE|H@+Q3fD z8i0?S-hSj)`yzisyz71bsnxDJ%xy6}Ohdb!+J*JSm(`(cDiNM0;=YVAcN%MCrw;9p zV%ouv7M7B`d{l)cnwvZcy;RZEkF~3`1*CNeM@NfvlT%XNOdI52v)Rp}6z&v@;2YCZ zDlb8w-kfw$Z9}QP)3K2Rp4(4ghZGuSFS~K70h`G$kilk^&OHN+nDAT_a^H%Nd*#=9 zOj7OZgw7A-7?$mWL9~CCtq%<-skUi$Liwn)-Y?&CM{RO92@OYVHiMt<#hgx%V(*fU z@u$1$o3!T+|J1j?fTdvinpDDAty1QFZU!73`fS3X@q*=Tm6D+UZ5rPPcZ^E~$yM79 znn9oor%9(~j4NuI60T(}Ee8&2erwmjM<*?Vldwo~XtG13tKWs-9bl&dih!}`Y#Zp3 zQh@%@!TO|dh*ObcA@c4fV6t_6i;WWjGNh9D88W%V22ksXUl`Y~HbHU?^5S z;s!(vF6}Pw;X<;;T94gL732m4fNpu`i>NLs?tfk3gr66W&Td}#&8wOSk$mwv$3;84 zr#WjGiW(N^_wci2_l9QTN9aXqAe0?-hqzePs*}zg6oB6f36FSl@qN4H+^M80_>_GI zt9+eIgx@xjdXV~^AhbW``R`4DJ;f~Uu*l`JOAL;^p=k*l63I&6WDcA71^&-;=tCBJ zqS#p6COz<-g5JUPHC!Ft5119>C_Mm&UTmKrkoPkoRVu^0DfYHYy~%z_Iky?!d19;U z{rq|B7MCZSCY6EX)zClbi~8Zt=YJz?13i`pN@8#DEqOiZj;$PcK#WlHP{(I@M?Xo2qdF zpQCxoWw4+(`KnE1u+=LV&6`CXS^Fiqhsk&6S!W2po-mE|>BECv)DA#M3YhwT*aB8P zum#d2?aO>7`fGt~gaEJM?b08Ve-g)w&hbZVdm%UC#v21g9#szTs6)JzK(6RWP$(c7 zCQ($c*Dut+N{?OolfBbN9R#jn-si|&hM9Cd9<`_T>G#ZCtRz2SV|D@Po4LnINsi?2 zY#J(4wy!jb>XM}rnF1G0BU9$Fdo0c21MB>I|L$uL7o%EuMaT%{X0QB zP49-t0g9P)+LBHr19b1o+^s8s%l6chNB^cifQoI8^^ZOFP5Uo`u#J(JGFsH>_1AA; zz{f^k2-sgPn#n=TS@(s0c`2H`=pKteIyfn#@fO)d^H;}Y@f*A;B_bi;f7I~+$2Dpq zseB_xe(Cuy$|$TJ2upjyo)Xu$xFk$p`AMsBVkgXAsC;0FdN!xLK~R+6Vhi(2p`rN7 zOZ0S<>}FA770`;wqZ$`uGaS4jg~iSdYTo0iXl+o*xhMU^=!K;)*Pop$;2h$B;ksNp z(tG>2y1u*Z2pdWC;57rNofxmVeYkVLgf+CIsE=p5-=ZP00Y-!ldE2EAQxr5d-eMu% zz*FEk@b49bEBzyh_t?w$5!WlGD7AM2^qCra5^H4audPgJ)Jx^5`%?0EMW+)un0uDFC`yNU~{dl=vB zk+G?cmTcnM+^YoR9KPTBkpu7AntNTsTSHJR63dgLKiT?SH#5FS#mW0-n(IkCeSA$-oCoptZ-5+A<*uv+t|BB;tiW)%vDtLh<9&7!wv< z?wXhyr?EI5*&qqKkdp%4t>6c6RKuw3mGbdw?5jHjJ*VXOF!FYUIN+D@??OBa#Lye` zkrT66vrbZ^f}%oBQQ{j{abLRf&tCvuwG`!ale`Swz)a>Ubg&wHT@^(}%=2bqTqC0d zL0on3$b5YWfEQmSI2Qg_(?6@H8!NRaOx9=X>ASyRt1<>-1J-^VS1GsYUCxm%WWPvu zRerq9Cj$h|Uw#)*?gDW<05?QgPx@5cZ`yhHm(^skwp{bgvDM zy|BVl8T@9gSC9uqdWUoVot}|NW=D{a%bRNH%@P)fq~pI=*{bMVgBK5-w5qAL-r;WO zaqG*FXfXt9Io^=l2*JEKc3^rN@OKj~`wto#Dgpn(4`Fn)p{5XQ@|1;7e~SC6$2d}a z*KEKOK-%xhLn0qyr!4lKCQ~IHK)r_GRxAd454l&4o*F2W6XKzwlSN| z|El;y<)o!CLI`sf^XtX#X_FX#BrobKDcK2-o^xO_QctB?d`EI<)%&li{r(4Uh7p+; zSb3Kl8^)ANtYhQJtES@x9RcH#U()UZQ4Kt_DCL+zu6pEGv(}B5Y<4!usUNi_fp_&ZW^?m3kF3)8dj39lhs3q-?VOz_7Y-;&MIw z$6-b1hJ2;NgNQ?nZY0r3_RGGxZ`jilX_vh3Qh8(tm@=ZN_J@bGzt{b*9W~y8QQ%<7 z7*?U|N_WY>)x}zGF&%5!j5@6bWSF$UO*?1+*>o1^QJYNJxjZ8yhHKPklF=|iFExFI z3##^=ik5jhk-$`s92kuEjrhz{5V^Y3ELd&pWd+&k`+KuAyL~lYx0fj^rDL=E=oQQz zLFvfIURqO%fL8o`uj%bQ^yn{_w=LaMR<_sDhj!PY4vqoO&o=P8jg5%Es~a5h?rNqz zpVoxM_kxzs$=@fd+urkTf&`E(XBz{&pNq*Z%N-u?qR$rlf_>1S_`$D9g?~t~BDWd$V)orG;n%$>NBxST(R=gEG)wN0t77N5+;X(~!=)*AKdh6TLg(2eIS{G;h0ncDy-823-B?7V!YP^Vg5!6BifQ6)6v}azl!P;6E6Czw2stRlbThh+u4#5w38COIq zbjK3Wl9#84C5q=4YexDcXW?8%>Qq5Wg{jdp-CJz&v^zX-#ip^2Q`IuBI)R zh`j?Jv;e?2Ea$Z-FV;fzjHgc_FlXZw8cU;(*#b6$|#_!1z+us zfA?hwNyRNn;6eha@yI3kv&c%g)Y4-gGPUg%^1U|`4=$efY}_=^3z@FJmrb|Vmz~dF zNtJ9st;j7M!}|Cek7{Oi((W(Q7l6YkTp;1Q{iA2O0Xbp6ZoRV1a{*Q~@20)<8CqNF z>>MB7lg_#%ye(g_6?dlpHTJ}sh}|42nDbv2bG`Ry>)Teg(Tra<`rBPiyMpfwRB#W{ ziowJN`uU*LK}7D}?gKs@?!Az^>X67R;=s3UG@IV$H7KImz%kePkaH{eV&_id*)r{W0%eHqp+Vz?N;Yr+ zT}1l@6EMF(-CO15n2xk(6Ufxl=u3ng?Ks>&8pRMfA^Pj#5zmhpL}DFLp9-3l6Ak8u zUxzpa#Mx)O`CC3#yZ?{z$Xm7WLRmq8m(=Lv`Fo`=oupWok%1(v9A=Pa=7I=*-$|rB zKT9;huWG6{m~hJw_h@u%5!m{`;g&M6Pxs^ZoHd5He^V zK~Mn(g(3MaJ1%YKu%o+r?w$mFO>6H$HlAx11D$8P6Qf>B!3JM`o84GKtTtBH&|UJZ z8fpz8R*KnC)|ycVycUs2-cLua>}Vg7_GVT|B#lOD&{ z>QPC~embViG)RHj?a#GKG;}XRNfjt^GS2T2xCO_7;5VeOQ{gcr?EOX{yx%=_SJOq> z2y6(;;80ppPYp^?$_$aWJ-jE4Qo#PStg~QIot3=zhF`nua=IcRVWg(;X>MZf(-@Y- z8B1>=s(gSM7b7oC?3%+*by2aC=eQcp7@Aqoa5(%d77)a+a1V5|9bf$AvGG)szjJ#P zwbn@5cR<$nRee&BIHlccuH!!+E122IJ|ENO6ZL4xYtJxf2QRJ5-N9&NS@6yyBf`Z% z*J1z$?5b}r?IU@#!-&*dCXaW5r#2Tw=6)uGU(cPS?*bD$=nSf{d|~l>1tGA=U`_~%uGpl@+#=7fUHGhC;SR8j_PG9Zm zz@u@$7@C=l;Jb7zA@(CkCl;xv(nZZUi~!)EiNdI$Zbo8hihJV1bc?P4s-l_%VLG&aZ{HeR>eQyj^&xB&a;f{Qe|*w#7eD|hb))HbFpJp#=-A|{ zRJgm*`&YfYTA{GvL*w$&ef0OfS7)?_-Tx_%mP9}dlXO`z@V$#)T*`Tm7{Ql3bU0qO zJ-|dQciK)5o=Igj(C7H!W-IAtYBI5#Bz@ATWdq2P4_kNVu$>9D5|Nd}jHBdjVE9`V zj0U)e^#I7uJLuM~zj0yOMG6uIMiSm(sMn`0?{EMhKn<&e4R0B_$6i0c!1jduH?AKN zP$zTkvjdX`8_v>;(ISm2bsuIiFP&~g{IvfI|Na=h2q(v_j~O&JO`~I4k%9p3%+C@@ z_>*hWn2i0;4kV3E1B9#QzHu5(F`)Jz%A#XT@0q|6oFaT!-|DvzD~7AaM<}ALKn(tI z!q$wa9YH*Z4i~jXrF$}}SzOl*0Kbtt@A!+M@o4L*O|Wao%GN5g#8;=Cns~3MSl-$q zBv69d8Lq;Di>|M`k#h;D1;TM~);l;3o7kZ+iWU5Lr?`?B>cUKrm<={|5i=Yx>pk7) zJEW;-a-@|tcU(^AZ|{MM9fZy*PfpBw6}x-bd`+{eg|Xjz&ZJwTcSm6b`U8<o0 zf|d0OPxZ$+sc4)BAJ|X++Gs@J3PZcb0<+jqrAZUHMRJvqmYa%&VyG-vqGun&j7@m9 z{kv7lbQH{NugyDnj&rkTn4C$z2ju`>_T>YJGJ>~yzKyHf^8C2QzoWs8kw-Uep*43% z9wn@^q^48if>h&aJ$KG)t#=f>ZvG6(xV9ot8D)i39F;K(05Y=4tqGvYGn;6!28 zX~!B*MKiJ^fF}=OSU=;61YvXozgyo))~L=YereJk#>P*MKbL4l*I@Pw*evqKbL_}X zkNTv1-xB^AQ*r@RUo^T)34_9CSw6scnJ-Tb<~n8zVFvSo6kdmBE^`!3k3kHf^yK}Q z$6({DtDtWaNex?xN5b;3q0aT&? zWwsZT*x6vll^tefMpnuHBr@^6K0U>@NJLE8DMsv#?X=#MY__ck}nH5~V^xePL=e3poQwI_QWoBybUlOu=E zcQ+!&_x`W6;JoCny>gzjT;wg~QgZ)3L1S)X#0-Ru_BUKxLshPY44OBq5#vtgZbnMG z51Z?o`z`)k6+$nEi=Ai!hoCiF4Uf^T>94R7(;+2`xei?weNoR@!p4zrglohMVFpnX z$Chm)vjEy;H1-jjl_)Ok4dAaWg@N=R{X$^989-4VLKAImzJ4Stji>NfU>lW2d((VH z`l+4fqA7bW&TpWCRxF6EB4jz7m~-my08D-4^OfhhKM4^IUh%iaw|O^Azrj*t2C^aJ ze@=G6WOvf;7pGUgkIvPzyLfV(IuqjNY(o^l`|ZZ&zETr?TRwgIAIBmj+wm-%7GZ)W zxsrLD7M465n!9Qj@=xzw23;H@tw>vhVh<1!4pwj$zrD zrckHd2b~?=&n-eScXw8b?^k{?e|x`dDJPQn;|bbixFxDn=1FR54=u_nrS`kz`53$`4Dl! ziic|$PrCa@{+tuyD{WyHuo)rTsIOgpR@bz%o4BSV0Ct?!ya`ZR#ewN#UObg+u83|2 z!96IFVjHxs5F`ROU6GiEUHS|oWdcjjL*)Y(enTv-_nA{qKVA+kpuW_C0;|Y-&~DJ% z+M6{|C`UqHa9!L`h3e%z;Ay*0?@n&zaOi`UrSb8! zs;CIOEvYdzr`}_MH|g2jcWg`#XLGS6liT0=zZ5v$Z&H~H?JFBJL$u+qx7A`64ADWt zcL0StjljZ0i7tm!9il9rv2;%^_$+C(?>>AU^@(0&)$4Xj++86^fD<}80}ckDY&f(M zQSKG+&*v*-S{mxJ4$kobwa8kZzJ-`Ip3_|8xy=*d553xdjB6)%jr-v+L{v>l+tQx6D&^uV|?_k%A*8N77u)!4`ERS{?fNeBKvLIgs zHxefNK!CJIWpE}^1F%8Q0CzC}NgsG9>ahVVV?-tI2OUjXhcLLRS(7HLpNOiw_`y!{ zhn~rTz(*VQpsXSJ0Pac(C)U?Fyuflu+t6Q*Q|eQSjkGrGgQ4V*1qVw}39e9o7uFj0 zl|PR;_@VJbC=7+PLBw0u$id`ASPpW!?nM&(h;(mPbc^n^qC_xMI3SY2$TjE4_o8Cg zI>Q19^PJS!QWXz(>@GPvSQBC`sdbP+T& z77l0Pk@f*}F9ynQTsUdh4T&n&Nh#9)KtFPhO91A!f2`$9;9Y!+Yi+jL&-bHxYzZ!2 z<s!JeZGd7m{UDMCRw%I-7eHdqp5c;`1m(q)DS&SD9mUeh16GNKuV z-MNAI@?w4>&fo^}EX3m01M*L(weTDOF+z|dy1=H!I_8ZItB54M?u>Dg%sFzuv6|-@ zHvA_O7)SAvT0$M=i?+gEt*!YL*Qt=Q;u&*(f?iYYxlel-9Y|S!z9g+*qdg#W1CAIU zE;r6K_en!PS(C-$F9CPFQ)qX_gK@3CUIEEL{{bL#>^W^^_U2ExRsYbVmlzMak|eQqru7<1?otX>1NKP6lkADVOIbnW1xU~4`(Q|Ii0G*@ zIVS3xz(t*hcar$Fa&hsu0~2;e==R^`y733#kMJRqzwOX^HV0XsStt1m9rtSBR{e|Q zjg?3D_|$zhwv}66)}Vq|g)q!uT{1axoew#(vXd0}*yrrm5h#ml%f{fl``skTVg-R&t_ut%y(TF< z5+cnSJBifzATPEL(EfbgHePG-z{#p5tO_3B#yjCE8WT{@Ob&Tb70KBw~lJ%_>AOHRd>FBypjQ*RYRG*Mekl!T1b1M8-3 zY3HFr<+-MR9?-g((U4GiEm49ser(|Qoed`Bb?B}JJH}i9^t|~BBpmbJwXyPCs2#~3 z#I)8Y^N~<5ykuE(g@b|wgMjwnQo>IX(`guC9*6`r;FB#qkR5#0!FLnuuBLm(eW^Q(OLf{zs4hy%gp6t-ba~3+ArJq*SA; zFv-`iu=ny`e)?&>!nDt4f~E2tFp|;mGv#D+9Nr(Xwc2#U7X&cb(`LTEhLXm7Y?R7>d5w^D1rRr4A=|IYuy zgvlJ?&lEjChnRsFIlO@e#ly)?!W;cN1tfTj`#ppxK1on0FVZ^-ZKft=pC~}M5oBG# z3^;n3zUsf=3G}gyE@QVL!-k0;JSnoQKkeR{5w?}$UQbth6};zC>X-Ddy8-!X;SDWL zPfA8`kv$X`Z7J6>Idn-3mm0>g{54n!OJKVQrxmUm(_woS7N*QqLK zCT*}kNCoF%cd$^>HL@nEOAv6I>Rqa!Po zgSP05(0w#hb3dgww(s%FN9k#*%EYBfhInFGhe6P03&UiwuVu>XaDS7)zGplqYH}I* zCBNQvG_5zDcS?wRQ#yg|qi*trLS)}RZ=%!HwKkX)AWK@)o=e?@F#`gz?vJMX639W) z80O2xmV5Kb4?T>D)t&{0^z+o*AZw4XFof)9N8CJsxio1e2?|aRs?7e-B2R{RP#DEQ zA|W1nGwKG6EEqhense{XVx##Z1hwNe`fxh+qS=N2cz=i;$|j17^-5_{epA>ydx|g? z_Zwy9P#y~<#eXF{%Fb-yy`$a=I|lFLe?5=yV`gb*NjQ!X>&_?S0$6e@PbZQ+V0z8! zjE9EU?Rsndl+qK)^YPN*R%5Gg#`xKP8ffc7QAp-opWtZ(fczF5Jtv09C;64_=8>A9 z0Uy>k$pXKLB>82Aa<7FvEjFY+!*Y&O{3J{&O}Y#}_(U$UuVyP#`(G`v8F*j~kM*Wi zPJdDxa)ad!@F@iT>KAP$GpHn;?z86M&sm_qtsrOkcHO_x|0>tRWaiIBDm0XVMT(p$ zwLAS%uNA#A0=h1zGSLc6i6QVo~E0+3XOr^}N0j&NR zQVtWEWQ<>5mV5yrHZ3-*lyL(FQgcYT%JrqQcvr{{$-yDRMbqKDB~5us$O%wtZuv3u zvZV`1um6DK2hHe9K}wT@C_*qaR5aAxsp}qlmRXV}F5ZFbUVutw&dhlY|QE{A}vnW=4VS zuLFCx#98-|Zht;eO1ZrogFyDk;v!gaIdC0g)Lp6_GoqiMf))5L(tYjkKFhhrN7#p{ z)=~3=$XcE#YQ@(qhEPM4@YXipHd?WzvAHy07;}mH%;8|YU4$SB+(2=AvV<%|0(&BF z*BX<=Yu z>Gn;sw!d>?WLK5}sLtl}CXvI68`i7EbUGKU-{V?vj~FO2V;Vj2wX9>GiXOqI`8qP@ zdxQRRB+tX6O6sWw>BKLa$yPCljsBRsbn(lJ5a~b*Y!7nQ!X)k*c}5?uLF@2GC^pbB z!O8=!Nu?Y}FKgm_WPp^uv`mE+&3nZT)7wEuP=X6o^tY+!r6Yk4D{x+C?zt$)p8bDV zqFMon@-;Uyh}ki7(W9r?=tWrFS{-S0{6zFxsRY}Gq6a+fZuy;~g!A4h!pxsT8)XohA9hTJuM0M+ zirF{)O$lP6!+o3fN7w#f`egoFiYZU$)$%X>Gt)|CeZS?= z#RAZN;d&g~kl}bVaB}Ye?c5@@RRW=>36vm2O!bvs+FdYEJ07L7Lp_MHC#1r}>tE3G z%dzPKWQeSm%jco_m)C!z889e<;=VD3M7Jf(mEEq`QoFaYFm%NX5OS>Y{+j@CZV>hxXGcJJ%5}#BPPXs6Q>yBCi_GKxQm-=ilmd*E3>QH zGkUxI7srK%UJ0_>nT=l^pC;oDwz<65DPBIMKk{w`JbahtqB|$ey-&bPn-Vrhj^s=w zc4CH(Z~kEnuQ%D9dIbI;h+dI!|2Gr z=m;7sQs_i#X%q^dg@eQp!ca?`7$lfhVw>9mdqcE#pKZylm?I^FP=bSC z%?!E$nWSfnCo0$x{=8qlnlMyVps&Hx!DqQW$m$T#`TU(NrtW4EviTUck6#VNSy~ZC zFU#Q)v)DPehZrgue(HMt?* zRvT=U4N=agyF&|r+WAl!naFIQY~9!8^M3(ZB?clH&l(H2=0z^=P#Zpo%XGG?pra|v zrn||dq(pjhmlt#Tx%e?OauRE1f~_lmC+<9z#6bhE`Bv=eU(HF4VnLP@X}-NmT)Ox+ z_hzwtV*jD3Yw+{k38JPLGvCu;yUL-+p4@FxH9v}h;RR(Igg>Nn>7CH)*c*HucLeKs zCW7DTm~Ao*y>CNNV_Es8SFgpFRi77oFn>uMa6qr znY__L!O+aIoM2>}-YD&ZV=sOTgMol3OcY)Z4D`4gt|NVszQK8=3Yd=|mH&p0 zvp=8Ho+OOXZ)Xe7K{GNu8y*bJj_bS4{^*qdbKKnAycZL#^SYn0g{3Hij~ApdH>9>* z6O_cUVlg}ROf$1VdN`tnPM#n7^4IxY6mfpVerw8$J4-z?T=#wGESTUd&w>PF0;7!KWbDHBOmMS@F)IXvyGsRFO*adfg6JCMCRQ$g#A~yW@$J`ZEJaB3lwx$nY z68Y5(r^h5Sl0GNg4p19=T1v8Q>0Y7u76J~{r}6ha-!YHRU{}^i8J;Hmi6f~UaVbH)Y>nf82@#`_YouLV@1};|9rZz^b=Vm`W^pK-eQtSTVSh@{rNbtZYXr+uX zw8QrPm>MOBum47PuH+_sA?@kj0j>dQeLP6DuMT$Ny+1qN6x<4WrMmzy-G-oS1&%XH zdp_>Qw|+N;gm}QbLO+0B0x`;kX5Ws5rEPws)jeiD?dp7&_)~&!q>sh?s8ALzV#60_ ztsaM|i5mimobxEsX zzA-8FHxx%>2fr9V$(>Jg4!iDek}6&zPlM0Y6u>v;CXsEuu&P)2$k zHW3Km?cI`y4aGjb1#Anh^okf+VE4cc&5@zHnAd7Ef6b?=I!|oD_e6zgchPK0dt#P$ z%xnX#x7n8_o%<2`k1=1=HLrIO^+5$ZkB{iQGUU)jbHmRBF>X0uXeyge1T_HoOZsNS z^9KtUK;zXv;;3u_`lg1IeDmwDU~{5sl=8JU0eOxq?PkZpEe~#$dFt#dtfvu^-Na6n ztd>2jVsz%_N00}L5@xsl{3ObNOL0G+F4g}qWlZfji>~fM@JdCl^c6C&8?x1wI(1Bc za+!*vduI4a5B05}fQ?=D!SnNnZF_r%YEl-a{i5ZH@4R~UFM3x$GMKbUNd#+AV{hyA zeS9$q3oixKvbKdcS9(KIo|cnk*+#)|f`K6h3jd|)VBGL;=>w8R+GGR`gO;u=M6aX? z4$u4^+dC-Mvntrxg zi2s{koA(P=5mA>B2ETJJGqW7n{CD{rQX(OSplxTye7SdV^PF|hdqQY0bRknTYLRlT z{898_mt;826eU)Pl@^--15KA7?;%y2?Fq?{_N|O#B;{PpY5Y<4dDr>-*x|L<9_^!& zbIoI_I%GD{iXaOvbw@UX`mbBDN{3>siEsh%_McEH41MyEUztYg)I< z@702&QL@1(^^Z+#pmOQqyDy8GMPf3GulTupbdQ84bBEw?dR@#8X^+GyJUAlBhV813 zTFIoGlCP%B1IX3i_IpI%g{XGk-hiYe$622a>?n1Fy~+-|&%r(q;`>j*zu0KR98rJq zF=oNeqFUqM5;o{2+mMoI@+R%9X{3et> z%vaj*h`85Mbx`Jr&jHc}nvj+sx_Wc=ufHvTmsVF!x@9KlsY6bS(etNs_?{`t3wm`@ z;w#xd7|M`>3J?PNp?@K3)PCxdETZH)vf?P< z0J8>tFoTIup#Sa>$*WxyFEfjhL*d6jd7IT#P`-XDlTLj>xjGvm;0+|;472N@R)aZ+ z46-_s7omp=#N7wQP{(%O`EnX2q;u_AvRIvyqP_}`sz(I9Fgr#lDHe!?kC8DO*2A^v>wc&}CDxIJC@Z;_~8!DcfthAS5$Bi1@&TIElnxEWXM|HKUs` zAi`-N+2R<4(~!?O`M;K=u4zKo%QtC09UwBW2c9Rj@>+l1 z8+(4i-yho+`D<&kV-7anZ&z<;+N!>tbZ_APG?PLP<`K$phm-dYww_z9YQWgN&Kwe_ zNx6e0*2yG3@}^k7K~{1-50DX{h@|(Ax{M~s{}hFd+s4C1KU3nCtbbZQmYyqnd%$;y z5CH9ZFk(VbcR7q;s_=qXzmJl8st>iHkhR%nxrrR1Hz4iMalOZ-=^Bl&Mk5Af#h!T< zw2&U^%+`ddJ^1=g)3JjM-vxacgE*V>F6G~v_#@tPrtgWi_6Umq5fM;i6xz0{H~mSz zl$));7X4p?P~2KbBKw&G1dJVRzno~*B=?|@!;A$Vj#fP*Fn=Ib?KB>5BtEEncfpF@ zYqwI+tuhgN?+rOGxEq!HVSn|K>`NmamUY+6*8|XBJX_bNGSPCSs8OKiv^QIXaBJu{ zCItc-Lap*iAulV-8R15zf-NOwOq)~({01>$3$IE;9>JuSnru;VS?_JEhsn$Qg;MVV zTn|^o-me%fiic5B6C;k1;!VUGU?F(ix^VU4%CP;$e|Ny98-ZLx7ge*nyfPqZzblQJ zn@48bCkT0X9Da-Bzm=*>+4Xl*Y37YFW>*+TI?IscJI7GA?-)oMTA(sCKTe=rwmY65 z{-LSB?VWhR$SD`9zB*lHXkSEc?Gj6xG|A0|clIpk;_5A?F0Ns>2li_#;Av_CPED(hxK<+9vEV&zalzR!7!H`U1&e&?(BN>)+s3IE ze`J)PhoA^y)a@_n;_>z$W?&}RS+x(TIy_$i|Ib+u8#^ESM7+A*Z^iN(t$~(k4Agcq zrpl9yy9m2!DAEF3;%dIhsjBJ`fK#XlzTLp!pb)J?@%?zNCs;TEW26fffrdEAfiS}f zPkA=DcikEH99=+ncegW%t1Hiv{BdR_ey@o&+O(t$GfS=QeVUw;YaauzWrG8+ivg>= zlCgUG20gC%>i4AwDo1#~B~Yvj!}m6Fg^yyYs{bfue;OKefL>C<+8^ENY8Hb;!`g;)8O!c z`T33=ahi$OB+LC@gu^AWxDYIG3)P08a=`UGy(?RDeY6YCr`l1r&ZLtjnq^{iF2R7Q zKvO%Hg!RR%edEPm=R#crlN&gZCL72RAk-(Ncl$Y6S3dw5FL^Oh>i6LU2># z+`Gfq!b|_^eq5>&N5ao7Roli}FRC4l>7ZC^;?u)eP3msJQ`Sq;XzY$tc2xkav`EqF z@_Ax7a8=NnOIAv*%Gz8~;AZg<_9Y;p3}~nN{1~T0;5-ACN(mo@_Rxc9y}+fVINu2` z8KZL2BZcd2)$R^2d9v$AOL?TKM-GK%-=?D#n-q*lkJv%HBo8Ad!Q2SWHPzBk-WNpR z1RNY1;I>ug`Y)&A2bD$JqQb*rU!KWOPkQr|652;MmbR&sZ+EbJ{5}KUX|@&*-}Z6! znL@B1g!8ih9$AhaN>f?Tk#fRh-xw!p9;+x(gk?d^PpFzu!Ctp8Y|xiU4BpY@@T!*} zmI6VHAKMM8+8B9P<)KK8hNmevJM08ik(Mh3?gqLDBT0Ry2G*ezLf8T}nE9{sdy_W+ z?NEXeldI1G=1Gys{+jb?Nzi&rMMuY6-N{NCzu)Zb4b_*97cQI0pXy%~da}MsLW80u zeYvz<-bJVv9YpIrr?DCde|ySj{kT;=$7#rA>u2+Da_>`WFkC@FSMW6{42!6#E&Bew zXv8?rYZ-2I(zLe^xe~@10*j$@>BYSN={hw+z>t3OD0)P=3%;tVgCeBR@9`6`oG$Uz zlk*{6O@I~O$UyyN9ZJ$z$M{kz{A*DkQNrMzeo$s^N}kZUeguFi9$N&XDDZ8_9(aLJ zYHTQ{JbG{_|J%xmSgcdu9Ygs-+bsnQo=lV5LU zIix{$o!d)(?Dw^>DOb@(5|H{Vs;9xO%DvT{%~W>69S64ic@U4q*u4xbG;15(4ynuB z3LSSY6a0k(&rCx=1u`7>QC+|eq=ckflebZ^XxI&M%S>@Bs4k4}%z;(_j+vGGLdlpX z(VuYM7#t#kYtBWU|8zE3tKF*n*|%?*R9`!`MN02Dj3&o%@<5n@j>R(LaHX1UEd2n_ zuBrS8e=VlqRJ(FFOG@g6FMELU;~*h$vP=>qYUAk~-9+#gb@jvrKm$YzKZ@5?-_dP4 zU%rUoR&Y`yf*0M~|E}ZshVge_doBSOP+vj1E5VRBIxSp$2>Rp?OKxM3Y9y!{{& z3gLTXASJ*t3wnB)5dJ?y=lxIh7sv6>-L`9QuFQ-O*>P=&P=pfKRtjZj+-obcvMS>m z$tv?}WL;YkLbAzD_FnhiuOIGza2}s~-sk;#J)aRNFfc=ChLoFUx#ahQ%D+97_+elq z!t*Ws+I3CU8y_p0FL8u)8Ad!@3z?L5{pW6f{R zIXSjui?OO6KR|^<<=;%I?W|*0D)!mLBZmOmib=u*%Aa*Z%kG#^FlL(yP~A3K0up zE+}+-T+8nKsXelyy6g1uX(V6gC1cWxBa-id$P zthzv~F-~G~!R(Cmc}%&~_Kf(H-s{Bw8gW(+H$zlFm$bb&veF|7X!hl-!9kA z#J48pb_&{OI()mc-&b^8`ItPsrv4-_>8FG9RG{I>MEfcw5O&&;6yAd7Ys(w<$VP%S zsKTB$i9mPGB!_YLZX!fs<8=Nonw{z#7^q&>k1rEe6Z?x%9EH<>0HNv|q)b}$Cqx`T zH`%5_1JXNsbGIcB1C&kGd6e{AfKiym;@^J`YCER=yqP>qX$lfVI28j=q#uwzLJzKA zh|;1M_L$iKJrkB~-(&2oqdk2o0+YCFua&V2uCkM(l|{#BqI$Eh+f9Q&(R~W7HQ&A4 zJn=PVC;*IPT=KagP4Un3sv>Zprjd<=>CLh6;1VIasJsnBL|GJVhX2iPpy1HY;aodh z2fQloVVo`s{xRS`Gc?SNYulNU@(U+&Eb=GnnTYis2hXl*pe>!|{PWrK35beXH}7<- zrvM%K;JTHo;m2Ern1jUtM;s3Rj z)2HBeItADh=u7u`aQN=sH*ER|7KQspulT_bmH0^A6fifN*1c8?*VhI;Q~i>?U22 z-w!T7IS<=9kD0H$JcQ6qnuF-G=;gaJrSQ2@26{}+@3Z9#`5uh1P8g#t{tTmI!ABO= zY5yuOgVh99VjAoNKQ)$jceWaj0Z2YF9UY0byp5pr#$}4;SaSa~MeGG7Wbn!bkodVzNGj3Hk=B3~rMV+eK)yu| zF!}Ic4Jk3HLC_^PeQ{r#ZC(PVY?M#O=v%zlI{#4?WK_!v375z-o87|LR@oC@f9!(k z=tPdo0~U8^hc8SS>$-^^s#2pMiNK+sN5M_cuJ_==`Jo9>W*7Ndwvo1-i z*_qCy?IjEWVi@AYi#-jotBhQ$8f&!cyb7(ioU%&Z_p@}OGr^!3zoYvq*3poFSGEzY z2z8OLkUO*$nf$oGx;lKD7@kRk_4Awq63kRZ{xtr#nH8iV9rBKMt)b_i)vugD&fT`L z!OiS9Y6XDhHC8_J)|5eqllRSI9ua&Rq;lRtV_mT;l>8HXJ1dc!I{bZ0Tp{?bU`&fte zT3>^ZA|ck=NmseX|9(27ALkhlDekoC zum1A&JzmqM;zEOFLNdGP5j5f0<3?!3NVFPiFI;|_fRy4Kf57#)X$C(x340=Naks`#oAoNXO;>Qzmgntyy7H81Rk-gUylm-2}8-{S(KiPjrrI}TyjKbg>LiFQ#<>oTX* zd`!+FKU6Lp)hoF3GkwKusv=G; zFR>zW<1V~GM(^uQH5`T4;qL9h@PdoWsaqw`Lwc^P@;{m(zPDYXD436-F^W-ICjPB; z?Zx>1R2;e>otOV0%&&n=hPIXW?uMsz3LD!Y%}~F{2T)#1O0E==F@TiR;P)WxiMae* zUhVTqGr!qRq6IsG997XDPdh-Kb)Hsx9ti2TNyaIxID^Wum8 zp%DG>0k3@VlTvKMKjc_wq1@hFI1}3HDOe0>1I3Wkm*hm}P2ce4K|JhoYv>K)Z;(n= z-v7A__B5#;wq$SSD0!9y{D{4CP?e8en>RK5udf>mQbq^)`b1)rwya$1?)Ib^MFVvB zK6kA1<)<|FAH%N}UoObIy`N4=gWVd7LEJ$3qz;hRTk^b7j~?*2I}%N8XQ)Pp-$D&e z<)dCHxDJ-GY;Xz2c{m;3XLaDts#7rq>^8T1^yKCmsm977X2@?`ulK@B!bD?ZUXE|V zH&Z^hWg}^G=#*fD_FBkPLUo0mq$(GkM7qR(R2kyhkpun1mm>H(H=MbCU?!u-nmEu@ zqM58u9C#WRUqaiQTa@ZHhn6QqL~=4N*esSGI}9w_zFg6D5*PfOMoi~KZaUImTXXC* zeHP;4@J8;{dr5!X>f?3h8MF}3o1P@J<*mnw%z*FHj(6pTgL#=oKdZeazDLcT5eq_k zX@43tUW|)e#+YC7#ewB3((hr?+HTOQui+kFN#s!f1|pJ^IcS^gg!cs2_9R@&daSaF zA867Nnsc-yJ{w6r$(!F0^wBDVMj=duhIHU)K7awZtCOMbyUpofN5;O(ETBO0&Zj=< z!{Knv6+wcRe-J?$JL<$jZ%x?cVa<|FZQyRb#nVuLreZ8+GWm(Y{$v%wz_}1R)3&9p z#r=prDu{_1l|=6xXW3*`Dlh5~f9~>(4M*xJid2UKYA%?t$`a%?Afq`XeDE#MBYRUNzEy~}U&QmuL{a?qU zhJhvoC4qh|%l-}>SBn^iF@*VyT?Wi9$cO=X(ZrCLqE)*6N?&BHd(-zQ=B$`d&FOJg z&}AfTwcg}ArSJ;0*PJt{07cM3{-ZuGbgz42SyBF#^QrV~rL;HlC8jaBZ3ru|PJN>1 zmm`a9MxW-q%2{}Qgd`xVo>mN5R9n550YT={(2>_TK-UHWJACx19mCA{>^l^+r;9NJ^f0_t_E) z>!#{-SZ3&kcJ=ughHo1}a?)8Gpuu`zb32oo!X^kh-s3V<>gIQ zD*|H3vX9B3HxWyHOXwvye7;WnyKLsiPUGani1;!KiZje+UF3ldUE%E|nfCaU9LDZOce4{-Y(%&3$+H zML@Mo0rv|YKt9V+t2zEuMDux29=)nAb>fw_roG?@hwIF^RaCBmx37ny*)CNEyQ{9iVg(?8tz9Qok>^pcme3k!%48J-f5d7wGuoz6Zx=uv#J0zLtpE_|# zhj{t=;erwp?RlQzg%JyX_Sv?wE@GBim5@iJ|6C&^bpE0A7L^XYkeb~Vs?67ddNmYQ9w%k@QnPvy)PsxaVd-r)F24b7;I1s4*nI3YT_Qj|6pZ@dUt zck|V1Ka3lXr5*xlGgo+*yQKMKaMW;q!&^39zk(;>QUe{B^^HsEW$-jm0_egeE0CQh zOoq`CfjlM046^=Fx2QR{G5?z|5mF4sf0v4te6%;XNH}o>wI6@0N3*N)We0Xm0}?~i zs4855@2LrZJX4Q^cs?I^Cqq!#uK>L)R6DMlQ&cKu&~Ob58-ZFm?I#{`ppNQJjDOR_ zLh&4h*uR`d@KgJj%gCG9UgBetkP z19ru(=i|75eEUz2M3kryTM8R@YX&d2zs}xM)G>aDm??JW|tBQ zN55@)BNH(k+X&DZQT4Lmc&7H1DGw(GWrEI~_5)j+vCz@RxJN}*UM_yvy`brb>ph>i z{t89wYc88ycVcE1;9EiNtX&KDj`uZo_1&hu~ykZv~5d_WI-g-y!6u&c1~O z1=Sr&J?VpY^2b%xI#6#Xt7c_Cha3GKiCIzVufz+hvq@-1w~*;tuZW>=k2tWeMTSj08B5YDkg=^#perg+gOJlA!ZI z41@ye30mX0pBX`kq{B4T)l>P))c%cwq0h=?rN>?Ftetco{0^ZyGv+eEWB&ls=O3ve zKK{go(o^)U%O~CFhsP}TzGc=H6Y?fmU-8TcwmimGk>p4sreGLsK@7pisH;Db!=J;B zr8(($K5q%x7pB8qb|Z0UvSrZ18D_~OFc)I~@6u+h@d8j)wOIlek&`UjxK~%-GK?zJoYOI^u{38)EM4A+$#7;*fA8*rH93$ZLRA2B2!U)vLDirK*Bu6Borc zvhPl9M?W!H1IDIK-uc}8N2f0v#Aih~V*3V?*BpE1oJ=;40lSToG!KloO!$5U44x_e zbEqVHRNlg|@>6!kGdLso=)N%6YTGAXvHej9E4d(0y8i}^&eNJvfRIqBh{vMeGBlQ+ z-9=;x4gHE-EwcJ$*rWj+qW+aIkv%sz_b^IWo4D)s`@_15o4B~!UrNlPHvO+Jt8@)$ zrs((ZS3obl@{pElEL7hmTy3sGY2%Srx>qSDW{E5z9$~XLC}9qBAhi|;u?H7{bg6OM z(k=AA%AXE9Mo>}3-U1~Wqm>>Qd1kXSRY1(Mb(Y`=;9rh0zbtODn;5?1Hnh>YYg9a~ z5Yl?gk@qS3gX>CI>$H_-VTG!N)r3sjMc1d_3E0NkT&k7ZR?n5CeRe2qrut1 zt-+*1{Vd?C=rE`lN=PVFxnf}=I5kNU`)g#M6ccJIQ4FPjpPiv|O_C9~jnlrsCq350 zo#BV8GwH5z;yu~ZBdHb@Jiv)3)1v??sx9ToeZ|C zt9FstNJ-2sCo%lCotxi}*MI`D!wLJ#_6vz~E~a_Flg5>)*q>$f57irEW* zH?UsBNm>M^C5RU~>3rx%-?%$+Bz8o*VeF^AM|y9czTSe$^d+|q&yl_*wA81XKI&h_ zc(~lwTgiVTnLVFbOv1Mu$bJ3@Ww$F)65AF$ESeP>NY#rX-3vJeb zW6rMdbnBjcXT4H?HW1bB#C+&`$K!Epz~Rc!VKgdSD76bVAJUvvH&=fWwCvTwp%R() zTX(&DzIZ)hhDcnLO)+xg>OGLxL760t6SDC)ye0jR zZ>~9_zANVC_BDIlm~7*bn%LD2(&fH6gGZ_`n+hu^9%inAh!5~-gAzz1*2oBg3|0*d zRJ=6Bsj!om7>3Y)e|idf13WJ1qx9}=p?tug?Ww3}^~nAPv9VgVU#O&$KzsSpti+85 zz>y~c%b@()uh+q$X8XmOHw8n8fA(dswT0hKJG5$BrHz-GQlY+gys{?|4qbeV3kfBu z!;a6+)!DMuQjb?j#@`1w>%e6G6RkZyUCHiG<~?ty-OpFfYz|*K6XB%ivAzHoNMO;W za@*^OLl??;5lsw-*wo_?C(~1YsToaA=LG)5!ZE~I88!`j=I&k-owEX@EXNVNH=i%jEj5j72d|d!srm- z#EF5EDG%he2Q3|gi{BEp!OhO{ec5XY~WnT zYmt<6DmeR3Wt(qZZ07m);Rj=51%B+1)qPD~Ym@uu-J6JoFqtcLX;Gqw*%cvOn8I86 zKcj^90xX`K$8zMhfow*>^BF4uC!?Sw>TIieSn!o9BGn|BJFJ@FbM#IOb?@O;nf2Kh zJ)u(=9#;UgeU&MCjsBa_G4w~m_b4hafRk1JGj&V@8UrwrXR~SckA0u*O^!2wao<$* zx@+6u!U)CbVS|qhX2uBQht>_ZlKj^#z8$&uFO?Z_OktNR4Tk@mR4XQ^DXX>{{bQC4 z>!p|p5pJ^M=`aTTpSHTz1WrX^J|&z_$lyJK2Y6?0#Upqq6_4FeFHlQf&)ICr!>*9* zu|JQi+fk66W}N{MVXPFn^2Anm^4zJkJ)ZE5Vnm5(0}N?)OAYT3FKQ05##o} zX~5>?u*{cr&+Qu}HVQaQChJNEl-b?ZIfNsy6y$GH(f6xAyDQvOSl_(69$3kN@z@`i z|L7n6oZ^TZ2R2Y3^&S~XWlvtxI}#y@*HBdTzcR6FIlaIr_fuPxO>~8LCe{S8BVzqW zBRO6z3HM%2Ln{mFnIGQf;4j^!br+1$zCi5WJji}_HzuGK{mhQS6~i^w`U4+ks;!?o zYRaW8fYOd604XF3_>dvaB>gU19*|`U!*qDna9mk=^l=kxgFKgL$<1-&k$Gqwym`E)OmF->K#PdF0Mw+O?s7<(pmmy#k@|{_OQOHzD$Th zK=?qH6y&5vdFoEO>e+XRUoW97dKkeSwI3464yp2C`WqBr7aqdqxbv#*n*C<6;$aAi3$Oe}Fed8oG;vsAbq4kF|}{@!NrM51SG z6M@kgRA2c)b@{LDc>lyD7K>mu9F^P^O3U{LZc zH8)Iw(l_}EUS`?U<2R^)8Fddf1e$(xfo3kkPRA7aTM>Ln zzt=H2={kMKcAI23!`ltHnk6q^34>B8gm9GBPcd~CY!Xu6nqU3YRVHNJ(cs42nNFaf zdCePi{)fW!qY*%_r#J#~lq6Z**eO;CSpmiC(x7Mgu8>0{22PryAww5gdvBIqp4f70 z992Dfgg^}+i}#)H5TBiN9ZAyeaO{ycS--Obwa|08!Z`-aa2yMK?SzSv{{q+gjSr_A zN;xlYngtBZgiFc=e8YCxgCubNfqgGJtVaX)-`Cn_EI!Mn^Rt%)ro3>KjGy=F8tu6P zpPoyn;x-XS&C~lFlW%5^a}z=w7mIH)gNl$g!9+1H^3QFzZ57uMJ{q1y;K*Z_v<7>* z&e_fYP@Odm3T7Xn*T~<``I(*H(o29Vv@Bs-rXX%jmyGZRb&KZ`D?m`fa4a6qaPCov z?oH8nLU$t|Ki?$8b>kCJYL`gq{}aq8(V}romSgw7fwA{l^zV7YpEqn_h{H!Nr%%W31 zwFL2fprAlAy|RzePDa#wdvNS+-mg-HQQNQ-iMB^Qp#d_1{~iT2*uXq3`v#`b zo^Qvt(q_Zj9U@_}(c<#uH*aYX`P=h5RDP0U?UX>z{|2T&WoFn;Yo25cO~;8y+Qrs4YmgqeDxhd2Fu5rznpA z)66_Qho526_*iObw3f%@o@M0d9}3VKzyI;D!4)9AW8DWdp^sCTqq&BhwjEv1cD40~;3z3Zgg({L$gl*`G-9q$8^;9#(I0!l z5rr`zO%_km{#)L%gISx&Dz?tk1 z=xF=5|E{WG6T@krFSGo57Ee1()nrVKeBRs(_^M6&rZi{Qg!zY2p>{MM1*lx)$P)o3 z&HJzJm%aYe>2`ZR(?xo?s=DQ`H&acvWH90byz*%(ZU?bA!VDHUD?R^u)}{$)(kn#C zE#ON@zTsc8yTTh@9)!LEUlkj4pdvNAuScsH`DW=0bOO`&8s_XmNulT0`$qY>(QxiF zUXBh<;`hv$V7m*@Lm?d1$)sDg$G{IGOjb`T5vOL$?<=HjS`@Z-cj#-Y!f}{zT@7P* zFbgUbd!%#vIoi)-(!1(|=gTnpd8fYgUoD~J`Es^$2pV_0z!u}pyw9ipQ0iDieo0$B z=mDl}@}nh5UrtOJDmEBuqCKVFdz@>rC%ugu^*2E1Q#BkD+5^%P+Km}F;XWs z@htYi=KSCCS<@GPiGKZiRoyb?r70C~R|S_h{|96SilLh?g6UEMC&sqY0SgbMaub3W z>80D!biwz+DzC+ocUNiuYgW-a_B+m5{q?vI&jkUtQkHm^W3&y%sZxXoHQ-40m_+Tq zRaOJqTJU4CjezDz)<0q_ZxQ^G_@P5!N2Ed328GTMD|J)9?>T@hj*!2|k(d0q*?&+# z!M62fUyH51SDbCCl+hG%+MyS4;!rbwo9cGJ*_3|$V34huL-|5~0e1~W&~703LIPq{ zcJK@Y*g5FaSn&E1bO^iH;2sFl=$RmX1JRH<5(6Gt0OnZH7+*zzbGKZ1Q}5?B$H4cM z!SWBv-KR-O*};e+P*9ZWyuc0!&*wp=Iqk{jk!tA`#IA-Mnp?CEUm9=8e2ykki8Wdg zz=j+E-*8hAyWwit<+&F2>&8bq8aXjMy`|bb64^Qmu&YOaJb`bND4N@JFXGro`SL(& zAD>lsbX%l)JpAx`O%A<;L&`n|j|H6_C`)R@g#V=IC-9A{0dq{hqMZkQEi8;pn4LUX zc{-OgjB0`09rGH)Aqy$YmaU@z-jVZ@fur2K**Du1(#YPucXM7Hy7<68aCiQ@Po6pf zsPhzkcvdKj(JC-^)1^8^NvwR6NEQRG<4&x9rLWOIrwM`658O6Qa3#GdBx$>c`@ zn0@AY(z9fiFdL4+<5in5vA~RVcejC|1$!$i)1|91RyBC@V&aH^5zM5Hje;O9{iHZ@ z_*oP4LOBuO&KH;$*|sC`5oQqr6p2E}G9ccNDp}aya8cxX43gk4vY|d`sY^84EOm4a zN=x)=U8gdKy+U+G3;D{_6&1**c*AL>JAm#sV9rnjcm`mCy&+SuhzSR>p0G$8=%>D; zCY9K${(#j_k-y;nTt6rFW9y_qFP@MZH7Mdlhgv&9grc}H=vB%D>7Z$b+tE1!=9O1P z?<#y&s{-hYPwuhMMw0W^HylcvN)=~cAI>}*gO!-&8Wl5h*$h8p{j?4`U=7H zLg8aTR|J13dJ2fVZjh8FV?!!JpE}^Br%eKGVXk>S=l|fWUB}VIaf9y7^AV?Ap%Asz zJGv3%0F)gEkmrnM$e3aR)NKopK$ zA}`(g6!h&};J_uJ+c4|kuwVTC(af+U*=_C&ka9G^w}lqi{scaOo7#Qe5oE4NTn+uB zzr?r>c7zbwAVWeI@y@ybw$<)P0bdE#ue*734-$wf3{bz7&cVMoB!;n(LBC&vA5_l) zHxV23`#Im@6H?S=)C#dlXDal+yB*Hx)#DD+)v8|ZmC1O01NTDpfo_YvZW-B1m!rSq zTa=`Urave1pfDrYarE7VeZ)Q%t2=yIGo6qhFCXfq#aXf24_0k2M0Uwv!YLZu!NBa{BQ zsZpx@V5@$`A1cbByd&`#raMUpq*2zFoz~S-iJ?GeJw-U#Xf~Y=ZG3Juj8yG@PRYMe zpc@Ru?j$uCzZnpi?M|{$4mcyYmOyr6jTYY9!lIg{#{NE`Ca#Ak&^d|hh-h9{pl;hz`)5$vQPo|2$&=}q+K&G z&KOXWpk^107Z|BuKDyGcS7=-kl8gjSjWIyjWq`lI%MSd@h}XD$=z1j(244$b+2I<$ zAO?&}hO3OvHt4eA*+gxt?U`;w>l!5bQYEi_5V9PJhSG%g(uKl?V^}fk3bv_wKmkDa z)AG)>Vg0Sa&|XxGR%G8gl8tXLH&1$1M<45z`J%<#n*K57C3b>&MJiL1Z9enVBqx># zVS5IrB1oq4D0$m?IqWi?Evm)+WF-HC@2xF$xZWb&RYTpDo8oB4Xh~gr-PMv2_n|BQ zz?Qe_&0ND>{u^1{w|6`ut|fJe4AcOhj)h znJdIrdN$(7OwH1Ni6c2ZviZB`#Drx;EgOaaoN-4JhWUC zmMn@(e|4rJ6YLwZr47*Bo@ie0b$8}I|E*3pL7!A`3;(KqzmirgYDmIWCTY;`FP^$8 z=@n4RSA|kWHBc4P6S#I;LOBt9#TN4!PC=H9#2uzRoG3tmdg|0Ah#x{7&9Kvi(lc;8 z15HH=v5}hm_`DnmE*PJNqTZ6c^aj7(rTKn@Hz9bHRSU)BIY^&pdBIxS3MP8lfyep}-|4=J}O(07yxd=PQtO z?75KrSJB^ECrO+9^=s@t`fRFuq{z()NwxDNHdGpHF_=XI#%~HtBkU~|;dW@zP}$HU z`Mr^fO`Z7z>7$a@&JV^rP9gx-n^CihUw}Ia+m)Sl{LYACH7J%nV24E>`kk5i@SeqZ zcAkAb$bd#}KfH+1fJPMT)2ndnD)+slaN>54-24rX|BlpcX{A4aRw{0}XEi%6I0Q{D z=)}H`wfI%$rrlE$H1qYxKT+Y_*vB{zCafp9J)!~ACfY_yS~ypoU0zt0lSMZg+7Mo* z4A!%nU=3(SbM)dKK>6D#0t&N>9|%nM^52>)R=fD`H@!8Ab>;UFvi8z5>7vVBy*gfi zI$ccnzB{r8vGxx!FhaaQxLL@#tB@gTo1qhI@nxFi9rni^wuM%-c56!AeEQp+@9E@* z`Lu!|nF_(V2rIR)vC&Gj9`y&RV_34|T&&qqdP|xZ7wZ^~o^J9j*6B@KNtAH9 zZSbP{<(+cGbTF%fi6;A>ox!Ezt?~!)$A<(|`U^_+cRdp5!mrnqsy-#l`(6v znmogDg#>-yk0Xj753Z}%lJyjx9B-Nt6TH>TLR5F7iUT>7$lF2urL^p{JHz6tP~!1B zTjM2-BaLa5wh}2yfJN#yU^V`#1h;x= zSQWo0|4Z!U{u<{95IsZ0@&n$C4SMV7gu31pRf#T;_lx3clB9HmJ6W-bUcmDBp-==)7YKsl`8 zLX{J}5sc!Mz5iL=8+aZElM@?2`Px*i>h=HZ^F79cBbHWXtkA+o9HJS;5l_m#NeN05 zTy~v9KhG1-RJ`fs!G{U@7H`&<dI{iM+`R-@5H!R0>9UNXn!4rbe8QGS)gr0sv9YSm%h4hc-RAm>3tlyq z@6l74Lllq??=^qqDv8&%pydgv@e|D2^0H>j>wVE0!zkT5XFKP~ziyM9V4N|ZV?jHb z(2HAa)?Fs0;51>)RZQl`7mspRq_|6%2%R}!1wUdR(Ok`rzcy8Ht`@g>t52^`y*}W0 zlkM_-bd`Bu4yDg5%}w~=VUtVYZ}3k6Ab^mhYEK|MI6NE&%95n*{m2N)6#lB$`=;IY zxtErUmiknyYJ2HF`-p|xLBXpVyN&1MXxiPs^aTpzgt-?mdT zgp0yxp7KW_^d z|CZNUEdJ+R!H9I`i`A?1ePW&V_DThtB)(aY?ZYqeh$nbP4TGO?)ICYwdO~!FcPGJX ze|a1Q1buvirgF_4=sN(9|=X;FQln(?;Uw?Dryqq#9L7NS5$jWV0Jpc z(e~6Z9DATB)>&Lm-754X`jm{F%ArH=x1Pug;atj}H62k6^VN`bYp=bG)){;$bR~!f z*QfV~9C6l1Ta_jyJU@~Mcu!DkpLxsx(eq!7Ekh4GuARL5+4^5wr%+YK$;lT#zBwJ5 ziu#)J*K;5L-k=H-AT__?#Vm4G(l&D)ZD)AcpE*sV_k}>NtKHC+oJC!zHMZt`R~-03TM`Gz58^MuO9Q03K3NfQ1y4y z^&aZUvb&hDI$wK^yR8_iL+x(ArOjYK9hi=c3B)s+_{v&`GJ+CA|Ly$&QSh&n{dajm z5t&ejqBLt-{q7WE zaPaNmxjnexZ3qeTJFK!sLRzD&WeED~g)Ik> zk^)1vX9LmU-F!>ZliJ9woAQjMaX|-3)^3ii?_qv05C&N z^scz8aaOj(7TO#eNBFF~d!bn6&v><63%xb>+ALJzd`%TajONjfL;exh*PucA9AA=Q z1$Q`JQeN0?SNS$Ky8S+!j9!1p`QD|>91%V6lkP|p&%cS~I$wAYNszbf!#Y+)WCxrU zkbOjn&m@rc;u2*=8Mj>B>uf{8{9eZcVT=?Uz%}W4b<=QH4O4~`^d{eV*U2)VEC@W6 zDrm*AaR_bnV>lB%73tXjTSpZ|d;3#_zBMunliEEv=G<1~o5^p?O9-{w`d|#P;Ljwooxs+(#xlqDcQ#g6}+q=Ds*@$!W23GPv ziVPwU_(vV@7Ww~C5O6|ZB_8=QWhP45H|hP!&%sTXNZcZCO^O-jdw9I@t@S+8P@J4- zGMh%tV&k9A2~&yWqgmT5wM%wmAd9M>;?t*VD97kA4_#<%Xno?G@YpM=LGompUl%c1;NKvGa|88WK@6PW9 z&Iz!MlWg5{=`)O=H3-7JGFve~0uvO<4BBT+JvO=iqGk)>1C*f#?H-rgGIr<3VL4=M zNg3u$pGG`>pgexvV^Gkcp1-O#{Si$y>%ThvFx?hXKjMsSzchL&^pmYT62bG(DkR2n zr~loQBCgGA6%T!MBTR$80JM~#UZC_i=1{@=Xwf8FrEj1dq8N{;KQMc5%qu(ya;XUV zcA=Ua!c$j5_^C|3;Q?p}!E)y?y#0eDT>_XN`{?$k|q$%)CKssm%!)qMib-o&j4_ zpO6#i7|Mt;+-~F_bx7w&`U_r)L?ZbZ3i#uOYX@dNeBuLN+ye+Ztt%fQx~42|ptU~8 zOJdxjr?miSj$FRvz(d(_%qpx~jVS@0#3#P|U&C7wCP+#<3`Zj!(!ZoU8>@^oijY-M zCUg`A?*=CV|4#TJG_`Yne4w+$oSqn|X`o~R_3?W9{w9c|A(rBWQ6{k^R>-V_fQsr?v_3olvGvQjp@6U!=Wz|QnL z`D4z)HDSKsIdPXJUNcIlGysK+fb{8BN zc*aDp$$P?Yjk6@U7Qaf&CSMePx@@|bk;cUZ>cg47vV`%oq6L zv&d-cqjL8QzdesKZF1mp9X6k&U{!lf4i$Gp)@j4mAt8qEmAXXE>KEosmkumMqO&)W zvihII=B#0RBKSU10|*Gc3g2xN3o9T!C8}C0P+D3)mHaP0q%MJm*6l^M7>lDzfpjX zKkbdS$5e*Oe+K9Tv&4~n4kO)BGSrk zAoDJQT#23UBLl_@3fD;qK6%SLxUBocq{gh==PSD8<>wO0*k|htcP~_LIi62V zH{D0~iu|`LzepJ40llx(AMa-Tr@{v#fY7Ic6j?aa^8mGz z84vOB*zO(5$_+k;AI&tpIc>e6DNUdbUp;$yCRM!%f$j3dL@KJ3fyPk0t2?Qy`Xmj& z?mh^r7Af2r9FX68P6-7=@uAB4*ai|GoX4b+v7=?@YegI+kOtU?GDNv! zH=+Gj4BYByF6CCZ0hEdY4Mqy=l-uoN1K~4+7NiqI5w5e&Fsi8Dzv84q?x~J+A1X z!NCkV1xgtxAQbEnw+wDH0(BSDv`uK;6NKN7GRxTEYt61b{rSAYIqy3FjFQCW%$CCc z-Qlo=Jca#Gw+3^ocSaB>3oeGxP$a>jAM*B^Wk`k@;m;NGALb|kL-9kYI!U#AgnDxER)RBUR!v zZR6bjgLX3XE0j)xvUfv<;P;o7X78R=+cu@tyr{+(QN+dq?dUK7eHQx*w8K(m`zayE zoQhh*Q<`e*>Dm^8-nxD=6YLxQh~_H;>fj0Fxp{$(8QDoE`pFf%E9AqbvoEP*IYpl; zWiV75wxn>hQzB$WzD%S&D-7mDm_#-seYF^JyllM%UmssZWG02#BV9E~(#L|p6B(2> zAVZxq*m?$OKy6T6?JK>Zq)B9YmM?B$%t2_~kMFtTyS_40AZ_?gSU#UJa5(t*W^fzJ z4{Y0M+rvzi-wJC-TWsU%SFwJ_9EN<0eD#&|Bip(9O6aJn5F%PS-VCn@o?=5|EKi{X4CMy$E*h6$-vTa}% zs;5sS@*v1JXiZ%7?RO0A=OEBuiUsp4L~s{$;^PPU^wuY=^UQ6!J&?BuGv-7{S;rTJ z&slU^(hM{76Yb-2+f`0@QyEYLPJ!dPbNzhF9EB6XiJqY^Epp}hjYtlacvV;P&~}2- zZDpX1V>4Lla78sbXzQJk5n-LL)u$h!hkkMpGIn|POyJ|H`9a-dd_jtdCp8te0z~F9 zhfT5ef3$h;rRG;v01#wz&{cy2HvX~&zJ04?-+}tEZ;?@W72Fu&i-e zMfUU(U#^3FgX$Z@*ME5hN97X3+8D? z>>b6-h>uhYfBR3$@19`yjlg7uS*5dqhpW(YrKYYB^-j(q$TWv?_)out3QuafJ2!v( z*0^{Ul$SrN`24v#XFm61QOU>e*sG}a(qyC|Kmq9mA8EmOZ(dylU#3UFmW-Kc;#(iw zy0@|7=}ZEa7KtS-BQ_U88*_g%zP`q^W6RivH4iLCdUnXs=i3hQ$M-_r#jwbOFU>^x zVq9D6X=`EdO;sGVY}({o*EWf3Yjk&i+V16iYw5m*M23b3p}nF|RH0&LuiZO$L!Z2| zzFcSJozw~GfF4dB`&j%Qlq|?w!XRsdR~D{KIP(A{`cR;JfJ?oUbIRhci@VQMhNf$+ zdqu&-aG3M=1JG>#0$kcM(xpB|7o~xI{OyT7e4U&G%RH!MFhS}k{%Dw9xar&!%JV>8 zU9MH^d+->D@59(%RQu>iPISg@FhA(ghD9<-=npl^gJQg{WNqP`F+Ke&`V`W5P4SYPgNg_g|d z{Rtnf^1xE@KakCRW#x^`#`7j+Qj@Cz9|f4L;7EkSp+}BE zVPz@rXWFl!*o%ML*wu|x1rljsSpoOs+9Lz1f&Upg4}YltIF5hs4riae$C0x49^IW$ z6h)}4IHQbYl$CX76QPtDXGaNT??XjDLMUaPz4til?tcD;@8j|PeBSTR>-BtI3Z@y=XjWFKRo%`q7I_rT8(u@+ViDV67BoE~>&R zL}R@|gnZkLOE4}bw)j#C|13zrKZxg{Q?KsDNBWELBW=>uLeGqau-`g>kIR$2O%2n{ zQ(;R3xdjuKF1a>)r}z<>f9JW&ZhCDBZmcgqJ77BclX35e>F93#Vd`m3azG@Fef#8y zsjj{|M(*9|Bb+CF4PBK(U z)uROdJ9~~j!}&uE?{15*gHED`f$FC@QTlqBIVtY>@R`u7?|NJCn90Chyjyj-_Obeh zcd9jx`%<;r)tLXVHIC}max~hpTKj9?()u19-c2sIBlRv=AO2=Ap7+VvzU@@ znm2UEJZ`EJcda9Wu^0BAI$dTq&=8Xk^8qyd))y2BFf;2EL8CszB-7y!ep=A_^&M+! zzQ*J^BrtQ$gR*GAAR{$6goQajG)4liH;U0>=@uj68tZA!aLQ{DWyBzT8T+mGBphG! zD^&Vf+|6Qpy?AW`$bUgN-zMKt8YD23hr(q$gyWXmOG!{!g;*M1TBCvlW~2~jb#>GL zUCUE<%K0d~$`q+@|Da;MA!D~|TTxF%WuJ&jU zxqSW7c_2lcmGvxS(<76z=mQ+195JnUBl)tKWMzF+SmJbKlA{iBdf zxqP{hh{#GO|A8Y#D( zS}g-x)^{2hqvYSk8RZMXg`KZSqvwA#XU!A0uF+Mr`}w>3KeS~XZ@(sWisy(l`Wl24 zHCKuW(Jk(C9tdJQI64y)G113o^D6l60Ez$c?H@7^A1DT`08c?l$9K=U>43qdM-m`0 z9wwS(ST#$pym~&+b$y{pvu7w0e>B^>t*I8w774F*vWf@2_8Yi<7&zeR1_r2&J$!)m z$E7C!K^7fvoW*5dkHsq_xSQjj(`M&6hWVUaiZWF#G<%i*nln>m`5xn!CXLgm%;WBu zAj!jXX%-GYr2Wk!~$=~F>&Q75r zWp|ao^paSTin)L1_g#PbxG>RG$~;g^_WeLpmhF;d&N9GY$npapGCC7oB*TQ#d@_k$ z?dyaFTx zqSytcRP^=@++q(=7M6jOcUQKRtR3eQs3^ z8tdkqIa(;iQ`XKt7_WW2KPV+&@lqOX+QU*pl^o%2tY>C*BR}PVVMjQ^3S&L>rK_;{ z_3-ei;@dR{m*NKhUzRDBB0%f##2i$S*(`irI^TOl*hMHfH0Y7D*-7?O>C?Mb%DnBD zAqwG-nTm+zFNT*%oMwlkm9fPnlEa&Pe`HR4=VNEKIeqi*^c5(}o%WqqKZ=A<4+*2}Zk@9vzaO8 zd49e;G6QW2PrCphwfIrC64nd6=4q0veec(yuje{0#ZOey`}h90&V4dq#lMJjX9p=rxtt z7VZu>4Z>0!zlt`2`iq)%feI)&WV6d9)kvn@K|9Z0opE<~FE=i1Qb9P^G9e% zON;Rb&GpSsRokD01S%@Fx?u5jwY`Sks>;SM`>C zd{vIs_vTenr1A|=f`D`m=^Jl6;ZG*)KVwgdcTHvPWYGRx-Qz}p9uQx?0X(|K&mz4G z{QGfjubo66PioL#l;-slu(0yx!Rt?&H|(q#8i0J5%jxq!z2C$U$z9%|L?YsN#1Cjd z0T;9Jm#~nt`5BKVpSxEhqVKNFO&@J1crfc|Cl3y0-TE z^{L{#^l3^x=l5FX_fPL}ydqj~3yeH^>Tv(7D!aedQP|DzX4Z<&U*`lN)aY^D*5$$cF5* zQdMN>P3AlXvwT&3Ue1lD$GxXUa^qMID>`IN5BjA{(lAlc3Baj&g!tArFVg_f6|X0oTT2=$qhU)X#N;3w8yo9?_ox2cF#G&` z>Xo!%KctLG2t$7-H3R~V4-nfAZ+k16Kd+Y$-v^Il=+8tIi)B+E;Uz6WsXqhe$wFOD-`0pFyBf1+@rEBG@#>!I%Fylsh`|BMh-2+j7JE}Ez2M^f!HG}KT+^| zFE|VYu2xD;^)AkXUGniar}V5{W96hlQR7E(H7=)Qfv2?X)(e=vqK10!nc?>1^~3m3 zcFt^%L4gkRi1UcGtvlP>@voA@XaL}^+WhqHZp!BU`*V;hgMuUf92Bp(sChDffH%a= zM_o^CZi|MP2fGkCM;#k`tm{L`UUfaq3zMFaXL+&O#!U+9)YzsXM9ll3 z6NN^?L9%n&-uG+LKG9K9)*cJI+dX`~(bV%5ET%Qgx3!a4tNTS{kn!P<7~UDG5%!EM zP;S&>v%w<3ro&}H)ec71RLJRX;%p}`fR?W4Y|ncnfnRr@nwhfJJ@4;Yv#lmQdtwzZ z2>%yh@5Q=W4i z8Aqd2F|5WL6Pn$(0j@3|MxI3F6BfH-6OX|QP-g#BLglGLZu@~90Xz9`gbw86>2j!d zekp3|@?}r;sNao}&qFH4w!VYjH~KC!ioR+?wo;CU%r*gLf=S@!&`|Q9a&3P(rK;yTDez2Uzn zew4z|TeIh{_f7jPsDGTD8(#n98Fu}`RB2j4P*@+`cJ{-IP|le?c*X92=T1!8UqWvi*MF-4~*Qh($V;=M0p zqoHxVc5CFTktFcb6yX5vH5Cdg2WRJI`YSWgZJh!hVs3@ENiekqfGXWp8q{oBGtJt*H z6m4mrPD^ru@SMTPAfqLDS3Mtds?>uVp&A*0JM}yHAI-V@9L3vA4u8I&rx8>g`4pdZm-=_HoKH2&gEsnDbFtssu{p(nf;px3=o#2}x^FPKvrcM{p4PkLkXoQw7ZIr~gDvS)<=rnxfNNEHm zVs1t`jP5C#m!0=-{1UJJW(=AJj$Em|J0JMUuZP|yIe7(Xz76A}wQsu--Pv;a+=Aby zs^?+8pZ+_zsF5CO*$g5Pc-QeRC)VK3B}vYL)B!E-}OqZ@br#rR7qdWyv#nZ zD`aJ>FP=7sG^(-C;Cdspu$iDYKk4y%&plRWUO5OobGqbzgX;KiiUWli1YC#EdQqVS zJT><_t}5`AzaM-`=-#WcykqcFcdy*x$Fb?UPMbs{|Xb!ZX8A_oVkp}G0moSL!4R?xFuqx`h z*-qa4;@QX8bu*n@ z1acd}iHh|ObztaE_BkgzT*;59hosYX>Bc*fu4WWo<@wwzxEwLtG!@tRc-Lj`N3k-a z87HBbo~;xo_>$VbNn1uj`qFi3)&txM9UiQ_oHgk{`0<@gwuJ%yPQm{ zU^q!h8^ZE%C~-UJbK<9FO0p*YeXIu@wPQl9KVI3xlEGmQlMmwfDu#PvQD>a6X1dmt zzR!=Ub@!plGD>6bKmFu`!xqkrAgbmwk@S~r|0U)7-a09~rD^svi>nDkDZcX@XLC&8 z@W?eb>wHCq?)NC7pieS#Ke1~Bc$!x>3Gt+Ou*Lo9NKzH}^4Ri@f+OAyJaDY5qb~a9 zGS_D(b6SQcv=De5V2#z?)p@|unorg7dGveh$2ln6B&>6!Xf0UI)B};31RkQ^wCR!dxv?<@Eu(qg6FG zZ942vpB&{kUB3KD;8^3UO0i04Eh}T#3~FR2eYWz!=B>nuoN;c`--1C=qJQNO3iu~( zYpFlM=lZ*Gt!V;yQwfL;$x@8sxKXC;^Ma(!%qXTl7KG(|`Cr8j^bX%n)DY~vXnz=3 zsWZsBMq~Ek45uRecc~nNpWq{ZdH6SMluX&#s9F854h5KydBI)3+64CFMvEAXI7y5o zoB@DBi6}_Z@yHcW z-}8nk+(7wCu(3_4NAdvcuMAXFFoL07JHQD2P^0b+no)C1H9AtO$w4@YC+ z=UR6G0j&G}%xBJA)SFJ*md~@#(tF}(Lps|eFz%y=nf?<-@{eF*mE0@J1FqZAVqz;} zH2EXib7MySIW1DH`0o4e)QvZ1AN-a53K7EGm;fON@qBCx;Gb@WC)OmjRm$yliTRCz+*nV&(EzK4`E91 zlaaY~2b|66(BZgEtaY8z@4a=4e=CmzH~x;PPEJQR1?K)8{yjZS)%mjfC|A7M2T(s^ zVuDRJeonE(%Wq&-)Pzqy6;;Db+m5a<6SR9wJG>yN#{7c}H^7u+ov{XSNpzLt`716g zU~oT5lfZtFBi^D|WNI#NkM1v!OTm`Z{|qe-XIRkkA&9u=gNQ^30d;?>&XUSVi7ZoL z@0gM_^AX~6dS7XY=kUk1sy-H@{C1+{Ik#@m^#KPCN_F4_iT31q;`ZlQqfPI=BiV-f zG-{bjYR;49OFCT;PxBOun985*ujXymmr@}409FY8BLb~d4FXgxRq<-rVQgH!bufukX2#j#r0s1Mr=udS-P zIh#>akq=~p^s?k>`|iClj_x(SiCJM#iR59q?tsUzQ@26l%7p$?uGt=B<&tUf5QGVo zMZCyI?FgSKGo$!W3&6HdH#Ltq(S|09Hc{x2D{P3o*q0j#ybX(e@>UNL7G>_NxXaZN zT3%H5*=N~+NsLL0VfyzUO)jA=>fhMklU=6?l;03Nw%un^ps;tZ#Q>xQ`N_$xLu@aU zo$6M zxa2!vNkiP#-vOUl1p%5-KD6ZU6Aos=Eip)5FbN@uT}7lkMbw5odEo7RJC(kJuhlnl zjKpZJI}3U@^WDT}4gM1&ZV5MuyVuylF5U3AZ&f zRF80bv=I89rjes4(>(8o>|738ntMZb^{*^N3uNKn|h4!^L+@ft@&k_Pe^@ z2_Qf~iE1Z9D1L^r3;oioAg`VW0cC>Fa{@}}cspF1L%Kp8#kc9zpleh3FvFZWPA3Ay z85wC5mZ`HBi)FB=skTSewesv=${6@!lM2jKA+a6(<2?@+@eXc$iBVX`psk=XwRDUS^^hGGCt);!a{jFUiaUjdy!$o27 z)Tpcgflqzpmc9lq+m`NzQ1!Biyqpq3qbO&;M4+^zJIc3#v6+jL1bPw;yGZ|uZ@p?? zfcgcq;Y?bUmx_4?&{`X1M!S?XbUqNPl3^V#g-E{xgm2!?lokR;1cmTPrtdTqby{l@ zp+XI{u)rtV9zElMPgchfj;raiI@aTLZ|d6G9y2hb;uI!1d-xxy6KI)W1WPv_4(Fit z+zw-@)^#BK;i&w!=^Y?TctnOsy*r~~=8cD#GO~6VMynHEaS$>+2ov>9oA0j!0{DH4 z%!}NTw-jJ>^6RvwsTB@HQ}QHfdf9dp>1Mv4bL2y?^$YRZb7%hnVz1e+`{cH zsPVbTfDrEIm^3cz>!w4u*mT6?P0qStFVdt3-}%kvb=L^s1!O>`LNv7pWP)(N2Iq)W zHy5;GiEPI|-Pp5x=iXf%38!}MD~B!gEhiGDn{aE49;bKqY$4WDLNhP@Ighyr_|K_h zMXe2bOs`nz%mq`0Tkj+Zvxp$qxvDMVy+Gj}E7Ufc)qxUk47nFKG$~R0qrf1JvX~bm zZ%!LMlBOMea`U^(Eilxo5i|Twg`Ua<&Gy2q7|;mKZ+C-T0P(H7V$UapyCAU}`q-~Z zhPgsHi4Ffu~D^zk=WLO9sp#T12Mu~ z6OF3mDckw`-;)<;pILYp6M$txMJFD27$Urp25@K4rAXcRMtji4QVYHdKfp}7kTHw<4vuG(y$tVJp*XKv zc^b5P?teTfyC>@E)n{sz{|s2M2*3mL;ijk4f;}qeqp|y{q~w&Ym>E5Pybwh(O!h-4 z%g>e)ZeAL*ZCs#|@YB?&?pqsK{e4o8Cxc$ePIqCOBH8uIXB;e%ZwkYC&y>Hz{Nk6? z=kipD;}@u!U9jufurrTP{yT&S9th|AFQS0T^2*BgTtd=<3 zKh(h@E&%neqx%)N7o_$TED>~ordZgwQl8;jgz*-dPdX$(HV`p@*4foJ`cyxA``IeM zOc$SyH^uN>f78CxwoT!=oG1Y1HioNRJ?33RHabn~W~Y>G3{>65K*NL)Ly{p;6fYNL z(JvtSc=pCyY(UfY_m}(+9`Kwel!!M61jtS``7KA62wd$RHT4w#i~KzG3G+T2-8Edf zX{_nzpZmAznfQz^q`A+@i7xyNvo|el$QZm6`0lm_wk8!2tsDGys`-Kv=etY~@!1OB zR+5hw4~DA6xyvvk$BQ8OoC+^V3n&7N!p*Bt8XixP{8~P1nrFg@Ud4&C(!zgL2?Sa0 zem$Q&I|P^R@0~t~1oOh+dlBn5q%ygG)71SiUGDtGh{UU|Xl^iL_m;tj&f5*(AC#@atUEMPJgV` zP}@u{_+4^Xa=GT7h2sa*qQ2$Kk28ML$Wu;M2>RF(oXViSG`3^DjrbdO$MJ#SXhGUa zgqG>oJB1SGZ{EuL)Rd;ePoPRHn97@I6OqK^I?>VVKUVts!iIb6p0etRN8|^7))U6A z?n4-y`81LX@0hYsf6o#HJ>p_!H@ug`LHH{c_seFV+qS7wCMaBKamugZWzl zO}sQgbsnKcGotfT@H6eb1#Tx|ckXsyJ8-h{af&BdqrJXRCbZOrZOC8M+kGiwkHn-ps_2n2 z`fxmH-VHDc#{GNavkaUFsIt`Ql~WN|5xM(#L-O9&vOB^)z)+7sFe9=T5f`7&&Lzba zsgIX`0qU(j|Gk<~3EyRB54S3BKjD%t#A@F>M>k&~i68qJ983h>KK`&YxYjlBd5>y=^l60Z@q-W0m4UkV3 zYqfBBHHYTl!!`5#j;6y;C7-GzZVKBxu)afgcp(t+;y7P_HzW8ADMsCIaH+pCtbJpu zd$^B&J^qB@ImDY^cpT6D@1x`9avxbf-=}Ic?jin`2+B(lee2SjIz0h84KyU=B$jca zs1kQbUv_wjiEc)34I2VSRKJfeo?9y1qY*R!1yV!#p4p2n3X>$`fwbx{zDVAy36*4y zqtIP47Ciy`V*>BTTi<)`Mfd&qbM?aLwzon~Le$84&qLk?#|_^8jO?f{ z0R_idO=fE~qO%vU5-io$FnGkO8V+v$84fYHbqb#mhwlh+fJ`ys`k)6{Fa@gk(B~4_ zjzFt*zWr0j(s6N%3CoYW7JR~Gzb(*~leAx~qiRP1A-xyYs;&nPJXchpx}ywbhmXm| zkD^{|4U+Onlviu-XMyeAXzj6R@NZDi;F&vZd zI{!>E9ipxd`T&EY32St(Y02(i*{%L|OkevBk^t`E$-bicv4pFGkFOmVZRSeKS^8gpK z(DTWVj?}StGKe`SayTF2Mx6c_M2=6le=K(&n~Uo>JRW;rkFeVtnrt&krmSE2lg*@l z-*Yn6bfKzL-U4w`NX~aWknYs}*~fyce%U8s$FsjfTz@-mGm<-B-=z_KfB$fBUyJuN ziOy%syLRKn(OXGAdR3pS!Z`!xR!U)Tcqo62&N8FQm&A{If_4B`HRbPjF57=An$@du zn!;5D zsXBqB($+fa&l*YvAnF6FTOlo(O6@wO_li?AKn4kwmG!>rj?WeEqi^ z8#zbH&u*Bq+nbnyM$^b(NTdyjYe%CHAKW$u&B1A@`N8ev} zG*u{no_X|D;umufMmtiU;f?^Ihu9vzv%wle-UU)YbNbd()uKT;0^c5-eNS*ij`(C~ zz^#CAc>Nh27O*3x@Z`4%4Yf}ZVwT9ZTUQnKM%z@%B zM-|9|$FV5#URY54Fm?<70(ezG7g+!v(|GPQoQKnLHP4dI{E$gP+L>c+5uJ%+=kto` zHP5fk7>g3%J__2_$VWRjdiY`ag(;76Uq!s7#LH5=9-9Tl8SFnSr`+O5HHcQeh4=n= z@^MZ$yMu$=zE?6BzFLGM4a1P9|5CzNiiA&x#Kk=eZ0MB6mUP%i z7qvY8EiGjEKSBwSSirW2u{|~R1)_n&x0f}r!-fPFIeuhpJpdq4I)$EJwnCM?)SnBj zeYa%!UGr{Vw!eyJNY(6uBYfof8sl>n60IakO(syAh5Uy)$>lR4PG6}*{7I56^8WCt#nZ|F(WKr1Be{o5U0RE6kzb&LEIbH0RoiH+|^bb_VVzcc>1rE zse7TsplcC!UoIdyd#=UeVodfKz`NMQ{SP9PDZ&h=ery=zXnWNA%BoUBNH&tzQUQ7u zyWQ${4~iDSKV->gNL7jY6fKjdx(zbzrMu0T81B5gK;A=#+C~FPj>XJ>Lm;w*H>VUR zS8^A5R=2`5N+Vf6&g!;)?HR%$yJJ0X@(fW3q17G28QNhVANw&17?@pZ`j&>M5E<%By4y}{O}18n(qVXMQ8z00J)M6!?6p3M1yW@{G@yi z$d5o#st*;8EBUpb*QZU;aUT~DCwAbo^RqWc70Msw3nvFl zE8?MoIvYL55rtXYrpd0ke%s5~r0*^fzmg|M>t|z>|YYsFuN!3MJONgYZ@f!{0gjS@Od*+aqHxLM)-OmnLaZ zHN>~*&F(X9$!Kx(P9I&yUr-z0-Gxy$w@(RvB5C|-$}(Uqoo;#G10!Q|wV6Ce|Ee^g z?B!bl?424#tnKAL+ultLB3xGLEChtFcY)tFg0b!<58P!%@Lb*26=L@=Yibwm!fJNI zLR>v2Q7lXBGTxJ$pjU*=tn|C;nL0U@wc}jcePuj z>%VOM3IAJIUsqRG`KdpB{Et^hgYbN9z~hO{p6$Rq_2DOH3)94~?GW#-Wz9iN%07~^ zPuLeX{Po+o&EEhpoemr;nwm19cD-hHF5&RFWtpSwtcp$JMSdPam$eGMf?tbzI_Eo* zD+U@0_hHb81V{3!tq$)8{V$WDanp zFF**AGkREN_A0ZTz;B3&03RTt!i3**hpHK6<{z0dUC0R0mQ)0yA_jnQUA*Kbs1N`d zs`mX~+S{QAzwXb>4(jM24CA6|<48KdTVh7ITDS#yU+=k{3mj|A1ZQ$^MUmuN z1ZfM18X-MmC=u390QGG%Ba2rLK#x(H{U7AquWEm?HTUpk&LKE)7x?&b;6M81oq%DK5nHt%ub6ni}D%o|~N7fqL{w zT)GBw5#&o>ybG7>+!Gk?<(KHeftz7Lpo7TzihnnYKF;YLvx)NzL(P);tEC&$e&I)1 z=%>*d>6euM?f;@)Rim;>3`NWe&k-^aEi>=qSQ%|j@BvrZB;07Q!XfVOTvt*<>j$sx? zxaq=D>;=#G^Sb|AWWFx2D?4bkNlB)x%nv=tJG^e@VY&*PJdPp1{pr7-@^E=j|45Su za^71Mc!^=e@n6$8(#1)90Vk4rWee0kAc^p-@?&UWaF1~JHP)g2>%p+{ zjw?7Jq0pFFTWOX31}r+x?5oZk9meAa6-F{hYKAaV06O2)_Qo9inm}}{MJs57PhKKr?quiOil4>~U%$?Xi#D0trU(wB|5>g~?9JChk$+wqAsyt}v>paeY`e)~tK zIKueTw_C-V^bwNu%`KxK$6|VW;zkCmUl>%d{VEuw`U2xLbZDCcCw2RL=0drl7QfKk zM{xtq;K#TB2+myE8r<$V`IZDR2&VEae99I*!1w(n2L~cXCPuAFsPQ*a0MN-u1-+$& z5If)E=6}j_>Or^w%k3Fe*`z7#Of4rXkG(-fhssU;2zv1}qS7IC$=KG88q}Hg5o8vm zr0V+$#n2zjyLD1Z{FL??^Sx{eO7A6^ZK_z!s#0k_WSj&G{SQ!p4LSY_ zKNY^5nyNlQH+YzV0A~EXsF~S>bk)RrUiZ@vho>Z5h$oA}QmUG-q}(Rwz6bPw@__|3 zznKmt8u-5IkO^0kp{I=}2X9d1TENZ*nM=6->2hHn%s4cj>ST;aq1yf@hEHhRdD&HR zc?x$4jGhJkog$nSB~<0cLD7CN7UBDAVXj^xw`GbbgiQit#qqPZhx_h-1g!G%4@xt} zF$%y-(sQ?iJ?bpzssC^60qb?zD-<5GstaY6Y&%w0`Rf7Z(Bm~_VyV*0j*&(oaCh#f zk65zMT8z;@L(P0*e^&dmdx;+>3}?^siWj@eg|x8bWO7)B;IYG1^PfvI({{Hecy&hX zydERa7)GH{!Et0H;jhlS^VTq6VJUg9T4|6D3_C?~F#ZBZ@l9rXkDK3~G-V{bk34O- zUyaX*xC52%?kZ8%<%zG#biqs1#bL@ zfB*b}jT1E3uIZ#^3=&={4mHC}P&BgmbIsM>Tm$fIl=rMg`$TRI@DNnNKS>1(5~=vd z)1ZsHH=t=C;^YE%bp*-(W)itrai4dI6Sx0>idlg68M9CK@&mq;X#!0alA}@Lb>r}hG(~x7N%lx)uZO0#f ze}C0VFPC-KW$)IPBw=Cv`ZwA2It+1W-% zD@-gO0WZpn5!6nP4=js?m5sL+T=rG*zSO8Q-Q z7}^&*x!uRWE}He|^P=+^=KB7VFX6F|-N}|+xo;@O7DE*EyzrFHNqHt%AoqZuaSw>+ z>?8OX>aM2I*x?m1dC4(B#g#c5VWm`wH$t0_AfxyRw0}SY#1BSD8r&!4bR8GNOnzPR zLwAxKsq`1kA7Sl|X1os=1R!cA2%XxnqGA z>+O2L&VSP4WqEOju6HnM8Mdnfn{573}13#Ra{$!^6B`@)MY?3_2kfMO8zu=VaUL zKl~>54~sqt?L2e*dPC9(@C<)+g#TG^+mMAYiMokxOl$=Of80D(oros-005<&G1HGC z@FdC*9n4vEZ(x0LP@&~)>p|0_$J3FsY$gt*yo<(#&`QU3>UvO>2(uZT8JABsy=Eki@@a`YiN;$Q9u?KD%wy zKaR_LKTir4V`;~micEcjLrsHhaRsu*mneTvt(O>gv*HE1$Wi{uJc|OZ{Uxs;!EB1U zzQB{4nlIx;Qb&tLb`~3uqEv)es|&7X*a<%4L|qAjLwJ|tKU&wCcd5SjMy1Q$hmkl9 z_57``WXxHdRmXmPKG|>!MvTjH%cSg_Q^R-&P{8e&0$uHY+(1@gVT=VX>4r;ys0mzb zV40^Ewbm5f#nA9G1{vzPJ(IlZW1hfwjj-4|Hve)MmRgB(&YAi5PIhtY&gS?Um#uUy zJKs}__%^YVzgJyfPPdEP{}MJIzLkBBo2xNkA(O=9e^4K#Y&sKp5DqC<_vaE#>^dO? zzzED!C^xx{*dEwsh8voB`e;=v3<3Zu#*<54M$-E1`JD>}nlIo+P6ZkRvZ$kXdsO67 zZZGBW81?)&m#X?<(VLuY_bv7vD3pmwK)99TgW z?_4L*?3ZR3ce#~(<3U%z3JQdKx3vH>J+r7t;m)}%n%-&QRwPZNvRb(7x7X%Ne7`fQ zp05z}VZSYPv^-rko;l^Q9X$h`G~JE)N59a=bZjVq`aq%{_0~6aqvD&nDTuaXsmi#1 zyshzWh5mqPAm2%g{SK-joFY%B{whyK;mUx9!3IUa(`l58W}e7R05O8FpYHw$d{~_Y z2JvcZXvn#3D*K4XE+?|CF*YXEha};i;QnPBc`ACdJM0_M0QN5t#Tx2Lbli~s--f!^qqJnOCWVP@K z_&j6q%8#-6vGgJ0^M0E%dsyZ4w6l*P^f`9-zVb-i8;1c0O8fq@FW~Qg>C&d5)I`8t zHp=~b6Sx0>inuvhLqODfCNwX8t$9Sgo3W9rgsJw(2^IvGwGENu8E<~0c}3Ou=J8xe z0+SdOJ-eA0KAl*a)%hVxQ`C?D2f6jF8JP9pBI(%j^hf&MsZ-Cywd z452RuZH~V(?8RJ4A_zASyZUCx4X}7Eo*HsE>$-}-1*w$VUPekrs&)za4IKH6MF#Nt zL85g_<&UOor=gZuC2x?|DL48U%yC9!w=<4&U#hB@g7@b6E)IL)&%(S<2rP-Tcow)F ziiQ$GKW}PEbl-s&3os z^=-Z_onpB(&T`U~M}0`!{A5Rw6(y9}D=eGRdWMno)-1gA>P(IPU<|+G9mA?{GdH>6 z=)>kqAqnrYWhS2<-fL#%_|%BGvv(jWzTS$w7*6KOiN<2+KC;ngHoO{y(nE~8Bhiwg z1W^WhhONJMYDyEp&YEwcO0T|%T?M%0DCcbqtPCPeWj_{2*$P9;%skJ2{1OV#Y{t3_Lem>_u=iJwIeXqJmN1HIeS5jepnl&C1PBB@D8`xNE-XCLk|E}ay+1sbwwI1al zQg&Jg3Wbh=H<_pS+|Tc5?zGFJboF*Ep9DW*M4X@5|BnSC*@v)K;b>A5z&AYzAyC5! zaC9MN9MgHJclq~Z)qxHfpNsu+ygqQm^(-Xqvdc*6M}uIeiGn%xc-+zqE+B z)O*RI<>eazd7+Oi6>gkEc{(|L+lkLjcdH$x3Oedtl;Lp9j- zq=EhQsn;KMJb&Dad=ig<@R-8=d>Vvhm!EblpNOI+YI|=-BV`{DB~>dvm4rZ4IAYi( zNoBN+XJ6hlHQfC9D{23@d1LCnvapKj33WO1=i(L%c1Qxh`p458_UaXP4X_}ic*wr! zhlWW^nC0b|7_qPL$j>LT(iGUSF*}xDl?2okY?)2ifJO6nWv2tCL^>3{hNs}{&og^I zv^72EhrII3O}4W3NMxrgzJ00h%P0I5YXT97?A~kdd8m{3&mg}9--mPgEwQ)3UH6uh zzdvIpHs+nT*v>^r`<(bCREF;ObLO>J7y6GI_1M zSU@sKJ}DUdcgO=~!eA2{utZF$VLk*@Ne=heIk)iaH|-Wr?9zr?KhcYo_`2lk=l!Bx z`5CoZrqK(ggt7%Z7iJ^(&uSql`VUlF=H4%97ctZ*0{r&&ix?6v8EHc?ib*fB42^l?M(e!Y(??+ zFTkm@xmw&ICjTK=zRGN zVAS+3xN7-zbQ0PL`5nea{DYe;pu;+I&sp~_=eNWXBK%8&cb;troBd3Fur2wOm!AEo zfCB*tBO%xG!?WDeSaNMXg&62b?M0IvW!Z^Cn6ej9WIsLx0$hqr|dk>)Agsa9zbc~)Of$ddgEYDm*^)f>ODhe!yY4coVMbs zW2|78PLLoEArv9m6BJ3=JR1s_@0XuzMlCVvo}0^ zk$dklOg!Yy7)g(V^cNZL!EC<>FIgpmW6AZp?pHXrlD`KGlL+@>!jRD@hYF_$#?cI>Bgk5eqEW4 zS`1V=zER9LLXC&b8qE>!l>ODIX{@flOM*TOZxD{dD!fWSS`uJ)kNY9Y92*JYX@L^L zftRKWvwh)mUAcG0MvQ*>6}P9PVdEB6D%diY+$%fZ%5McUl~pQ>DVLs{rnPs`>vD!< z2DMI1@5I`Z%o`$l6}U}+R|G$0O@rJ|TEPoS)};62#JIah4^g7>I@-4uu2d_cgFzMsb`r7i9IP5l@O0kBaRw70WRfj`;D58hh4^Q!l@u>+KX-ZWFe z4gP^PO#7o`6SeeK{9tR#C3a#7`sUVOXEW2fJ^ zc>zSDov3#c8?JZV_}~==AwV#-wk~+_aDV33XlrC&{B=>>i6aa5ERFhOg~VQR>@U+Z z$wEne=+Pd^_Z^38pZznn(m2yD2ctV2VTqd72G=C79hZl8RJx4rN8jjwF4Kya0AeD% z-{}r9PkzV4%04ew>~t}N*V}7tmGEqbF8B^_nW&S z$L+TS9a@YOkyaw#I+%O!$B(!oH#yjg9Z5jM<#(r67Om!N@WiuvbsST$Ms|X1QQR`i zA*X_Sgq2QtQWQzBUBxz-8j@4+4XjPiY`Qny@=ESl*S)AmyGQd+q#D?$*Fe+IvC zQYQMe7sxoeLZ#PIe15yX3?Nm9Ep&q=P{uZX;FJRF-6ZqwTK3^5L0kqpCJq_8mUAVp zt`HedJ`v8?z|r$Lc^JPE-f+V@^3(}osF+tu|foPWl_=pE}z=sGW6RxFp5odiK7>x*qo}il&z5hBtG^z)V5jFrFA;p zEqv-w&L$p37xwXG1Fiyz{qR}aYiWHTMF|~$l8aS{mDKXSu&89xBtAeq`NY!zKVaC; z?}qS)?nP1IOtWVLKP)AW9jIloUhj4U$KEZZTtpbk)j_0AiwxMfrnOHeEUR4)4v>LI zA?w4_(}fn&FMSJ27r1@!SxvC`d3?e};HOD4+XVv|sa`fT3}wg!saV{I4EUyVBYOPg zbV^i{k8#Kv*T_uzc_ThoqB6Lp8+XTF1k$GI zMnjHpm%p(B{8yO#r?BPgZ5MJG_z$FG1W-l%4$AVYD7&{t3ulE~F8s~Y{yw{l4xfke ziZH?4$3cTGE68~n@^qSA^Y)h+x{o+PG?uNU)Z z1e(ce_$PV}26m}-!bysfnOYT_AcbB{XuYBQ?!}GA=lZPNo!zoQqSwfT|Hs zJv38ec3YM`gt%66yc@8yrrmK=4Yq(kT3_kum9U#;LlD7FcC!Ryv`2ykh^Jbf7E0BA zy|*UO$M|SUNM_zRH0ovUYRlm7;@>b=zP_ZmpBfI@taASJBo0Ll~wIMAr2=mUG}Q9!7{ys7`;xb78QJw00vv_$RK8_~gYNU?=7SA@&J zaKa8pCTwVABI$C2F@Nj}#*2DyF`WMrYj${@b|mpEvQYF){zLQzU;QELJrz|K+ITFYS~4&CmPq&>SpicULhNnR7reCp0&G#uKN5c0i!+g9p%5 zVh7|L%gxP86$$!!g8C<0pr7elg3cFj{wGDJQZ<=G?#Zy;V^6Bjuel@0EwnC96r3XD zguzCX^kGg2l4P3}>?N>e%s+;(p(!yO2n!^2o+1&TrAi}6rJmmLLn>f)ECa29^@fV@A@ zhlkEatr=R=wTB!J&f*9GHu>R+>`k;68%*-*8r4z%CwJ5j41;a+_8cAgu3 zO5e%kGYc=&TtM9lpIHzQJUjD*3+SK!;vTWts2|jnx5Z;~MMJpNr&zv015k&dO38uZ z01-U}q!}mBF$Z}!A)J+mgWPTF<>Ovc`L%E9L0SNxGzr?&6HVMWYUic4h)AnD9Pz-_7;wsjR7BhZ zICsaGWy!bgKPM+|g${-r5pI|Np1D(X=jo;}ckP?!hY}EU7q&RzL=+;!WeRQDFYhTb*j=S%(VHUix$%}c*0~?jFe_43R`wn15TFmoo3ZCahu%8#t z-EV8XCtt|&A|f}Eqdw*LW%s5m(YYs*;(*+YycTDUV2KCpP5?u$JH6Z1z{jHa7j?~5 zHhS`kdriJE@F66fJN8VeRChjUrVw-HQu2amLzes8I!N0-iV$J%jC5|trm}owy_@V+83(kELg#;2%7)|xicG})=b_?`!8`NE>kmshl634uVZtH?<7!*2`8qmvI;05rOr`) ze8a~cV#b z;Yfs-MqZ;1lZ$TzOCSgiBKm+?TU6NwF0^HE zhLAhb)K*|m^VCOMfx_{ZZF>e>W1e%6tZrS7DXYuwvH0~=g0r@Iq_I;f-)=YPT=?B9 zS2*7d(KGz(O>`~m*F_(0 z&a(?Lw3uVG6O8Pa{?Bea87&{jThluC@EIY!nOLm485AO{#Tx98IZd<$uDs-o9}l1; zgGD#er#jh?MuY#}XN#soU3-uNV6N}$CbX_&l729_(JcHhGy87)4b?9vSW|r$3U0z4 z^YpKS92F6cZP2FqgJ+A3^?l~b@P%`%ss9~Kycq;KUF=LDa2AH8T?N-SJ5%8HMN3k| z2k(xQVxEL|EWyxIs36sYuD?urL6n-$88j_)idtt<z1nE>?cx98)3vcTz&Z9%b(MgYoeHdYff zMOd}4PuhX?KQ@JcAHZR^mzPfZ&zKycVWF0PrV@n@FwJ1CDuE>AXY5l_K)g9bjHrk z>$XcL<7iEd>o@2n<-0VN?;_S2$?yf3h@}-T z5FTy8i#=Od%o2>*UXhq%iGul#DqOui%jXRj$6P4~{LV9K zWwuhNqh`=ze#bH$c%ogzekKh4qK&|qgKG(b z{|U~mSPF>96ZCuNM)wF3_D|cDf^G!iD~d|5Z!mgq2?%TaZP z3cC1jfLM~3`Q!k$(ZoTuzdBcB zXjiu_QT52uCPZuE!>;CY809Nf4MUv8on#aT3+rS1UJFjwL>Edr;)a5FAB#C7C=FeQ zaZhHv1RfZE7bAvb55mqqz4eic@UX5-saaHLLaqJsDWn1o``n6^Ug-%ipVdh+*5Y)F$(c=z2m9y z!X3AN*ppnqseMCnVq@pV!Rprh{<^oQUl*|9{0F7t8M6z$RvV1H#6m&W#W{d-Ez5Q{ zd~9ts*B*4QEr~e&^b63_u#y|BxM~-$GJRM8LKLOHv$xC$3rN>0EF`qJY&fpX$Xyh| zW%s(cSfxu|-_+7%GqV}P%CKBERy7Ixyw6H)62W#|(Pvw1UrKK-b7PDO}!kobzFrxOYGvOMK zi0jNHMS5B@HC!CaZ&|?96ClB&Ib|c52b_xVH`aOFWNQ(<%H5iWu@th{<){ zCgN5Bi)Wa4|cGyaRI!9tlef`Z^B^;JNfRlrGOcazxcJsOiBiK^S*R) zFZ+Yd_3$^saV>0ZV4^|hHd91Ab@%Q3h+R8ooHz-sapiy6T|^xdz~C0KBint7NHuoH{Z?F2%V+qQSxmzX_uZ^zgVk4GBehbB2zi{I z>ubXjYu)BxwU0z#cf#S(YD_YyXE@`@*OtzCkgj}5Ssd3~-PDj-FtrqWi-ZfCAnE!j0k&H{^~2 z66mUh#83q&ZhIMa@RKbN->+y!Cp={CZ}tS^qqr8*@^{ zWe468Q^6cMJndrg-W@G*Ui0}xFXXmax?5Vx@mGN`pAjsM?+M=Uaa< zy;&w1akyF5{E_8m50f;6BS*8&tlcS$R4YU6)H3&KtRW#LdAaUWds1n$OJpKlH4rnk zFC#IvbC^lRFC~3HXH6)0ceWqlM~;?vhPHx>@$Y#uPs>hwUhGTJ@b|U7Fn_D}+4c4o zz9q0J?{1<0-k5s3+`kZ6NNthAbCu};_lTCWh2T5zMA8t7v?hd%r_eM4L#a>SkYs4_ zUCdj+dEU3%GVGw%ZI;*GN{{F@9fjc$sjMiFzJSSu2!psiRu=0tmlchaR!wjJz<`~^sNUh+@Rt_6pkD$tSpa-ntHD{Yn zV#B_*j?WJ>UE%xfojzZVCnGq}wJP*)uKFz(`Jh@XCak!oU*Nb2KWJ;Sw`L}kcxD{k zhmSIg(j#oP{PhOxfe$}l1cbeVX$G_Hlr%l$yz@8=GtAQV=Cbn+ODhWjUVJ)W(MhXo z@}dT#wCfogdA3mugpZ8MnsF#(_U0HGJ^zY3aSOJS+%Nim#+2sHc^`bhHM2k_-pd!g zVpK)GB2Mgi(Q{&wRa37(p#c}jvmq;p-fCx66wuhnO7I4bydT$N$O;w{9aoTE;vsMd zl7ZpSX1cu&hIFj|Qx^*vXTqUEnSC}h zx#Oz`>c>zB?GaVA(GzA)jSJ1vOR>Vu-_#ece;p5XtBBPY&v>ulo_HXsNy^~cIQUf_ zfx!ad49YPjY;;N$K!|ltIoeBwG@rabS>0b6d1$ne;g8-& zeU|A~LJi)eL3Np+2Y$tyZQHeFtYK<_RDu{Vx%X9=xble%b479k&>L_q;w_GwrC!88 zabTh;O}Kc{F?{oyo4g+S4B_#bL=9kgdA^p&+}zXYT8J<-Z~6PA*YBT)x$elyNVzG_ z`LbD_SgkiY{1)o4A<9td?=frKMT{cnjhj|_?0bO9q_m$2b*qZ+5%;glhx$Id2amSo z_f4n?66sc{qH)X65x>8yhhxmfuW|F88_z7Ic~r9K?mtP#FF(OCqSb$JW+UyTNUmhk z(j`yk?TB=V>gu&bhHTfVZ|_3LuYDHDi(GiMEOrm(M& z^ifV@FZgAJN6X*B#g-HPzAeVZQDeGL17~qFv6j3-D6^gXRiPs}pnD`pFSZho1ajPzr~s))voIw;hU19@@n zvi#Y)uDdrL=)}DT&?{~CQ|XOw@vILnyn_XIsbI7*lz#^sSek7f}YgVWHIKaSEDW{*=nR=FM?Ga)FLwhK%7gt+UV zxk#9!lo{<#y1&Q1aYX!Hr8UJ}c72r20z28kVl|5?7!)f0kMNx+ESM<(453m(W>Ilj zKwZ})eu%+UKFH{1DsYwmP2C%vedTsMEhymnb?+dB4R;&1J{D9Vo8Ehn3p7>#4pw4? z!9qv}49#_$^1H42?rFt1zmjk4>xL;7q~Wyd8Hb^G%MV|^a7%{YB!*dXJpCy4TV2Nf zbIV_aJZ`K|jDiZmHD+27kPOcMW8jY;zmqN+ks%hTbCYn`bAFNKJSS`^LCpJQ^JxCb znmft&Mn@K0(zdH#f~8mlUG5j8`r{cMNY@4J!pBa-s4yTP>V>k1*VnMRg$#w-6-GjU zg_t6uWc-sa`>SspAR57YjVMoqn#WZ z=$_sy-L<*ElkV;eGdgxIFX7S6&Yrx4wO%46i1*CkECjV2Ty*pO<0%eruVP&i8U=0l<8F( zwJC?h6KFDf`%xDh?G+IUlIlt(ZHz9~DtX{zJ6|q>1oKAfsmPxp+_uwGx)}Wm4ST3^ z?^v~)r05L4Y-*%p_VYIeQbBqI?+I2UG9`1oX(imqXKKKRg`<{I>P}VqFJ^u3Ho(o@ zJjWhm5*(AN4*_!zsv}PUq%-^^UfI~am=BO+Ky3R>jUQGZgI*B6@px)qqRu$CvSK>L zMZcaYpnH%?bq6VDe{8J0U))ze+!^P9T00Y52?gySz*GWZ9HcY9@qP@?9Tg3H@!=TV z1i)_ByExFY{!PTc-X0zpsD6sYg|No~8`z_qJER&Q5WK$Jy%b++c=M5yXXqCBXn(4O z*n1)NN}F=+R#XS11t#1gl#Yh&sOaH7^=tZk+PnReczC#`kBtbu{de~$cj4!v2lO0o zEkq1?!FSYArX+0mL(hQGUlV@R-b4Es)3fO}Jdg?f_n=Ay#5Xf>*%6l!oi^@YLSc0| zK~XEmFSDC$8;1LJ)Ali@UWNL^)_UWp(ZlfLCF4)Ue2}r6_GlcoTksFFr`WveyfvU$ zf$PmLRXDvLmMJ`OE&O696 z`~hD+kfY+toChcMuymO?dv&eE*JgXIJ*I<^|BM@~>SEd81L3Fs-Yv+mP1HZdb>cYe z)m41+MaJ`s#6qO$hlk3!TR>Vu;*cg8Fxrqa7~@~lD%($GwGE)Xyrw%7_UV}h{HHjE z$6)*REM9ice7~DxY89i&P}y1RP%yInndrZi+gQ#a886JQ0vLgod$FjrV!i`HLOck) zk1K?}e<*(EG-J=_LZVjYZu3&96@S-wfAqsVDPClDyr$RQbA6!flN>s2=kGwJ|EzrM zNqryy89Nhn_^UU1=dXKIdB>)d7Oe*pNK3edaSu3fP2$43im~vA3<=aKkkz5MaiF$C z)szREE_~(DA4I3_PSs-?ZqD<9#>zYd-YuqRZo!ErT0`39Ci1Li6iHT&#T3EILY9`d zx%N~>e<{pl+RhE-jIISfik*gt zmO=>3i4^nBDueppmGxy#0D5Z$GIk#RT8PVL_J9zGIQhOY4LYcFf94<)og*Zhss)oW1vx zRLsH_k;5xLCZ7Ar4-F@BD}OA#)zNX;kt}S~FBa!urCW3|;RPACzd@h8>H)Dw5l}?z z-+S7ZzucIr_5EpfSv|bqZ0*f^@5-HK0a3q$YMxqXpr&VBFfa$96#Thg0bqdv#ZEbd zCmL5+w%bbkUiL~~czYyNwgFN8*A(ONseBsy+bMaiyJyb(w3t@d%)VJNT!Rs>yh>9g zDy)DO96+3J;kMF%-M-fe+X)FNnt-r*l;cT>RXEB%UqWzUyz)6FU2F9CUcq*lLg-)Xi#+dP2L+ z-50J63f|_%-_;;OUC(_hmcn+)ni4M46YjFI-6j7rbHb#wA=2%|5k&j8a!~-+`~HiP z3g)tIgULN;1>W87P!*%R$5d|KnlGT9?&rQ1r4!v%(5r_mw&Pr`rl`m8#g?1##a}0^ zsEA3^_ob|A;aUWdZR@e|~e?B1?ylA8o^n<0u(B+mN$2IcowybPp%&)`;_4+m3 z8vNemabF71eYqg0tT@3UgzPrlkpjsd=B4Lsj4Sd*_{BJvCN+l|0TkxJS;wW%(4K_D z3@sszxaZ;!m-K%*UzzUCGY(&W{Qy`Je;cGS4MkOC7CJ}LBr`F6I1>Y`bzvK@278$T z9Nu)#S!dF(ZXDx`@SY9r!q2w8FjQX99$u`;2AojuB9@9G5mI;lpp85T(3cPi;z&PS zG~S}u6f$;YT>RlS^@HJntKDvuC6Z`wS6+VUaiU#koKsNzip<;g%!I1Ny^WagO%C}~ zx2dOb&rTvf0b%fIwl6$21#RCP_{}6$>R4m_ znq8HReKb=63Vv!M{k?__2LB{c@%&I2pVxV;0gj#u_1HIo|9Sd|;BypdGYjF%Te|y8 z%j9}A5|z2%1=Uc?bOp|*ZlGEeHWT!YP1v+1h0 zLm;ZAN)8z)06Rb(6v&K!M=iQas%BnUXEH{ea8n!aP51F-5f+h!OJ_3%xvX^2Xu_rE ztpq)raXk-$awl_ee4d>hoJGSR6pLNpId5t_t`9V2B`h&N%;I;L9y>Sbo;0UeE#R|5 zqX98^C=P1L?Vx=Ro=y9rrPIR#zh3nJx|L7}=mbB4--SHqH3dDk0gTU-FW1F*Kno#Z zoUAo=;wlRk{-f_b@^b=2hJUDOS0NR&tt-}&qu9Ed2Q z5|Lezu+|=Q`}6GF_^0jxVMO~x9UP%I07q1MLLZ#umky+6z1;ZeX8h@bhzakVg|QMb z#ll{tQPl19I#HI2`SvmH{Quyho7?v%HHU15iT(%tt(IkVxRldg@7Fj9VZsl!r`Wty zk>=xhzpkRIl$6n9LqB7f^U|^obVoWOe0e4@Q8xLG=&@P`4bbmiy@VNymy>S5B54MHs zk=@-+tv*wj!05RPS)^^{C>nb)FS~qDMEbQn*|mpU;>}#Z+%Jg1hkS;5Y`Y>Xg_G3% zKx@_;|KeGK2^&j)qWrm=Zd<)3HhtZWNlofxzx_K>gJ!8}lu}r2J)>eg_zR={Wivqu! zrDWH%AWH%b&!x>LfLK(k009v%tQVfT2`P+weKHtC!fMvWsFc>nT5uKDh>h0AB}q47 zr6Pen^D6HY(-fuCH~yEH%Mbh~4V4LHud2}KJ%HeMz24XR*+1YRNklKz0!o6LeTB)*8>Ibt^5KA`_>1$ z+zcSr_8^X(gQx4z3~b|lRT-CWs;T}0zJi;6pV&4z4g_|j&&%Th6BC=Pg<5i~9a(ma(N-%o! zpc<26=`P4>yF_ zeF2b_;4GO*Fwr?ogiv^X5^KPQvryD^TpwoC=c0+(J|B5w?34@h1iAaDQ2Etfe0F(h zIjKKDAn4QFwBU`hfZ_>9Nz%&L!3yj!*O+GPeMBhz0T{3G7V}yit9;-|{0g3KA%oHR zn3*(Fik=CvDJDlmvPanCjPGN>H_%LC;;5r_k_*1+=kx7|D)k5tqg-lPZ8jIf<-M8o z&{CIsZ#f%`#Hc5+0K<$qC2ji}wRjItXd+7A>PNkS42SSF!*dpT{Ugb;Rz7hTacuPT zi$R-iTq5)JM@nkPb^Kh21|SzNsA^;dtEhrhs<*{v@qGuoo>_7dR%dP$9oHiqdxTwH zxdm`?W)tjQB>V&(gSUvk&qhgI6^7rz{)lZm+;k^|kXE0|Xx+iy}^n{7{-+JmEWRFrj+O0SZxt>1zQcF)@>P|g8<6kc+!FjceYJ2M7 zc>~AfM)LfpXx*c(dw|WPqK%&mz|Mp!gzKLq>?#YWuI?+P0m9Apg{he4BVEdiBvs*o z4sYErt(=PLKiTe+b6aU|)_adTbNOnV63SipGR&zUWVHf^6s}!$e}8cGD=OaKiCNBA z9&NJWl(RP$B)b!0alkkJQ;i}#acR2uceJDA&YXyrjvv~8%S|$A{NWg#{rD@Ch&akt znLF4g(2noA#V@8zT~)_ctvfdDdkAjxAtKs!t+|~f-Ko=$HyU|WO}Ps-kVrtLRT^d` zfF-Nl0*k1-<&EtlrN_FiZkPR$&bo>tKvfB4O?N@3lWn_cVmU>ywuOwm30`jz`}n?< zP6EvL^if&U)zbL`P^P;VAYTcuQbLqJ3rU7K*t^X*xlIkHHPVDd;`zo992JfCf6flC zy#fBp9wJ%PPv>$xeSo0=ACr+&msi5NI06G?gB4&vAA65lqEue|Z7wxwnwpJ_$BX(4 ziy8OvAAgaAS0KNWuQ?Y3b>br!hl%9Fhuoj5wxUe?7REwkr@|r8av>Cz%p@tSI+5#{I zp{@tF-tsI-?{)K--`|bl{gmh3a#($T#a^3t$arA(#3oQh9 z$r6qVx=-eb>Kk{V1ZMAm*FS(Q__W+v2Kcc2o5FNg0rZQA5!y=FpDicxC&AqR`)TP& zHYmxc2_-={;p&m=t9l#)!%K8wj^4_fn%TN7^D_j?4&Lfq;ZJvmnaG>Hoex-J9&Nu1 zAxkGzcEWNj{~L~Yq^jcFA+Jc8;NB>f7Us9V()^mCB-eKdhz3h$2#g3ANs7^8rcS`I z9xA+OX+W@LIzY@+Tm^z}nSj7B?ZB;-;3eb_>cOcBThR}mP3=^YZb7XNu=C-ef7IFV zg)}rFO~?$qUbj~Br27-V|-J2O%+PNWs+3r>ObM*=bJ zcTPd}`laVc=x-pgnTs0MU2pJ+P1wWL>T=-ao{$-BJs@oVxlf$9?sm-qFX*@IjV!eS zUE#%5qbcY$mRrOB69dM*|h?xtwK!|Z<~tp zpJ2}j%Mc{yIJ&EIYGNe&UUa)#wP&jNwmg2v^++^&Dsh;6_JT1>VxvzY`pxTVg-eCu zr`Tl&!yysNl2yC_Me1+cVJd54jCnDQmxB^{gPeP(@9$8Tt~@|hIH%KrbFx_q@Stj@ zz8<%G|FzeeiogDGoBisiUXAVxZm~v$d1EQR+kz^&gNMPo4W^t0Xkdn1`OlJu$@zE0 z3AAk>{WronVYiGnkv!vS!UCD3&htei=-Nr`qfoJL%sXwcEJ*M(Es~oBODodc|xNxkurBnb%v`S^t3BX0Fradiv)=0~qCsPKki@-g)rxO8b#ILl$T;pqfQV%F)$OaI$VgL#iN@LZPlw>vDyQd-;1G4_EUT7- zCSc>vlsu!0(t!1cM9!1FR7lRbnV`JD*AXi})ZXb_BuY{Mq|!*YYD2(;== zlfb@PjAxeu_kwgb`c?8ThZJz@QC;{0bikK^#Iv6YG3OGq56n&muOCu>stvODn+Y5ryM((*m4552?K&-1E`oWyk#&X*8vw zTib7_+mGk$y6`GS04)_Fv#?kk`PLUZa^GzxtYL@mN-*1f`zN+?zwvIqM^P#s^bYD~ z*-~?})Jt9*>#3GMz4CpPH~W)s*cc7S%Yg5DO%w9W2_PZKv?mW?|BBnod*V%I3Llya z`5tHknJNY>e2$bavGb0xX`wT{^z}?XSR2M!UJhY-LZ z8?bQ^{1hKTd-tryibvlX??v-2ItGHJotOSjGwBx(uw?pXq$3eu=9c)o${66acKfO?D^>=Yym zRX%pE0v1wnGT`Y50YnuKX#yg)f0z8i%q|T6__Bw8Q$KMDEsZ_Iq>#v#_X-Z8O6F6n zo|~z1ay#4&W4Mb-lzv2w4f^9odH!_rkPNnBepyO0Jc2j4=Tq`F-*6C(q175%yphe) zjH}xvdcaSkJYr)~h1TrP?Inl7of-zA* z&C6(_e{uh@8aXg#Dc08Mo)(7><6U$NxP7*Li#;v6OHD~BRNfr@A6_qcY*&`C_@Ll# z&Y)vg?*UkOyUYsN#^!7RnLcvq&t*i12JSEJ9TB~WDJ71ReV?Wz&Mw5yvp*17EZEI? zfWUI;U0^xv%WN5o3&Nh3vti3(!6_6XvhfGZ3<-nVff66ns%?2u8LBjD);{L;(lC3u zZP`2H*oO6?k6tNUj@q94eh`pF6~2u)8sxCA_5pc9PFfL%+7pewF)P->0Nl0#8Ia|8 zZbFEQ;egB0-|j|m%$OPxduB%Z?Go&(yIUrdU{zN)Uiu;i?55mRq(YpxZWJ4w{}U3ClLP<1Ec7xj98K?Dbjuu!JiHIl}KyB`Ar4F3xO>d~xF_<|p2 z$1Q?-E?0?~*G@#;HC(+3a61!W51ZQ*fSGQ{qTZpAPLaW#`ERtxj<}>I#Foc5c?2@q zN<3!8f8DiS{;6T*h*c40b84U|bDMEEJIHZ~3mhSAum~K`Yc}z{?LuXK*UVN1)co!? z_)+f-=QM!U=cvD}orHwLNw<0JGPnIm7ik?3~5_C^`$T zrv3*E-)+E%F-A9xkd_b-q&7l8MQI5^LP|hsX*NPakrbs<36VBHVxvo>{L(S$?ymj# z{RQ`&opblO_r34?Jlo4yGxp*0(VGB^8Ct1^JH@(#aA+U^4D44+TC1W2MF?)#VMB}d zj}OGqLe~1n6I!VA?x^~^pw%s#@k4b1ZmFh@tjLg0n_1_YAoF@Swvu3pz-(OB>yYPp zvUq_rS>deOYZ6^Ky~9?P4bIS=)0GM1OD(NRRxL>Yti6X>X<>Q8B^t-)C#&MOHN_&r zlMlS}#6J1ZXFE=+Es5s^=W<{*qVt zu7`r0sLS<#J&Q`8kwO*MHsuvy>^_Bup`WnVqs~#r&5y;`_o-jJP@if6EyPH_@j!+F zo?#y)m?8X}0~5#G7Jl5nb1B=xQYoyZ5y|b$LvW*Odshg+1GINqz;8>(5`u!&Te<$e zDG##Wyf}S(9|oGUk{cTMd_(tn9$V9c7L{$ArINE6nWF=MYJ)vVUVhSAB|4l8G@S25 z_$UmZjEhm##Lrb_egtY*YWro$AinC9 z?;%<81>F2XQ%74L2X$^Jf*C3w6=iwa*?Z2)Mc@uda#C&V5OPftK-W@u`!q~pr<0u9 z_KN8$UjX0yJWpyUVJ?D^@}?CT8VpijfT`7}s^~Y#z9|L(h-X4mF+KS(l=k{_$eZ`* zC#gKL=fF~dmt5^1WKiZyF>{{W^xZF3bVfO=HWvX1SVkaE6atP1Y=$%BL3q^N zywmIx#yWZ#o-K+44?PA`0lhFP?}s;95kI1W#c+@4++PQ9qo4X=2QJ`h1N9cqCd7on zX^<1R?Z4dczZa_)Qrn#n$(xy-zD@Izh*!i<#8#dT>^$&?C>k;CvT-r`g>Y_u`K*Vi z-|Kq-I?M_@oGM`qcRym?{66=aW$NmK-K-3daO_q{wd2zQ;t0EJX)yiax@-H?0My{l z07LL6m?PmGJ-iXT?;|6?Yk#Cv-sj`1)PolweASpRHunqHecE5(L((|Ps9nK$+~YKq z`FZg&gj~kg-Ea%_1V>5u0+!;f!zciWh*cIZz&TEwY=4cq!aSfHEeD?T+Fj9XI?Gf<2Fz0<+FNGL_%c390K^wdR&cYg3EQQyP90`rkuLt?T+-Rj;_y8kzcq(dvEEiz)S2-VG>oEI9I- z56=GRJiH!bN5tHU(rlDyYvTJ%qpz0(2Mi9MydZom`Xz=BSMFr2ruS z;>b^HA}g>_aJ!R1!Z)~zSl(V4B0ML$-kBZjZT)q-uF0?yI%!jv^?J$kWm1UEK{E?< zO6b@s*g6Kl14$qxi70C>n#EalCA=upRxQOHN|i*RN3Apkg^&FV;~PP}^3!oZzzg%L zF?zDppVVAGv7^dAX=QJzJRTRVuwdggi~9ZGPmW9f^L?qfir7b?slCq!QRJ$$z=8Y- zu9ihZ&jD~t>4p}6M>&t&o z=QvZvqvFt3rjCW_++fdz=@K%lhKSD%l^)K%BnVa7fw{|`4U{^_@SnsJ*VEq!?Z5e5 zWVPV)dU{GpaX_x^t&>fruz-8u2l#J7t~hT8Y*fug`lc0&UMH}xdeSePkH~+~_?M4_ zRsKS%(M7VwAy4peX3r znnZ)bVAQ8=YsJ2o{VIWbVpB|nbI9mzxj#34yV>9zWfCZP-V>!R?M1@-CmK$Cenkj@ z+>6ziqy5zAKsU(r$*gWE#QwACj5|CsK(tZ+l~Q!S8(3e5xna)37oPCkc*MzRmh`+s z3Mm7u0P7$`KaBM) zDsq;XV`q`npUZLAf04#xvbt%it9JwVN(y^C8f|=X61HytPm(f zB!e=EQG}7SPv)W9{p8TXcB@=Z>x^5Z-p?@QHK#~W=^9}1vcLJax7MROY>yS}E7*dW z7$T@vLJr>gVs@89HcL@IZED#qK5k~;-RJ7u`mTT-BR*g2&x`?KRua-}bZ9=HCl09@ zO)Uymmqsdf_xUydUeU_|x_hfyqe&@PQWZ~2OUY35sS*R^FIjT~DVTbn^!UW{u6r)Z z-Rw}84V@W!C44E(%s~kR6PGc^A|9x3na}V+{AYhp7>O(9u`Ar40mq=`%d`gkRJcM% z0H3E^z}~{RBllOOtBo|36Y%=udC;%N{Vl&8Zx-xe*sYPD0eX-==nj|x+6=mP@xm~?&IDRpa46qL5V4b!BN~c@Azqp1M187hDrtgI(iCR} z%4!qzCiU3B^c8T#j^nJg^V2%vIS|i#(l%?Ch2B3 zuEBZKU5dF|Ux;!ts*OWCH7QDfE_^&Ak%HeCVbTL~7)T{y=$4RRBk1s5X^UIc{#QQ= zoy%U!h*T6nAs``WRPHIeKpo+hSC>HKP04pf=8hT_-wXdtGpuEldp+>7Iv1*5wz~Xm zbv{QH1~Qt-yt>oyVPuv$QDcxj72-{gAV0Ayzgy#Q4`lYqavzT3@Z~koPD3q$-coC- zq7Oz{K|u)PP>C7hiu>TV{3MCs5TZ{twhXL)n(%R8N;(y|;&-RC&{vx6w2tlGy!} zx1SFSqvA*6N%a94(mir3UXZ};T=x4oOkny8%jyS74wQSR&okV~6#!;`gp8^?+Fm~< zJpxiF0urmpPvt)M&Mb63&)nEG8Ra_(j?Qkp0IRJ?P~#3(b=_EA0V{G%yDR2fH0)3q z^giOO|JOUyv`d%Gw+4xlM;KZN9+IXq6s5tS1J(dN!$``H?W{1Plr8vQdqb|B4^T;p zXiux8lfN|b`&81p8I8V~Tx_x}U-fclXzpVA|6clB&lFlP_leJzkBuv=iJKWg-%Vb< z#1bWg*x&t?GbBk>@>%<@TiW~Q7cc+o`V3z`ZAOLPmhm?g8cr4b3`%Y&_@UKny&oYZ z=o%fUX;0*s;Q%P6A4VRz_nxzX z(!l+>ONJevK;rz9i`}|vOHS7dcgX3T14@1xb;(dRiaW%(=}8W5G68wFhpTi1Tm-4u z*Rd+hVz92tZd-%F$cM1K!ab0STLB+Z?s!Bf63_LrDn%SLk|%^;nh{-&o= z6u2Q0z5^Q09Wa~k>#OKlDUq@n`3YBZ39JA)PHJ{ux+Mg45|i=qL&NT0L!w_NN<8c4 zZG!ThQ~jd;=z7+$&eO=tdfpjg)1}G`JSfNa0Ltn=CQb7}VxDYE^NjlsNLPBFsWaT1 zI-1EkRMh~x&>CyeW%>0yjWoffnL?6hhbF6*UJVl(-BaKveFy7$E(dY%bpW+_i2nZQ z*EC?HyuSND3LofA-Ea(3&^A}BunIU>{h1R&;j)zeJ<*wbnsw~}Vh6Gs1 zz0gcAuf?`T1wu>p*9A{ho=%U1Ed0J+E0PCse9<0<-lmZ}aosa^ zI8O#))06{`HfppGyDGkv$Q{I+*|qRecq&`CWRMWPHZ+wc5?lKpS`XJ3OZ8d$1T1Df z-HcyyLMUC!EE5MHx-tTkk{(|we7Qp$&KDE;-m^p3AH$L$l@IApN97iK?TFzctt4bk zknhYiLU%>SJZu!Oo@zm<_%NUiSh~m>0X>iduX;6K+RA9E(mN+|+CF7I#P=Loiovq2 z!g3n2;ms5Dll~BJ5mACwv`Y25dr=wkAkBkrKRra(~z51A(`WEwOP znz?1wSg!(_>=!DJge6A{-N$>Vr`d1Ak~vyPjQ;jc7}|k;6bPI%4$3Wb&b^~M%{9%V zz4YcvSLEU`e{Gs~M$e0Vo@sh}N$bhS6@!HkgFQ_O+Ux7E?{)~R+_ET)LG_LL)Gupz z>jKsa66EeRm4F_o0b_Q%ZR#s)O-hOXvsc}2^B}NdLdNnEX876{!>$B<#8F5WnEiZu zY`f_8iDKa_WsNRAzO^$PZ245j2Z}JkgA=R0P1YC(u0lyHU(Qkp@(T>CEgOms ze5t}{V09g5lRL1CX>(65<`HEo?;*zX(3>mDzGZ}5?u*_$W`)#hPM?{}wAmTC>bStR8o16Bm2!VCkK6@-f>^YACung!W;wfYJe?BBL|55LZ ztl0YdOs_GS;&;yEwA2xif&W@wtGo*SbaP_Q77#h+xFhGc^4Za}hb>CoF17yeu5EgFp(b*Z5TSNVL z_nvq2^9CTYpT3zayj&F$E}tDMI5geM-~Bfr$^VwS(7jx&f=s9kWOpTY`%Di}@ErPF zQUI$rY}h>PfO{wQ>2Ahz*u>0C163Bn6ySW3;(D6sNHElc-&1NH32300jWQ)qx2X?2 zLJb+tuzE$PUa$(0>s!wx>(2bFwDqNiTf6v&f5)U_8!y6Baf1*-Z&7pqM{Bj z%ydsSzAh&BCXn@o>3amHohpFf8&_#c$hX({0hXrpCK2LWnjJ_kNz7C>(DGcD7NyZn z!7{*Qb2fKUJejK%40K3`2hU{9c;;{*--fsr(h}7{5wxiR7=M!8a%)2#3b%h@`KLcP zcJ9;xY<%4BPLy8k*!poxhrV2d`uf9z?+O03I5^XR3?@gNZk?M?y z{5{sB5Ib>gv@4Z^Kyi%rS>KZZl11$n zOc+ql%z5Bg(V?*6y|zQ95H&UQS*Ib7WN4BF9r)|z2A|s?-A*I_K2N=FEw^cZ6>*ym z612WPJH&|IZ$FRsC(ylP5(SFuw&COTGAwS>~*i14lv)K6qt8|RqfQ(ASh)( z_?XJyqC$eX!k#um=D-u2D)tVZgeAkC0?uX>bD#=y5iA&eCdHGop5 zl#=BOJ(nFMm9&0Qeq93t4%-`An!gM7IQxEI+>qDs4w_GTfvvb;scid`?fFQDbO9T^ zb9e1@Gp`uj}qyF1$AeVaFCAFk02z~ri;VfDV zP6A_L@_eAyr+zrekp9Me?p%bTkx8zj?aWVFuV<3*Z2Mo^n z+R}9mOMMFwHzeo7E&v;6J)Z!+-km2?OE<_m(;-ryNOY0Uv6@w@0dMWD94Iq`co-7W zV@XMx4(d1~09uHJ8aw|xiceoF{wv)L7nB4?CmUl}F)FZg4A%2p_oRBA!2E%&-IJ;3{3;EZyS%suHqrx}0;jwAEqM z!!vAN6wawU(XM4o>M(t8*VEa02v2uEQ;qf8`F^armmFiPPr|=y6 z{soWMKZW{&4x5N@bc{1XZ z$U{WoB)F7Zfk|}y!eID6mQO}PTdDk7GW7ab@^p$+(qZaPZbmJ4 z*TbO~JI@!WhUDC(xozd(vMm!2b{U|bq zEB2&_)Xrz3WoOl&`Z8M z3BQly&vW*Kw2>VD|--=X^|{{o5eOaqU?wzGhhm>tP*e$;YTQV0otK0}^Q# z;12qrc~?+M+|?Ez`RpFl`}#3rjgxy@QOP4T63vEPTY!wb`(_d!zg-$mPhhD!B6HkZ z?t}E~Wtx1?1}>xi2zttWyRQ!*(IR1wV8CuKge{%n{T5l?1~< zs&#i%4Wapt5yR#ETX2FL_RjYe*g^XK+%Vf4|7T+WfUHDAS#OF_hx+0^0;$#5zZAc- znK$qoBvQZ8vmCb{o^|duHaAD2Se}hgy-WmF9(@QHPCClH1?DGv)%#RV)^ul(!$zuw1**hY*!r=UcxKXE@l zBykVECITPcsUbplnEi2cRufG%xQ(ql7>@?#t!euXFN7wu+a14I`r|=Ol(!wW%>U5Y zPjM7Z0L2YKWTZ~lD0w{{{8ca)VjZUAK}Who3L)#DAlMSxHaEY;MW50Sy(i7!4bA*8 zQTC{9tC+k!--v2nnHAu8cX>*tZW7&?mV;2#TQU(0g=&Xko=<#bwO{xiLaB5}jE(g98JF^xfO~-#N|s`U%rtM3zSq|t zc7|^Z-=m_m8`+e5=n=d?+*@!sA)gsZY^&$0WOoexWTH82n6n3RC=Xdl?-lAqioLw>GZBLJ_?1j4Y=81;_ea0werf2*B0Xj z&u%enD>BwidIWE$OtF$S3zL2!QE^`}UJEb!lgZ8u@;llH@6S=b+UTAdUnZ{JNx&X8 zKNShJbb<_m?U-}#V&1nbKz7p$0-A4HBC#iQLf6CYbMfn96xj))fE!DzQ?MiZDEHk- zn&Go$iCcP(Bt~GHhb+zE3D=-i;oKkJb;_^Dn6Jk37H#rgD(5u}VdG#mXvwz2r(r7!{#K1dU!_s)&9td_kJ zP?V9nk2Tu&*@Vy!H(l)V`))6CK?D-1Ri;w>%GOm{gW~xmEkLOEGqJqIP!N9uN zVh)jr1&~StQK6LYX6TZ%DKpmLpTIUJNkt4Ahj;q=R20B6r$Yb53{I-Ax;(y5eXG?s z@wM)B4DZ_?+i!Qi&kXN908EG?j17{@j7J5Yy&< zCZh%~^NAs(k*~h{RtB%97Y_L=Bc|+XH^%}$zglodykaD_f(A0OEo2?m3D}nuH>SS~Ai&+A?lhLPp^awWV_x zvHAGd&z?m=Px5bJ>5;Lynvxz4)iiaq(ddOs0x@Om>S!&l_q;&Sv9R5 ztHe$2^&e8E3pri5zw4qVwUPAE48Bg$=G4DGssV)VFr@pv3O!h;)BA@w%%_*L)-~$d zspfED1u&UX^nnI4Ghxhi$tojm&RN?dpm-js{pBX*Y^$}pSS z???Ii+i>w7~C)&D*;Fhc18j|9NkQ;$$Hk5j<6aqu2xC5&_^ z1NY8WkhZ?xQr35zFsjC`yqB!C}=#FQU^_4B57(2UX~xO zhtSr?5Yk7IA+x=p^L$&z|DcNDv#EUp2)H%Tw&cKhE)bVPui1S1WNd1Iuf<;y;ULfQ5_wG zwM;K-Vq%SBx3dmm@o&lUrsB9K`m@zgS3 zuP(1*`0-=HOmazmMFr6KG{FH9K0i4lcg8gUcGUQ2d$%Y28WL#Ezpq<_;6gJNFL267 z?+IQT!FJAztO80VBTymUHY0M`cYxpsc<#BhqFzn#=K6P`3G1tcb1@xDeVVW5bEi0! zqtWH@1<1PNpXGj+i{EF>7uQZrWald0jc%`rQ7euBq+`xvqSR(SeE7LuUokv0Q|G%f z_x+3b(Gn8O#Zq8N1CkscYT>et@u8Eknf*RE}K69=%4bt4~M?6!P8E0i0}=0 zO=6pV+Su6o4K;*4I~&OhGJGfu0v_Y4$Y=p!Y@L#@@3=@585x&*O#E`%H8L6r=uu7y zi2@3UArP1{kjvTGDT+b+#BD;q&tLI9p#nl&T=O~sIeg@Db1Lqac1O@vp$byAxFSf~ zNOz$T42z2%!S>G_RpEWjgEg?*PQ@KR!_1Agon86{ofGQ6Z6>$QDP3(#DR_%UOZF2z zzo@^44$S4exzO(?-2Rujf~OAcKfVfkSFq!|uo|}(S2X!b066lX zz$-STniIL{5VJ{%q{CUlG`S}@SM_XzxFiJ6Rww$p9$wx%d;MbL8TFDVMI)?)z7J@C zws&}!1n@9QA<5U**$?`@E9FJ4Vrq&SNY~ss;9-zN0Q01R1Zu}?%%0@*Ei#}czLzv| z_i!bW`IO;j49(a`LH7W=aH5TziZv`aUpP1V8VI>+Rf6DytFy5lk zZ?F4&TQT}KV}wa`N4+A=V{fL>ry4b)4qQ2 z0Kqx!H!Y>Ku$L;45z-nK_IjD%SVH@|1t>b^(^&=Yxvy*3EkN}ycMGHqY#mOYtW|=j zL_-sxmL5w}v~9qbQ~D!m|3vlS7G3{#{2$N1Be*>DVXD zt80vurWGipE}G+Og9Ei&^3g}%>SrIfW@_ud1vRRM-gqtXb-bs1T63pgFYG=Nn@FpM zh0_7B#)oNmhf38oczXiYh{4p@YKsBvnDErbJnh%~q7cd6{M!dqo`=P-@A0rX?q@3U za~+!-MEgbik3TNEB$co>iFra`HpZ|Gt$pUf0`h~uN8Atj4b=Qdu2)6e=DCj`-W*uo zoJ3~pNyE9kH!;1;1NX=|7*|P8wr_m4*O!AutRg|VR;Fr8Kt94f`6*!0FYqgtK3a<6 zdj|Om$ofokB1?fJY+u(Etq~`cp8G7bZAEoFhW*Q#!_vAGE#_A5qjgP2LWi)*;vU2D zhTzGtWYC{NuFPcq)O%4plvG-bo50I`GEGxCeq<}(gU~M4Rd#vn3zeG%u6vocgQ`9P zAbAZ^tm7K>Z%WL)?f4C(fq&!ApADzY8HvobUujwnBEf`&h0g0QfBnJXOf&Dv>|U#L zH7~{7zWKNTj(4YI3d(Wd6~;|8_J4xmAN-490MN`Q3s>o6PJ!p*+s~vMYqv!-Lspa? z`@!5d*={|_TUN(zD+Hz5@4~F0yp;4)|TL841V#IemXr~Ss0`sY3~w+>m2ah(s-V51$0F^Vu8RwXyu16L#glB*8ILqq8+rrp6T=Ei*OAP4Y32!66JO#$2BPj z8f{+i5Djn1o9ar4G4u&aaV0U932sb^aiZzo!p?Q0t;R5P7etkVoWNHko+*h@B;yTH-1PaBZez0NQG_4mj0UwE8r zP{i5u<58XeNF}Kg%KB4|IJ~pBhtH!^o;pyGRU{>|iUps30AelE!- zqOtMs$;~dd?1--eoSiWPPtw6GRD`2ifPKM^8fgSFQpdY4>F>GG!*o&U0FV9keW zoS{NYROlY}dnkjlx*Euv3;}9buyxpbJBZ?l*C1tJCfK1RG{YYtWZTZZ`#c>yIv#sl z6u$GZkmLz$>Fk{v#<;r0i8Wuaw$NsW{J-6qrtW9YgjOT!)T?J>JtY#-8;?|OOjzxQ z0i2JyRf*qOhVEDk{!wTu+E!wlsy{pCRDAG6G1+aak(l`UeAV~$%+snD#;8!`#o*YQ zFO=3#OX5^}FqtTro`EkiLRl zUJ?;P75!;Y@Nn7#zxv~V8uVS-SxAeCFEwi< zg|u#OKNv5yG>(6%t1O?Thq^qIHSp#?sgcl!9YYl-cB0G_Hy_T$rG0WY}FK+n6LaW;N zvFySSAD7Vvb?$+;c+hW?5)!fK#;5;K{r4WE_Iq^gnO5Ei(Yu$R9f=B@y>uz!3vF)^ z(joZ@kf&>j;K%KNUL3^Pn#srurk7Bpk7IWuAz|I1K}0=;c2t;6AQtYaS}S3Xawq`L zUwM=#I8IfI5G&M9C!dD$7s+fi*<%ATI3q!i%tY{a80lMbEF(3*VWDsI*-Ev~8@_c~ z3y3q4e9xRy-FNi3Q8@P?tdbp$ckCcMJj$UxH*@opU=l>lUo$%!95N}$><#`k(e?&r zQ-!N05Y&9ys(di-HWd4)Sism4LEcJcS_YOY7h-FUnW^FIm6)ZcXh47N&_ltSWHSCX zEmfRd{>asWFHv7q0>BP|9xe2>oAiH@qq({8vO2McS=;Q~vtgm~XRCJFnioSvH%T>sUW z{;@>FpKFo2zpE9$2P4z(=%K6L1J3wSPHvukrw<6d=QOrv>a2Rm#3+$)p*J>15lo1< zUDY-k2H4tjkfv&7W|A7nynyq?C(#T3e2gY!57L>zP6G199Npl+7s+V^3)zFNwDh<} z5|C^yb&e~m(`usz;NY)QSy#dzW^J<2q}V$#JRAx(&tLOnjqzPCmTBBp3O5!oTn50e zg!@ZE@!N3u;R~MC@HGH?N&)0fcx9zKe>!vLv>-%)Vx^jVrBrird zl$F#pmvbnuivBh=&Ru&{N5Oj{+2e0uymt_d_x-N*&q$wp|B1$^X@rTINk0?zvP~Ht zvT|J!Yo=w1kfz_GwWr_GRAFqx>#%0N5z*C(AV2o{SLP0q`lltlEmoi#gE^q$cSTu* zVVfSw$u!YF8G9O9yW;~UQ?5RTvN*lz&&maz>1upv?({#DqEGD?5Dy#V^@nc$`?~d# z1HHad(|IlG`J1IEO_8ms@UUJb`MCRY%9e7FYphL>Cd4vbp+#|llxM#HfSR;{qW)~x z|8lcCkGa|(g~!V%@46iG1(boWLav9TmIn=oJ@qE5`!~KUvzH=wU>qtD{zpUE!*R~b zHS_bG@tF!fTffr`lO5w8rxE$6HRfp)QIYXX$TUlD6Fh(db@|3|wHW@m zAcQ~8RJAL1^YrwL)d3p+c{Mh6T=_IQ{>f$dlZrgKv(>fv-^X1@1bD3QMQEy@p)@3^g`{7<(7KL232Im7#)U61M_IgF&sJW`6|#yW?u?)az%-ZN;`d;tg}t>0@b(pNXnt6UGTKQp98jKG zq5Az7Kg3sa&c1yf$}hs7BYy^(Ti&(%_n}x$qxqIdVo5+Cu$ zVS+vjGT_(ED|Owriw;|X{b&56`fN$2^r%YSgU~xY^Hr=0a5I92$q}r|M&+t6EBC%_ z1}7le)jM|OXwc9&L&HBP--M{@xfj}XHM{|9gZJi zptyHg&5(pox?F1B>l6Qf^QI@?F8w7M+uzE=eFpgva{Isq`^xwk1IhJywoNQ8T_^j} zn^Su(F0;)Ub{K7LLOyf!6?0rsQT5_2OSL79&Lg-^CToVg;i#g)GP0sdrSD*BY@t-= zjK$j9{n^{0+#XOKtq5y^Yi`wRKYVR&)gr)iHCO;icHJ#nONA-vGoL|3I|M>6vs(pE zI_1M5ELuLr<=emO7#-0N{4y)ddtB%B7XB&@?h;nVp{!U}D7(05?d~3+a4cbSpd~|W zY{%*bggswV|NCH3GBRX=79l$wAJfvdUGG0I;`j}HnVaG(Z>Ws4z)i(ZA90m1!Mxdh zzu@ff7VKHUW$b}J{=-Z1?+TK3y+!?YJO66>oEUg(}|ZaCOxQCTW#%2cX<+Uwf!?{!1d%+6;_8-+rXXWJsi!Od)xjo z{74ML`nd>wh-`~foG#q;`@tBQ~uR7H!FZ?>a$nQ>%3Vc1$}GIW6^<()yIW$ zLR-t@efW%jz|`vIskD%ur1mJ%DRn2zblT;k?}r?v2XrBJncn^vf75<{w=tl`;X3ZfOp&*f^9RtR}+I?jybi&@~lTVv)Z8R!yeW= zu{$yxxWA=-~Lh z82L3#nX;n^%gdA((SDgLLVOTxcSa}3)ne!#)`5@&fc}fWr=e<86K9S((Eb`#g}KY6 zJ6XQi5R=zS1P!4OE3{Fs{Iu*u?vxQCUc+l{A%xW;Ns5Xoep zlokw1kh}$r%@<(e>vmxf`Xfygl}rD4*m-2tOCyZ*Dygpp3!1%HQ4_XKfMGuX1HD6w zIUT<}fHQxpmVZF?Lc);#z}*7*)Kv7#eFI;<^PlEUZaD&5pKGlbDn5{Xyl-P{IlzJJ zDk+LWUA9Yv><;SYh|}G(YoBr4TBrPSA@Hu=)N#EkczYhmWy0 zh4YNH59Z?AGq?^(IjYUDIwV#uSKJ+3ed?~0N2G08{9^g`E=S_;RQAadFDHe1q{eV+ zBo6=g`|(^S&2m%U`K^c-`;PHIE6JY&>$mL}mW<%EIg6 zviRpee~)V%%dA%yzIAc<;nO?e-TmRK|I=^l>Gr+J!79tXdFRQOwcG2zw$__KF3QWk ze~yFtkAwae1o={=Hj~Svwn$R5A964c!`^a><;dNfQ@l-1hoD6X0ev_r9)|Py$8!QL zVRZzE?fdHh0AM&{AhJ8xP0RXFYFo#)9_;KKn0D^e#jWk`#*Ku~g`8nH=tfk9`ncb- zwk!-Ce*R2tuUuwKZG1>_4V)iuWIEc(kc_VQ~&w| zYA*CsZHZpI4Z0A{J*B6^p{8fAuIr*TYAsuH|C%-cNl+3lTUQtSI&}5)O}rdDh82C+ zc1F^loCn(zS{b?aW|emU^qvL|dGDQg{&C;#oLh{0yE4GQ-}ZEZGmz+10k@7UT$b)> zrMP8Fi*Y_{O703{t1mLXPeOk?b?3px4JkchJR|*GUhA$UY^?{6-uy(BNo4wg+HozR z+X=#^O!MJ8x-T;7Gs-#ixnkdNdTV*=VJJ%SOm6R|lAB7eh!l(XJbEqc`Jv=vCN+?MK^v6m-1YUETO-`w|0IL^f2F~?6lSuY20iC`b3@j$r=Izcnh>)d=xVG)@G+rV^d)l zM24nyhWM%>5si*jF)pqwfwIA~-XcllAaE-4IVo(lD#Z*com=~-xFMGuR(>=va-p|m z^#>jIarMNIhGUH8rY@L78C%n)u|B@L2`nhzQ2&Wt%A@dfq6#OW27g$V?sa^4*K(Jz z8S;(xq~(=|%;IOi*G+TDJFiubBrJb<6foJ^6rsxh41R(U4ep4UtfYD_ z=0MwHH2E7{WMg&pz|mLy7C7PH^QjgpO%}rktne-Q`uTfe-VWjQ)3|_+@;A9LE?=mf z4@>+OZ>lVIa7#ztW4>pJLYk zB?vTK6bpb?y0liWFV45~JGdfJ&lyKs$>QMC>3zZ5HC`h1O;Pt@oK74}c9sn8DyUxccLYA#Vav_~4B{IT6L-sp?#q;( z6{bQ)R3!C@3Xh3L8Rs8wPU2o|PW&8gJYD*)|7zh3$33pZYw{6hO*fd`-FE}BfLNkA zPT1wBlLqG(Gj%z`9}=*!{AYTK3i`tMb*b?+lfYxsTG$&AZE!seV5q>$MOxWf2J*G|KE%ysfYB->s)JD1Bv> z08;3{U4Fg)EPv-Tl&{n+`zGOOxyuoM8Ks%%-=q)z;oQc3I~Uul_6u1LSvvOE3CMm| zvYc`x9n)7xwjWm3%gDmhw?0C)!}N`gRWRRzo6<*z3qtpHdf?$sNhS5(f(-KS`)t8L z+_-G7t@#k86h-CPM}Cz3RRUB#1QLb4!}jJaxrQqG0gWbtSpd>ZJreP5TxuN3hpuiV zQWG6D!|QG_+E@k9;&?+I%X3OIss#lyWm)8!>&pZDsOq|vBkfL7M$iMkr~5KX%ha(? z>Am78xl}*crGJ&zeIO?`V7cbheOPe+nKqv1Kj^nJ$s;iiNuX;>aCXO+d*h^W_6&HG zOB%kyq9z%1k7>;@w#k)^<=O^_{pn{|B02kjK8pl8KN>uTV}622p{_Qlo!4S#eiaTH zNF#De2av@8V@@5qF~x=-*bVW%o=!vMlY9x=DdUPQCi@7DW*+0(&aUqdN*?CSM|M0%&I_J5b=f3lO zwKWpYlWcC4AV1Drd#$&hsoV#)sMs+nA~u%aR%S*JYw4Ry=7GWGDPzt5gv z;_A;4Gmbhs-FhL?PtDA&<_+d_-=hH*ZD61wY8gXNH1<){C5O%nZw-z& z)TX7R9T~p+sK%v)ZMl0PC9U#HMTJ>A>BQXYbA;LV^?uHR;JOHQQqow4IvzOscHM2R z0a5$JWI3bb<66KYy#u~cF0z(c^B)`?q2TJFta&-2`D)8*0VW{{7$LfA=FN(|F~2{2 z8_mNt{f*l!wU5YNQ(+VUf)jAjLIga;57E|2zutc@>DTRP?;olLtaxJm{wR9ko0OXru=%h{b1_Tp1bI)t0t@l1F?HmxsyCh zuPG$|@`&(N>~cGK?ai;?kGtIrwkq^QWZ5?SZ!vDCU$Oa1MTz%yIr@{P=#pO(W+ORE zcix>LNu|V4-;+#*;z|M^es8C!EralJj+4)K8~)M0LXt*1z4cg~Rk|udc{zTvx>grK zoG9+q|Jtt(qY35I2-RkZw?^1NtXE0)m$PNA7Q#pvp_0GLRKA%yM)ycw``d=jjyOO0 z_N?C1)!X}=jk>z+K-}3Av(e1t0ktF8o`a`O_gy-P5%Y-v?Gmx>`r=~z{&M_2k$5 zqH&RAz5I0(%>x{i1pC*Kbv!(E^BOq83GH7<0Df%Y^2Pv|1k;qS#|=oK7ISK(wJsue z{VaK=%7L#>`2LheOttPHX~PZ|ciCY3#&1IF?t&kzkdR-AIy1X`Ynl5anyTywDu6Lc zwE9)IRH?v_2`OBKJRlM^4%Ud*q}+nU;mOJlR(}C4mlG*`Eah!RZL|AjRs}D+=p#bh z2y3ag)qzUUUov!BD>MdJs$Ex$qm`qeC#ZkddwQhf7Vl1uE{vzk=7%Y$QrvHWMAQk> zSD)Yphllx()zef2bnOa=Y#?8v2o*31;9Kr-W>2N~Ep^@-rQeC>#E{N9^on)qk{psh zLZq`JYXJC5eVl6_>3+PrI0K_j=T=1a_!a9~Qntzr6bCDgai@!1`w?S{n_?v*9KKq; zuKdPt-o?%b9=YV|w3-g>3$Y(aC<0O4a&Qf%1GP16M9DzhMH(VX|4=cC5shy^DIc6< zN04W_9_RS3fzA(q`KgPcND!NZf6qd|wRN$d>X(Deiqp{g_FLNgJ+{LNOw&xv?bs1e zayq;n_x&8viCn&K)@qEth$iz+l@DLYUA`%bG)I%g`s_3ck3G?a6m|E2y1JMbM*gGB zIUO&Rx3wKm!}zGF*|xE;yEpk6+LGjkMDh!>p?|w^aSyri@YT6z01IA`fJAt0c_?A} zQ(joK<{uHq*wv&z+!sfblJc1?){8}W zfEEFDasdI=^T-EM3W^H%6)Y^wJdROpXa#yg4QXI}xvrX|NS9-dIq{qF_iW`FbcMw& zUw*WfgEEG%m(o5)DjKcF)o0JQ>AKk=sjqoI!YIiL%K?tPU@-b|Pf!!0srH2GVe)+y z2I|P2ZQE>^dm1I-WQ&`Iu|TA2g7W?3Oc-0-;q~Hw{Q-(b#M;o$8?@gB1d%2|+DzD$ zMIPPOJ*ew%RcrBjlBjS6IhO9Heddk<5*f_ftbDJWc8WPoEox4%v!dTjtDI&B)n%5^ zeziP3i^pzr%}wi?^1ig&_5ZqCy8{Gk^<=))zpL8>j-j-GS*Sn*X##!0f7qpB{%e0$ zWUp+a9TpDT??an@7gKx8iCq40Je2pD3bm_Dhn;g;M>&0Vb&BEvES*CP{B&Fp(C}42 zQ#%n9{yED2G87xg!rjAUakZxbWKMdtZ!b$m6Iy!3htXfux?#V{* zWD+J;sj{idHXSxMO8|r*syl^P4STj;L??U2V`lmde;0i2F6wst;>1nN3}Te?Eo-Xx zOVvd;zqbiKaqKLgOzLIfmC0hC^E=t`ajv`y3-;Q1=SJ9GCrnS@9^}uQq6TCu1EeW} zMzlJAFoX}7TxY}Ed4`I?NGgN_PnZ$f+2rVfCUr~rXG9*VQVes8*qA8?dI@9aQhX}? z)Oe(3-3gGZa;JPb$G~5{K*Wms49G^q-n<$ip0?=!BK|Y9U=t^TJk4vN%3ague5~5a z0IPVfhzGdQ=~#VYgW2yU3Hs}S)cbu+cog}NUlRIux#7#ozFd4ptnXnv{vrIDT4@ys1 z=uz84iV>q(w?59ol^UU=`gtp`y8N4x_^eAs2~A}*rpH~5PPg-})CG_@)tUwz9x|V4 zG9%_K|0v&VGk|b1cSERH6grh%V6IN+)Dd@JJ&yP9;8l{u6J$ZLmy&I*Tx3ZpeKFbW zTB($okv*fAE}1`*DHPV9R|Ig8_T|4M@Zlqh_!LPP&ri)qi7XIqk!OowcviB$80$V; zYsf~a<~kPocs0C?+VRsk6T9G5FRTaNE<*7)Ljf$1K&fZ|aP{fVfAq~&%Ch`&W01m} zhr8kG(U2e4yllzDB%f42%roE6?@=V&spz@xAw2rR7O(sVzu4$Zd(?{SBS+KY8q1N8 zE|m|ZRMhrg*26Z9>ZRVvLq6|Mr^=e-$(xzY4ogZ8>sTt8Df)WI5jGo!lnMApv}om^ zqp$gdbxq!mZV}GE5T>b!wdCr{Jz>^eed_f{C3&WWd~YP@@NyyO zEwKUAY5f(Soc29Tk}7Cdqjaz zle0+vCER1i2`jU;edI68>HPbwv6pB;oi}=G_cq=Oc6WD06YJmOPefB1DVYuq{S#C; zo{d?Nnf;`@)FdII>e=Cj8Q{RV0E{Y+_n1;Y8}Mfxxe z;~qE7Os*HhQ(%x(_SrD^d88dk3-M5eC>Xl{dG3LpLyASRl;Q z!yTKT!{|cv2C=tetGwq!Fh1E8@gjgqop8-HcDBL2MZ!oCj1k-znt+^l*a2i_|6LPI z`ojA5Xe1Kip)uP02v-4ZzJ~9%8=ni`9app@%pf}nzGSf&D1+F*lWvwM0XCiAL;46c z+a|CcC?_A{=*(eHM;W)X%w1hW~QO0X8iFKJ}cS8%4b~^lBB029>Q6 z`ys029;mpeQYv6oz(IaZeiv(0nn^a7Wn@hIz;9?vWzo$n<&ELVv3m?T=si$oJ>zUQ zMeNwW6+}52v7j7{ zU3A*)pj2QY1s81pxaC<@QtpI-(QwG|N=BL;t{QriToT7KeLuNH#VWB9=?ycdRv5wf zI~aiM0M(13n+n}!6)srdSb!FMw|p$O;cdb*n7)xW{DHEEnw09BShDk_Do2XeG`E5E zOAts)poEjr#OTMVtlx>=BmL3Fzex~kBIGsNs{RC8qFjiL=p$ADp0>vp{LU}Cvv!5T zuRbD2T|YLBNZl!koE^uv$>Th3hdht54Dy<#6~KLNdrhpl-|bXfL=_hQq(IdOUV^_7 zD_vy@77Z99o>YX}ssrT`p%^>VcN62NC!e?{?Ln>8aFs z=;2kjs-Xu12x-Dh-br;m?3*B)aphk5ebnCVzBB|>Fc8~je=DSURs40W3JvcG!RrX@ zGPY_}*VzUmr?R}@pnB}F_#->yzsr*%Soj0WgHm0MDVMe`gsVJ{$T%6}h0jxywj#g3 z&B1iDOvDO)iyI8-xKOSei0*g4R6D(ns@u2aq$d<)Oka|Pk~pHe9c%BN zC{DUPfv`S~V4}2g3i?MOOBkAO!Oet!LVL9wn6h~C7H|ru z-TjJ#yT5jVE~E=_aXT5Lkudfh^sC&h6bRpM*Gl&+qA&0gcd^wDY3{>yP+f8f z5SHq^@q+IvAKaJ3Q}`*?=C>A>6RyC17FN-iDEYeW`N>HI=*#_R;V*&I;GRasaHMLv zPiFYqTg#Td3!2rdLXn>#H&4jsj^@xkQniN8Y3aW2&fPW1ovc<-#Y3tgwOg2?s9jl2 zY9J|^Smo+n20glN@8K==w6_A2!B$4^JcP(u?Cw+ww1LBN;(;z=(tH0gh9PJ+Zw;}B zX}FDnRkG$p!A(Dn9*fK+s8Dp=E>s8L$uZCGDaU=q!8!FzW{wJ-VD$UwtNu34s&gol zdkrM(QsowDDa{-}fm{ktfy(uFdJuAOw2OLgoA}s5oN{s)aIx2O;&fWsQVbc(w}!)? zJvMqHgINIkr}`~U`KXa{pp%j<#lI~HjpHmDpm(_p{j#T>d##t`fb#b9<`v6etNXSc zXB@!f?%GUXk%sGG5$lIXF$TwroBfW+727POTM-{whGEWo@_U@hULeFidpgRg?Md{& zRV$9tQeUV4JY(cwQMdyWVLDwcf*nRowaYC2Ab&D`w0EZAu=N|-lyPwAp(GXja+<9i zKb;?PeWBcPdPHb4();bcyE1n1i7tjJDP79C<}8|T%*g7wV)VlW%2;Ui7M7cWp3|@M#r$zfyc&5spwnMjJUX#88H59Xf^jC-mjA-w-XHtx$uh2o zst>;YkQJFG^^QzMebZVRnlJ)Yu)ui%1!Z=kYhoW|-UvCDHqe6T3uDFs3K>1!N789s zwYomHpCUTcFZ#>ZMp-$wSwI0S>x|TL1@j z4E$n>22N=PGItq`?QXvaZFxmKzs}bAWovr zznHujuZfj?hudP-6@dz5?Z-i~u6hjj->*GD(o^RfjPkEL+|!sAL>GkX5xrf4v6H@_ zO8*X$>m!kBV}imflIZjj#KnL}@W&_tvcSh~?l)l*Q&y11F|91#-?V`nD3P=si~r6srBj{7#AOwPP* zKKP}K|Cir<^?k?{zZLh*a6Z^?t#|#9jSod@5uc~J%?Lw+xMdFcqOL#}l#s1|-0mQ@ z&da#-caA^>c!3>&tJ{&MsRF z{^I|=<%f}w7y@wzRvJJq!dloMj^pr_eFG(!AZ9eP_S^f1RumGU7d=n;N7qA?yK zt`KX7eXo#Y-olU&7*5)?ZYZw&W`*Fl*E&SLbN2O)(th*hAdg^zXGNyK7x3mAQyEA~ zM@c|6uYWSJxbGFqD*q#1OMW&cXvI;IvqO~nTLeLrFG_Zh3=u%B|W9I&y%Iz zBaESQgj|i5e;17ols5SFSn#)8g?|-;!%cLNY!@Qzwz!tdzKD?t8bf zHTlI4q;N;VH#il&f|n1OPOa8lNvD;Y1DCkD37ps`OUc^-*X{L?cu&E{n5g$wRF-x(6zA&YZQrkw|A~IrIZ_v8dMX(n}Sn=x4u#?HKM;1W6rL!;i zB;vFs%iv;7oDM(l0CmrrK2t0-pI-kif(VC(HBRC;3%NSFdRUiIUV93LCKPxuMhL6nW!n?_@Gt8IVuhn=cjOyG)zM< z|Kc(>|4(!QEE)$5ZsTv>Tk;AOyNneHdv$^98FVlWm`?8mK`sf`R)iHbv05(IovwW}MWQGW>ABDLyGkzN1;6ER~FWFoqGkEW8LhGEERQeJQ1XOC>pls}qg^ z<3P{V@SVtgJT-1lscyZuIFs%2mf8y4qtOtuH=f2l$AV;o>wRzJ0hw4LoXSey2=xt5 zJn2Jqab4c)FaxD&Jm@t5v$U+?dLX%W5;{a5CPg@$#8~Qz1HY3ZxM;8r^5_LXz%L!JlvvsmApMiPhw}&Ub((ZTOItWEhGT~_)b-3wxDWh-|6{^&@e=4 zXVv|DJQDbulJ(7o`h>NPXP`Rxd~}=P+V@CLJ5(!@g*D1v%$LhcUTkQAG>+*7fi-Z- zjX7{;yV>)$W-`(BKa*&NLQOuB7q|sUk8oq4iV~Rd3#Gvev>n{;{KqUiH;~Hq^{`k8 z*4x4hV?^>^$DD#bC}pmZmWnVxnOcjF+-SbSP`TcwjCtJVklghN*zP2AU$W`e z$y5P_VkrF=+ll(ZtwN4&{y>^vW8tdK!?}j@@jUZMUUdPB8v|re664cZsoM0UyvDA5 z+@E%xkV$^NK6BzPn~yE!{L{I9c>J3UJH0rQ-6!MdVEP-hI`BpLayH+pK?xm|Ux4~` z0&D%^Bt`1@B9$UI$r5|(mS;E!5CAIH)u5desO>Kd&~C}!Vw6#(HqjJnJCBITb!j!Z zQAa7Z16M0&pEBZko+XydGcKJyGq2em18qmx7>d>svnuv+U;TVGzT5)=kyx}+RA{_v zX=rNs2J=<=qlkbSg=VhP-A6cuo(Sa-&l7eW{sT`)%B#04*Nw{{6y z_}{f#uO74!7sgO0Cy#u1O;%p&b7=bD3gd(f&^bzP&5ix^@wW(XRIi+g$#A0n#)=p} z`B{1@H#1fm?uF%{!0dB3R3~2(6uT0!nPOK z8JA76c!#RMc==o@*SMDKr2V5_NoJWWaOdS{+^qt*fvTW(Ixr{-ZaG%wRDMg3RNHs3 z{ERbSl^wp}T4}U5t4B>d8>AzNAG!!^36;`@9hiw2>S3629AwG~EhcqTaRDHZY2Qc> z*8&tr5e6`~={}}DYU0El6~`Z@c3{8wzs^(p`?Iug-r+?FH|m;3U!$?Jnl0)C_XY5C-Q@Ln#58-7_ilu#=hILvHd6Gg%e z#WRH;#^YCyJ-xm|7x#&!^+7o^vgHm?(FiK7=AEE1qAR!OB+V1j@MXibNlbsodekU*EZ?#yD%(_5ZK!HJ=#D*(V#*5Auf|5@QqUi;~1wxIO z-Jhmx(mj^9s6ai{P$e5+X^7!tXYj3*S#_!50W@_?qL7SIXi=T zvG<-Qgi1K{QNe8L-$RP;F?djyvRzgGP{&CInuCZfh_+cc7>nX1uU?OH2+B{CAcM!7=3 zBSwU!z%p}f|CKu+qMR3MG0KwV#6SAZBPZYqw^m5fd&;ns{bYZU=AUc8hNep(VPaC2 zaPwW9t88@>ZIH=vDf;cozX7L$^=dwt{8&HFiG&TYD~vsx9`_`b+n@O%ouQ7+HKcnO z=Yu#3S#0l;9^S9P@67)_Poui1wcOv`{k~WB>?}$7@_IP#+O`_6LY#ABKbqRo()%x> z=rjl&a4QN}0a;Yn8eWv&YYJG6)~IXUgwH~dAsBHB^g}S}RZIZGa%4K}mHBY{@w^w4 z(C|Ooy1y+Pp-d(gz4Pg*MIp;oE_!jRQGJ6F(>m}qf~EGfhq zLYk$XX_4%%%EJK2xFmZ~AWfs!)#eBCzXR5}4Xrb_p?vIZgi$=Qx5u^)> z`7+eczpKy#{ng+BLA-Yn)N%2kMNDo4Hm9WfUwb?ejs&Q~ld$U?#7?z@HG+nac+L$OUCLH!V zRcI`jEbHHj8EOIhP&wCXIfLc;7gcT1C5EJz>iG)BtP&aH4lQ}O1&I*T&>Lp53cEU? zVhXKuPm?3G4{(Ln0kRu8iV;>RCvRXT96L&Z;R20czwWdf*hxbn4OE7Y z-P-i7mU3S9%Xz^vf4}C2T&+GcX0w`aUcUI@W_2}vrhH?#EB$*)1CJVq>}-a`C!gsR zAV%MaRv2hK(El9q2nsT~+<}-n5ux$1MEgwk&S0La2h0ydEZu2 zP42Jp)WGjGO-;@hU5Y`z9tH79AyiiX+|7uwc&Cw@-ofsAYAT@ZGuPY49&EXaPT9PV zB-!6=xVkY`%!ZvjPfs8@<36dVUH3GtN{h&(V_@fDKqID{L=7kfv4iZaDzEgTMovWa zC#|xrl`M=9cj4@q79Ub}z;iF10iJ&SnU%E%6bm+@a!}%?i|YR^upo_57o!gG(kuTWTffRZbN>z!R>$AGs@N5R#LpaO38{dFV^SM)W)`+_6K0l$5Vs7 z(L~~;^vR+g`^NJWb^GEi`J5IMsp-f#>VF^*Mal5M%Shiw{}NI@A*-cRUS(<~Grk4+ zzXy44-edkPz$<)*UJ-DVejm@?{`qS-3K{N+#O(Iof(x zMrN_UDPP)F5VqdJy;Q#GDnH}9_Ujed%ej+}#`NUEUYdH0vcXf6;}Y1!P>+V|6%|#$ z?yjszlhFk1@3Z;Sl_bj$!uSs~aC5kIS&6Ir3q<{#xqID9%3e@aPU#V37!*rG^+Qqt z?(OzNec`8c)Hyk1nelyxuWsnZpyIK*J_JSTXhK@ z4g+I)5SUBAhnjCSuS){_ajylj3+@aLbCHKxVZy7}HN$v;$hR@fLxCD$Ym)DS=9M<# z{S#krjPEMw%o}ws`$CKmMT9<66c7Uz!Eb+Wz5PV&datZ3N)t|xZ9FCEh4}g}u*Q&l z+-tqPOjP6Z)Q=RsoSRNht$u3pPiBYYnpR7?O=$&@~43c1I2#gXiZ^?NbFAKjk6Bs z3nCHYlClrKS}M1m1Q9Oo@=;jtV#DNO?x`P=?;L9MKpr@|M=$;3o|^m4jF$cHG1mN> z)T6AtkMXh_SJ?sW-2A+(Xt&DS+{wN4J{y^+yPs-!x0t6|DUkTZG6Kn^By&17|6veW zSju{FV*br}d7Hsv;_i!0){rQRmHlb=+o^tf71ySfpN5pK%()6HlyxSbML$K|Yp3J` zlq4ST4_JCujLeZ{C=!B$9@0wi4p;LSMi`n8pNieHBJq2ls~U-oVxTm$+vkd>F+|^% zuWb%IW9%YGpHjufT%{daoiDQDQTK^Nk#ub&l)QgN-oXL<s?Y6A(lHBfhi@W%1`?rM7O)#U4YNEA*x-zNQ~~Wz3xunKtXPEA;0qHd6pys~0a#QH@Hq~+022Veo!oKqb)~b-cyPD|0t0-2ds_?ykz%Q$M ziATmmkd*!-kn!_i%+$$~=U>}3nx^Wjx&In#Po!f<3YJJ*-!MN=znam~NEinV8x4u~ zfxr<{hL)Vo*lANFwz;G&Ey3B%IvjB@Wo~g*aQ1#M_kbWB%m#Ki4Z8DpM}9FtC^ksT zo&yqE*RDzZlhClXcNG4@bd+I*#g?RlNUqSRRsF3^2d(g0_!7YBaR<3ZX>&^D+;}fM z4!CTF$%Edf^a&o8_FTmQ7PAMGxS16)3DzW0RhZPXgVkqyc2BwQP?eUIXX*Hg?yF4$ z#kp*G35@IMJa3Gi?9(u%k}O?*r5bvS(!+K*>5944xe)y=U4;;N@*uE~WTFahpcBwc zN`=LeT6A#qP`!_a$UPemIsVl~ra466hazkvg%}y!H8mWWqY#@C zGYxsZ?A5;y^nfisDefO-%A2Q96CKD)V=a}UknW=DnU%J8)zbVNC0Vn1KZs`Yl4CCB zZGbxE0o514e^LV0tycA{Z~%uuhi9Whq7-5&H%Jy9NSZr%WUneDH=Jx7e&}smn;q^e z8C2OITaTt8{zQTrF2b$}rU!nGdGjS2rm1tchl%UxS%~xW_!s3MvM2k8 zN}CZdmy73Z%;}KeWh@8*%&C04kwF^3^feOEKhp@e7^C<7X!`Pxar1Q`Ik;e*7?;P$ zgow9NDIDj3I7A$YI$u@%ps!#6Q-2K0x+YPVD|4g~#A1^h${r*6;aLcHog(unu^nJU z&a`XpcY2H;ldqXR7nbJ#a{1zX+U)bio$~6#i<^ilR0g)!b2#E!FOx8S_?-0OEw4Y+-X^GZkWpov;kZ3Wu{*L8}XsZg4EXfawJ5F2a?2`pZ!XLGt2DfL}ZoV|2{b0SW0=Z>|!N!w@^e z$ZfFHPe%PmE}(a}uHP3k{N9lWm+OuPOdbvdw~0&sL3kBCna=r2rB|)=td*vuMNts-*hYdD}w6qeIp+~Mt(ebHfjE*m@iQ*iEih$N-T2j z<=o2$b8{IK9IH=G?_7>}xi$X?BS>r$64vMI0+;U#p%!L_fuDfZd5qX|%Q3a6tE5;6 zW?y;Q$W~9^8FS4Vs?O@B_|Iwlq^bu^s z;c5>a#vaJ`B|pNXQ~BI%s+BDMbazs3ggw)$6uTCNvG;Ikj!XPe5fsGI%%a(YHK_IP zW-^O_tSC8OzBH|SJ}0T1U4BO&Noc!>RY;Z=^;EUCHjz3q+jxVT|E-kq#l9%2vOq9P zSPd95yU&(f`;{-8E9w`8pB6_fh_h(%?klp{NS0d?j3ce6{Po{7J$G}0<;*GLM_bI5 ze}uM!c2S4qw8K{WG#yZ3C}*Y7U3$9OqM)O9>U%d#b%M;VYz8xP<1FE3{#m&ODsXYdH~X^Rf?Efl@ziMPTh@@R^{M) zV;*yiUk4&r;>qhvRj+5Rdy{A3bNkNWptz1pQ>vxD$LKv(2ln*h`BKeLH1gZzjE+)tMq zYg=0IL>hWL?rUoU1h~V#nkT8lAOK?}3ikTc*tpquFG?15WgMsZXz`|+f&IKVE$2Zt zVd3Wy=F^??X|FQFGURgH=gEBF<&b4S5LW|I6L~Fgu8mt2$YlCMun}zoYQkv9f&5`8 z2{Si;>T9Dfl*Uu@1c_k!NXY@3X+*lLq=ju5#R7uA*-ZYSss#|Ks$>dFC zd8sV_&1{0>DSeGHC^K#3tC!Q$9#aYNNLgsh4Ya(>8Jtn&!tfTMLamA1Y3qvyaPgb3-xBQAdm&_B5ki~mp>x%w$YD?)k*1i%Uxvz_Kl!MGh+%Iu7zq(%eM{t1cfFKL0o*++ZOX34x$-tMj3A>-zody!aCS>g}G3- zDVBk=21q^z!^tWJh6!VqlI}_UEK|w}WEIQnEeV9KCWib_TyJfw`mP>ypW5W^Qa722 zCqwoiGe3l6NBK34m-_9iqc+vYJY5q+NCmlmn}Q}#LmU)sgBGV9K2<(QToFhB`G0N{ z&6N;lc(F1_ypMRc>CKE;Y#H%qrKD|^rz!QIfm-ek?E6CGd4DpItVpl>-x{O&kt=6; z`H0=@&*KN?Bt)OX_Vw9VgOs_r!Q&%7iHcbK&%QJcfnlTfYR+<%TYR<-pO+EH z=)%i8K*1r4YtJn$*EVCg{2v=qm$80Te#i-Mm(=B_o}So3P#(Xo*c^*)0*whTW2376 zFP8qUbL)ijiJ@;T^{#}5tH#OZnv;&!9Jx`Qm;H;)i6nJ$oqh?-C$(pPxo1AfVs|)N zxGwKYS)q`Ar+g`)OLz@*m+y1n4$Ol6Lq?XJ3JEChOEv=rX{san;{NlJ?OddLBqk?z zX19I>mxvQ8bMZd6a1wqd`;)_HWrWy4?w*=#WB=wM;~IHC)-RD(I2Q&Zne46fuUIJr*>poQ`a9<~D$G zV$N|pmUh1{OV_v$vgN~N3=~(O><$iA&-`TNThCF_uEI&6ZUpbZZ{e6|$9`mXTOvY= zco95eU|NT-|KY%pr00XaeD#s#M(XoxEVJdqpH>!^sE-A{4Pf1wWUHemFbf+ozu>J8 zoup`9^u(iIsiY57I3?yH08|+MRM!rfsLRK29h*>X+N{x@8MsJt?bUYP_L+f@ew|d> z&xDB=piI^$peXn}lJ9ZPW<-M=Ye=ppInMhgTYlZY;=E$ixj4-XZA${^XB5+U(7LLz z4zUSQj3`#3O^K-Di;_cUsaaAgjUW>(ty=NhWu5JhuF?~RlY|M^6#M?CHrXO z{5QqG#JY;L|F)sYUoT$i6Ho{~TJdm== z;ErAGSBGYbdbOOKOwNx4lHFJwva#CU!j7Lme(pB0P^CuBliuCy7~_bAnyk5*;U>nv zpdH;H5T&!x(UV+4AlQe!>LLhxy^B{vfe?YDQIfBs=x8D2uu9G3GR{RRW{@xJGd=Oe z`hK#TfKW6u`a8)!2{mYk<1Ov5`ag_QRp!g8$h*yTQ@Jf4+4ZD)qEI{xP1p?Qj~rLe zLjHUCy0LF$H9UOw8ItUqMYIJ-6-ymg3Q~fdccbWB2w!VMPbZx3Od|`=&1+Npm<&dR zn|SXf$Dy-Y&O^*1=M?J;UX#YaeHbP1{c~0Wd(tmaAPpuMl8E&D?}P}VM^L?QU*%JX zu25RG1<_a?xeT)MBle;r=|7^_>~1wE0SQ3-6Bbp{0J{O=3phlL%y&ogW?7rR%`lKF zYO>ddQy$Ny3+wAqK9UZ)M{u2q%Jd9zdFrt@xlOY999(1>UR|HE!zy{$D|e7kNAn>v zAc{&K1)}eyJg2zMx=ip+ii={QaNjB5*J`A^k6{V@yyHL$vWFA@K3YmEqI6O%u5mNa z$6GBwFJ0xrEvQiZC!)L6Ra38045A2pLA*(eH$p`RQVxpSrT_FvWutnDJOKts?m$y% z+(~U^U9x4J$LWmV_3dis^K>ka@v@Z*WGEwT`hW~;<)Y1S9nN4e9x+DZo7g{QCX9aj z`Ju-<`QE77H>7qcAqB3egQ-T7{By{^uriH&gXX5T6hkao%JK69Y>1-x!NT?{luaL% z{ZAh#;7iRv z336fpa@(NG!HTnF*D;$J|04J^v+38Rf9v0_m(9zswlDVtKeg6Wf3;Z*PkQ<;h-%U$ znhXkyH3?l*u)R*H6dU4Xl)8TtcS zhH>27T?1fXDlBSF0^eS3_zdxgJm`4#2ocNt4vidzc2G@yEoRudAuJ3#$H4bVZ=LRv z59plQN5dSw7LqzdD-6M@N z0zls0#}XZrO{ibJ^9>+1iDGDQiOdU#AscU8^5un;MNM@&=Y9LVLO0iV&|GRLr?1ki z9erZ=xt*qTPLs-8K>I}4mfnukdeA%3CF-ZJEg_x^YxBAjCLT8RL<}4U5<4=Vx|-}x zvOTi0Y6)H=AL?%zX0xc1xE?nUYujb7SK{$AG=#3*-bubJ**vKsXRnVuUOO2rE$x%m z+qLSZe9gD;NhgN1A&IcD=@8}^6d5#AP0Y+E8b8?yI+($8O7rrjy^qOdA>1oY{w zdn)XC+JG{ec#SB{n?C9H?_tUkPUNjLHF|1O?K82LDqs*4l%NhnlZ@M{4UBgUiET`2 zb<0n=nO#Mm3{HD`J9Jkx%6#xMqns9P0=@>12FHPXciM4h2T%&TU-j{*0s>nmGcS3y zK60g1$sQV9_(S%Xd(W$2SKaD416%7Vx{7!s{02FI^T`7`5y$Os9O*4BvAG2iOCs>N zRZ7h+Ch6+YeW#ZQvr6I~-Zq7&Aif+1B5KkyzN5XD22_}U0#*^kkM{PE z@~=Umd#TN$qj}wlT(i{_c$(6qj8mHNd2ol0>Nq!X9s{l|mQEMdiqeO~(nZXczx=L? z_W@{c)MEwi%*K#jq$HTf$)4DrR~vB}cHr$5mZyrIrhcpwV%DbzP2^Al&!NU` zn`*?e(1F=1lC~;imVs-VxNf5WMj z6BExZn*q*00J?6c6K~)?ZdfN-LU+i_rSRgvh7FMxEnwlt*xDBMgo8UL)% zi%(eDW7&_!(w)0{lA=GF-)IW_E4Av#3v&H8UQ3_>4~JcEkr;_4wq9=rnX}^dtpDCl zC*Rc~uJd0XOolF;YQG0bq2-xk>Rz!`oXo#%rTrZK2y^AST}05mg%Ci9C4=h%y>6*~ zNTjxP6ys1o2YrSWQ9K$HS?s3#5h+b;u3yk3@Q>;NHk3AEWNGwp)Q0Uq_wN^0t<|5v z?{g)f&5djAI--l}+yRb!gw8|SVQXB!e({dxcMo;l3akp2zS{mL`o&>wGR~$C_am1S zDyIMbAyVamA2|amT29rP?TAUjM)!TiC2#wR@5wP8lwO!Eu-Wz;xK62db@i%zd#XO|E%LnZZ2}x(v4mwI7#)qHcc9hx zcV2G)vCkO3xWNTQ4t-{^tyghaq;Na1q)+0iN)rhD^WciE|3QtDP%x26kI3>v1za&$ zj4YK(ctQH&%9jv#VHRC1#B2fB6FV7b0bPk@-gUMrWykz?!$dk-0#;W;a;z*AnHdLf zHNIZG=S&s-5Yw+^fD{7!QPYJk>;G*qv!Od()bMB{lxet%b21$ULd{0<3@wHWxFbSa zE>U?^g||{WvKOf2hb2FPq5^#P)=~9|ns2c&{KHO{3^gyvOrp2>$0(~R-rgT0!A$Nm z72ke`9g<}8Ov!(Dp(VrK9y1k5C|1QuS8%ZODA`PJpUkF%r7q3OL|r*recog?9d0sm zTHC(EHM0HV?81#y3hi>;C%;Z=zP}p7qllbaS$UwK;-+)ZuN-vgg#!pjAGbP;o%4b{ zr@hQK%WK8-nV*QepKHnIzu@nK5i{FZ2+)2sRTjjrFzdM{$_fAS05M(`Quh+(v5C5 zDUmU$OYC5@$+9eO9eYD}LlM^*KLnSl;FD7@Co$xoD5p*@Z%}R)9JM0*B__}1NjsdB&oDC(9U2wKardZa$ zef(nc3pVy2R5_wJ8JJkncp<~#)cjqw&PIL^2~2i?7(K~mrIdQ?@UJeVuYluY63M$E zPeWt`j@yFW-Eh6ttD0@YL+%oUOy@hN%rkZ=p;O^2c?2>uovKhFLGa+nvEZJU_rROY zuG8OS@Kte*Tl`#3ZR*U!qsp2D-wXzc)mlWi%IPbO%m(}k`E>8%V%vG6)#XqRm}J@Z zIqyRCO@UotWnNR4QTU{6t;HsH#AU~m5C z;hraUTagZ6KMUSU+WR|WX!NQ{$!=cqrLhaVE^Jj1B@zyVv#7^OfvPj(Cduy^Va?+;Q3%$Gsfm_KJY6|Ma{IauW zdaT&!!K5ayb+Io|yx3{Xqbyl_z3shyu^*84JaU z%K3RpPo|NK6f~_>BDEDI@A&en9=?Gwm~&r@?dpw&p+&XOm@w#G*(>JelaMUp_!FD2 zd-${@0qIomac+#gLBsc~hG#y9rdv9GVzH)B|MOdhhhW9q{SYW*Tc36-<8Zb|tK8`Q^@FHjwei(bZ` zjGb$?gPeC{{+t%&igWRFw#9>akh(jeO>2F2;#*g77s`KrzBf#RPlve^jbGGKAly2^XvQ7ev4F$Etw#^RwVu6 zSip@Ayz=T=sff8gAa4vEL8Pvpzj*FCpDi$@3t)Z*aai54Qp{r5jAIK-h$rOZ-z-Ej zaRR(wLDUJI-nNwYp&WF8Bbcbno*Dj94~jD6bV{Zxq=jBV4fOu|_fhtL z0DC}$zb_xGXzM1B?P12oZA^1=;NFPhefIM!;aQ@EZ@yQa|JEC8%>b@fUpw>U>r4Ok zz@gmrW1BZmmNc3K9N|MHSUw(sWMFat2oM(lCuj(2aEWw{s8XKVuW8$a=c(K^wA&a&C#Vc!o1c4E1bO8VfAvrKa^^yL+@-DM)Pyo0J4C!qH z#;;({F335x7eas03E4;*06$M_`_ZM6j$!Ebbc`5YPkM<-6?9O_Bz5BaCC0U__Y zhQ7May26FrW{WSV$W^fa<1o19H6Vao0J#Q5{<5MyZB@{J5HjiMYp4oxAP%6qnj$|r zz5{@P0qXOR0_?9QQy4_JfH8xE=rKCBk+h*+akk;&0{WKL?ZDq}vL%qsMq{1fl*abY zYcu>z_}dHLO-;V3!E_P!ZcrN^;C-Wj_$RLi@OwoGlBj^XBB`H3)@qR=LszD;W=~iNEuv@x=~97NW*(s3C7FH z{Xq;2h~L*)kncfzpRs?JN3O#!WGKRK;DJ*0UqMg~E{2uh00Ny#8gEFtBiGzyBTkuPV*u7uEsYtNqW=Zc2C%`w{voT=?0cpTF?Jt^?S= z3_h%1-jD1e3IKodCxra9AAJM}*y$q&kdF(X7FY=CkKwN=ii7uj;;TBlDN<^X4vPqp+<@=$GAMyaS8b%!eD@t%$2m>T4zF^z8 zApg9DD(AfUddA*Q>Zm$LjffW z8sYm+;v;aD02u$v{FUbgiA)Sdhh_&+kZUeu&e)g8gM4fd@W+4r@y9vF=hS68VC8ThQY^1-N+b_-ji>C{`vR+`r*H~oV)z) z@m){v+Lh1vfMd{l5&BUX1XRzl;4%X#0Zu{#W}gNS6b%3qlq^K+uW`KiLhy&t5g0jA zA~y&QApAhQ&a(=MGeGEB0fA@91MmXRk_m*UAb7xP(t-`5g>xwN_^i?_7Ud-qW$%UI_sza z)WHHcp}|x5E39jf4PZwAH_GC&mfpZ3Odnt191*UH)~qQ)CTy@ANRQS){89P!E-HN1 z`flIuFSX21J8(z&z(jvFIDn(rdX*Dv2FVy7q|LeuAY*W_5{bfPYj-Sw4@@O+RuZSr zD#+2t>uYV?xI8vzF?+A&m|MSQIKkeKbvGYGr2Ferx{>N-^Kaj3O{?!7GIYj(OIR%n|vZ)}70&(JHj0g4_ zKcE|UIPc*+PC;gREGRyreu?a){Gs4yN}L{YgGE3|1%cE@!Q#USY)z4v(L2`vK-_Ca z5R6;*D}i2b*A+O9!Ty14bkK~NfHu!5CWJ4E?-4glI3d?5LoomIip9a-lHdrI-p5Zb zO4=YH4VG3=0zNs<{?DK6%OxM2)dT$TU;geN|M9>4@m<1zyRly1cyjb;)C147Rg^hb zzqHy>VUr_}bcp*Mee{{q4)hf}Lm`I83n~nqSQJ zV&~^L>6lN-e}n-?N@#$w-rvs8aQ2TzKVs9L&Vn%jL%nNtOfxT$h?BeQDss7)63R61_&yEu^b(Lru_z?)_!9!MYbJ1 zl&xgv>f2i64^}`Lcw_X+$%3A*xiTIIt(x=m_@VlXc$|0ZS0hM%Ab1iS(%oy{+5O&2 zyEm;+d&bFORLkdu0{}=905lVDMXOJ60@*>;&j)VLo5LhgCB*3k#{OLzrZqe5eFpCL zF@yf!&;PTApa(+&U0$Tc2Pgmp|5pV7PA-QJ@+;DmGN=evBpq3jG>ikU;5y3x{JB{m ze=!|QcoY9fMUaTV9!T%;)R*2b3aAyvoy4%9)?$*M(R3aR16!qR=lzmj(gu!?dHIkb zS8@O@Z~K(miG2oWpGd)ebNadQ(?o%M)3>sgA)i7x=_mRrn~(f|O2FpX6&FYHyc7>$4;vG8{wP2yA?2Z?z~#}egdTc&PK5o{IZe%0slup@QAQC&cuH>oHhDP zEi_up#EvpmyCr+ye}DJuPrkZM#_KAYj71Ms)*@(rSdnmh{!|AFFpy!vzwt}rj(Q@R z(jR-2&6ZM|O5uFQHXxluSgLgZ9|HPXvEhD_2WI&Oi_?P)AUpepi_5H>$l@wZ1L2FM z?ohn;cNzdhSmLDcq+<9m+TVJFz%uEKoM+3g)f6^=`IFpLBQ#N{Bl8jeN4CAXz?#5E6*eZ_A&D2W5g$U z@`WcH2}EQ7bSZw>JkfHDQNjHk&KY7#_`;>j*6x1eH!A=KQY!eJ zwQKP~1g5;_?;ucCRgbpXM;HwVp$-?2fcA&=2ZH_(>UM7W@uJ5VCH@FzrGQ}21+-30 z(f~l)*Mzxpg+)K;@sI2KD}e#2U4YXAoInLImsy43MLLKBC{3P;10?;@Cv*V8S7zh4 zJ|T9Y19(CWHC!igjWnn8=g+IFfRU%Z3G;)*!19%G5*N|b?{tOxXx?^@zS16}Qs;Xv z4Lx=A2mnOf_FybPaDJn*n5Y0>&Zr1jVhpN}TfbRFuz7szL3?pDh4EQSM0~N=>%0vC zBm=1nnqKkRFM|xSguQ2f_v4TMQW$_d01t5Y^*slfzH@;Lfcd-KxrT~L+G@*N#wLes zdVcz9UoCM|j4bpjm=@A4U zv3y{6KiLFGKaGJ29}rkD4$uYE7%7+#9EK}Mojh?_vah!&`X}6nolo@VO-yhDUoh6fOG*W)kr*Y@DRKZ@0G?{=me*fh z`^GcK0#FHzs51q7UXTMMB*1b%cNGKh7z|)*LHFx|;(KZaMIW%sq&7LK%iC_R8j?F5oyiv4fBvtyfW<;1+W7Ky>c%+A**xM#tpiAq}(15E+u5C}Xx z4^fI$&f|O-r@}sr@nMse2{Rg?hWaZr#9r+>+vWlF^b0)GYuAIkKg$qUUg<=^p_-{8{CIm=-8@MK(gdWnbegVX8m5xn7|0@eK-gmyb>V@WrkCORazL z@An4ovE36Ju!GNioNhnTbfEg^D+Ky(ALF#p@l|1899^&%UVnA>8!zr$5GX)k^XBRtxlS}7TYe%W zO(Fo>-Q-i5ijPJLkdphXC4i%QoeqF|Tx#;V3|$0UAU|7iioKiYB;Q6a`KC?p>^}R! z=?^~mV8;$RN~9c$VPzV@+d<6kA8qjH3zisUwqc(F=criFx#XXA z)u*4Ff6T;$dyyhJC+QP^d)tNZ4_wE4nOt~~gRRniuop)gJb%3e?v`hq<@B!L!D(jNxcLA1 z2=ziTB54VRA{dKneSn^TKa0tQzbgnO41w6Y<9C1ecS!_T6ad~m&fw6?$5~Hqq900$7)W2!vPv=q`cveq9Jg}G?!63vM1DU{3%U}n+i#=-eQvqPNE-pu} zvjn*1AflL*)NeW(YYO{tWJ!DM$@LS>Qqf``Y#TgiE9aWcg?S z)qx1s7FO1-@67of_6Xd z^g>@CE+`&s8KS{q95oYUf$FVTROCOmcK7ZVH?3H*_cwRq*R!zRuy0k0EIm2C-bMi5 z;TZS*=evM6-JVSU1pqbj2oZ1q2!w~^?fDGRG9cg;C8QW%PsBkZ8CnwCcK>MUd(VIH zlb`I}Mq4?ZEM^F+5|v06UC`+g7vR1N(5vuU=JST;FKO6zK={DH&z}>D;lF~(YP1|M zVV-X0JUMH{sXmw){QV>#0|=7?@R5W>IfqVggaIk$=Z?}OzKZ$)2}mkc7_EGJ{*OLA z|Lyh^|DVT#!lp5q*+~%?RuKF@XR6NkFEwD6h{U7qLWiq9NRsy8#8+5zIAl@3vx(wbxp8@<_KQyM{NT4YVdnD%08ITQ6!<^Io!{0TZCW$d{sSEF zp}WX3gt(#&ke`u|zxnVuS-j?B(LCf+PT(Kl0<`~+-@CP^`MeJ zc^3Z^4^V?u)=1wSfkrTL3MQ|7A^QPv0b+qQ4hTMM(}6v~qKA5h5+VRp12&WE3j_)W zL=}+dVov;I+k^lFxb-;vKz{Q|5WW*C49U~ARjX+5RPIBh6q*Uy!fBE5~pBOAbs z-qzMu=5>`$mD5Dn5aNMoc+x|phDgp<02Vf_1lSGC8!-|w*->swa2_>jfK@B_2lW%h2{OEuc*gs;Bz(tEql{^ay&Z|(F)rsDXj z)ES_{rl)LVXjA}Z+xue-jlsFgYI;Njsew>k@Ht7k2e^JFaLIBaC!G6RpK~~lzd_U=DNec`I5JH4bH1SWT5=gWshFCC{pL`P59HUdCAs|HoIyu13 zMBU?ltW1>Oq}<@!z$@KJnyh{zU;<3km6ZZnVaA*S8jM#9{2s3(*iRFITtr(dV*Edg zBp{K5<@E;xXfGMrirL2aGxG)v90(B-i_kax8GeqKgLlOOcm=tTWwlZV5Ksd+!0*1F z9Ki7l-DhsSd+XM_Pd>eIJ&|#-18V33BwL?6(gHZoI;=b|#%0|egddx>CUECu*@uMx zo9~ex%q^**KEGixz&)HDKsmtxWA@d2LD#{aQ31oo6$}$q#Pabgjf5761=Hvw0a#@K zAwg^(fWQc0gV>(?`$~SK+dmJY3;h7UglNG0pBKC_7uVxw@IQ7HGi#J6hB+aF?Dq8< zJrV(3gVkeWW7UKG!0pZuWMW@mDXBVMqUXekx*T>#@t-`upsef!Kp%vF0<=J#4N?mr z=zdR&U_}n+02ZLp6Gv3lUFvM8EGo*iRZs?yMr;dWExf=Snnj2d&1TfgbiUGka^hBToR`rxsEZ?{E)mNT>X4B5on|_lKA;QpFGR3E83)B*m z9=)x6sR}4#H``={vWWuVwHhV(GIT9*0B-%}0iSS*8{oSa=>UX_mLMc`_WiRMKXB8R zME>u*bN1}%(?7ujZK5e5#77MxA`Q@juxpl`cHsh00$*_69Qv0YeQNf+zyq=WUz-D^ zULt@q!!6Nau$Zg)fb>8bgQG(zEloYK{>te_<~)%;l8VxSwm=4|D+U zzjyac+vVdwdhhPLufP5zSuF-pJUmDM&~77-G`@j5p!jy_jVR@V7hp3M>swjUQhgij zk8qvd-Di>d#5I7}vs**eb5CCka?h+^dK_cL+8++d?sk zy`%s;-r2V8gVPJBG(@m~0|^J?0&1Qh_}1!~<|MD3dIks|5g(5CCw2ofck4KcE4GTn(4Dicx#y z)eH-W+ZncRuph-ku2XF{ad%RM?@Q=<>GK(EfZH*~*B_V#EWz>vV~Cr6$ZPYM1(E*? zCEzH2E6GW(kT+~>!Ax1)NSZXxOFRZa-5-363%H9Sz`L*Cz5DKyPd>R3PFxQl+1QcZ z>fRCNboAbVyVpL}t{wo@`*tjoPX5RP$l#k`?gHWy-b>akah4yZt^*)*CtFzTrVSoI zM?YhL8=EX2I6z#^9yvde0T0kW$^bF}hd>(P3}N`t14@V(+Eoi%X&)euFmQcAj20!a zsCgFKk5nV=`p@zSVgmAzpYj4<(+GNb5xoF%YDRZ6aH|xeEqi@)O9xH+^$k75|7Fwx zN^RZmXzd~^=&9=i0;tD|wRJoIXg>qerTt*hIYx%tFI1!iXpRhkv_RH?PykS4A?<*4 z2(wTMD_B!dhN4&zm?B-uUEq<;GDQ5SqS{Q)2DyyowGk#GCvc=OOv&O8}n@t zY^Bv=Rxo{l)@?x2;P}X6cmxpmkm1%RSO-M3*DtGgVs}xy$8=={xWbU{SNpw~=@3kB zVc*`1MBu?_u;?S=V^LQSWMnD(8=?gsBMnL70QM)&Nyg7O5Dh&wLpc6K(F8MoJ%GuS zv%!-l(hNr`@~oJ}Wbe;*xAVlKG8wnj&Vft+Dz)LHx)0IcNx=pUc#sVxn^bYQX0;n|v%76bYHoG4L}h~N`e$K(OpZ3oB4 ztgsDs5MmSTL(A_K`DEEC$oUogY;1+B)hLGBrie= zgw7x*11v;k02we~zd8(G)bK=I8IeGhodmU#jxO;z0DwwvLqy)1Bd-VICkYsAW*r;g z?{r`?58G$%>Foh#2~E%xpnpI^05s4SrQKEaC%bc%ZAHspTlVVNy*nY~*%fRQnmK?a z5xl!JW^+%kSW% z^7NN~_vHs?9UJ(R3)6^|2JC54k^5s#9+lhh-Xpx}C?2hTJAJrX9?Fq;LU6G(q9&vYmTC^#>P zr3#YGO7iL=m?3>+B47?+3qU9j)${fjWv#(Jgkw-cfh0brUs04ICt z1-K*g4xk@1+(6=Qzb>2lTqgfpjl=vY&4CFJ3~-z)UzaONi2MZb!pE5q0!m2f(2QO7 zFHr@F$7V8$k|y9mmT!RR@muCv?h{<(92o#kAa;LlgESHZ_<$bdFIs5?@c7C`D96Z< zj4^p*+)05Q)BuPDs!$b4#E#l5IsNweA@sU z2ng8M4eu%WR}?W$R0aT=V0;EIkmCi2;H9mH01Xy8P`I`d8DKyFJVLIX0KipVUXFgr z5xGCTdFAC40Vb#f-~p;HR)Z1XB^WH!#iYaQlq9;(U8z6UR#3EM=}TnfArJbAAwW#7 zGV!yX?B?#cG1gXlvR6fR%{@&T}3&{D9bBHG04{yYH>V zX^+2w)tiW$@nfP6{B)>yGHaQ=8N842XOX~vcL3R3K$f5Nwl`ei0x*Aj0Ym|!0VsS< zTwv@8qX?f65z>2yH;*>UTP(GqANIU z;hLG}#0YS?_re#99hfMblN)&aHa_X%Mr!GSv=>lcJlM#^61djX`7sXr;J!X2aeb2? z(Ao*~7n$p8`?0)8ZG&I{r-Ki?B=Zx7EBqD#WzH*}KYsHT>Hm9oFK;}vC%<6pM;{fI zb;(Cz(O*9_hKvUzyGAzC&5LOg0t9@I*-GANRI-Z$TmFV7wEnSjIk^CSV)Pr>wJio> ziKOtDEtUI5ONFbQ35eGKFtpW&`d$_KzEwRAAjr9t>dM*nl9w1b$(<+H{~CNl=IiRrNhR zH~<;~;Vy05^W?I%Y`=>ZZQAii0M-G*#XP^%1xO_@?3cc>Fl)nkKOk0}SVAmb#-<-b zBzUTHVVOG;f#8st9H1jj)j9sfIVY5HGSBST%jR)<)83za@cv7?0V8LdR@|Qx6kUK? zx--Cd&GY;xi&g~{z`b|7%3HDV1}DcnJjR668EhJ$p=!$90s%OS{)a%J8f2Jz;vmN6pZWp3(hX?-)>7Qtv+ldt$@P$l^q)q981Y#4 za8EYw4|CLh1O=67@w-AJ!1v7aJx6?=jr;HgFVjqSuKYisA7L4g>n#%Mm&FjNYKVcy zx27DQ_gCx%gkjOur}4+OuAO_#N`0Do#0lmMe0hhD^Nx2p5^N;ef=P^{gx`7Z<5ypO z>Am-Ubo|W5Ly~u9CM!)$8 z{RUP7iQ*v(ZIE4{J0aN+V1NQ3@Q0b32Lu)%_QxyUfH0JmA!GOeY0G5C_#M&!B5{a- z<>g(~;%*c>S$)NF1u9_#R(5o8tFM8E0WkkU%7UHr2iPW{{D--{EyC6Wr3-b6^KUJy zQz;Bu5LJOXA^>s09G|I$7K{o(!9q{~nyV`T9jjZ$+2*Tzd&dw1h6+64RM6($+szZ@ zBquG91Din$iA`);s&@ed)>pP|T)J%8D`%krta$M^|H%4}s$N>U`%IR+FR;3t`0BO+ zAu{Rv1hp*h#fL~3;v%HvWedCQTTbB2F%m{D{I?lRtU7>AgsK#bcI?B4XCxl0`{}U4_RhlR$&&Dh2^3 zL1p{czN0uOx^G`TR;TqF@>`d;JU=(`b8>bwbRq&y+9d$NYI1r*2R>tVPj4>{0KO1l zpsJzxm|z2NCG=Rcns9+<4Y7sEzaAzd1P4FZ(4R0M^$dh)z+CK}_o!8n5;W1vGO?c0xM zLj!yy{owQ&?bT|11}FxC7iE9mKdq!NxdAq==r6al=j2*w0g5qTD~tgQ2DtFP7=W?A zPb&nG9QZ5E{8s!vl5-#Y!K<$l0Bp=h5vTA(Lr;G*YCdlGBS3&^){S_9`2z$FOt#b5 z8v{OQ@KG4R0)FK7THmwI{I(Ep@NE=8PasE}fpmRa`k!@SvAPCxe@&XfKYq9o{tnK> zz9Bk__}L5!cSzU>z+jUwAZ|l#5Ppw@VGt1H5+tDB{66S zD1b1$dT zdB?~FItH6@1NH>jCVXrH44||MOyb-qvlps5oFiC><|tjk*8}KvGB)bDoW4dOl~S8mCa2% z{(NwS^b=AQAfp7Usuy6Iqja^;OzRZ!x3yQhcbN(J9a?8Gcsh!D_Qyw=jm ztQ^GxD3h{&;Xn?->Rse=s95A0iPFPMKDa2sf>?HB0=zKcH&iqKkC%SyX*GbkaxkbL zFxv1ISANFV;WYw(kp21TZ$1y}@bquL`lHvE?qMJX+SffNdL{<$IjI+_{>Xql;7MZE z0mpe56+jAB;rAj*f>V4Y?loa$X5RtGv$RO*`!&(4Cjkh~F2O(P{#@e$2)l58_#U+V z;1sd&0FOsruu_RYpa50DZ3$?S5#&7v3X#iiz)@`A_n7xf`xg+1Oh8v)9>6_LkrW69 zNF0s^Ash|W#gR!g#M1Ak0}vP#bywPFER(2!B%#uALIeOPgfOYZxu4Jh zCY+j@hvCx|7_P_;K`;Q-fr&}3UoSq1Y8Z+l=!aG^+3-tM zYkjd&U$E=GGRTZhXnJ{nENI02K_hW_i`e5r}y4XD?-w10LvJieCkUnPx zt0U4#-oM%5!6X`H9gz9R0%QiS#*?gs60{muGD3w>JW!J-un2p%D&Pq`&>MIfsX>^3 zG$Oz`xPw~QfWwLiYQ~1B3E%+w?aZV6Ph&uXLcOVJx1TBqVt$G0*T=37CRmmO`+I@# z|HgVd1JD5>DhLseCo3r^7PWx~6m(bEk4V1}jGvat zv1)n&zyLM<7Cb^Y1E9eUq7Z;Ua)myi!O^P9ll5ht3->Ku2ODVbtVKwKXQM%|Q)xc2 zF^NRYsQyI~fE{`_OkGhZKD$bV015B1e`YN?uZWcOC_(`POuU9_0y|gu=_OFf{g)=J zEcRuEAr+)zZ;SS~(9f{04g&au!+(Gj&Xel`61b~5mFB#JyHp8Cqk&oS0wCovA;`!) z;&$tIoD&SFU8l2)_qZ?wNR%Mn(eAyi7f5qOr2ybXWd2(1L!8_tDM0)8_qh9W=9Gj1 z$x*U8fP|$O4-RO-f#jbP&VVZhsoE?JsCnMNCS=ttnTUcWvNcAV)&Ded_j6VX-?cCw zLHIKY$OHmdn07>2zKNA*v3|Uhn0nDeXv!}8PpdV4=eu~BVtQ{rWO0EXE#aT9FnU4& z?b|iWuwP`*cURo^D;6f>Q-17^UVP)lAJg&i%8!=rT7HPmm0Gm#8oDNq-7A6f?{wex zBfW}&^`()|8DJ| zefkr(dVX(;1cVyrCQl>&V1%HgFdzUK&BYKO_CIIr&e-Ig-g_;WUM~lN{{EgWw%yi- zzW&yZRyo3bcmTLS_yBYS@QZaafvxkQ_j5@rhXX)VPZf%C zsRke;i2HmyyN1{kljYs-_<;tj3QmebxSs?BF3%_B(%R2F5)3)&nLH*A0<)0-^Z+JI zg*Z_`4KPyj$o$>OSsWtYk96QZ3gFEd2tc;4Czh|1I2dag&ECAa(&@ z`a~L>!~ryY%Y#0Q23Xj;qyT%CSY!ZPM+884bv`8a%&AgaZ}5TjyuNgVf_Z$Z_=~HD zAGN%n`hx-;NOk*cLYR?4SU-mt%jzq8wf%I!b9^@I{+(qR&yXQ1dK~o#I_8Kfvf_R~ zw<|od=W{P|2S3c0zk&C3=iCT5!VDk*WhtH5&H85&;SNtl`vWaR>tq(m)5l7LfWM=GM~`g0*bIQe4ue0aRJqA``7!R z2482S2iL8oJ#}>jg;e?3^&9GVG}NWmpRINRstY1Hka1wZfcOI&e+n8979b9&D-$B1 zXMpJ71-b$&x^qQPg7E_tCr@@S%+cmY6{5E#ui4)Y50E&(KMG|RrNf#hrcey4A08j1 zNQuC)^N(wnzr6Ihmv&Qd_9)Y-9z1wqaM#=6Y?+nc_V}R&KPaO5=0^#XC zVj;j7Q!~b#Eq%}W#D9sr4%4-5jgFG&Q4#af<3tY`bf7?!NpRH}OQSVesOb0$7um3+ z3lcF1`d{p`iGichM_o>@;WK!F?)zeZLU>`&h5~N*u`+x29HD?KD1*Mn0q|f^MF8^q z4buuPL<0ONB8-pD$>xzO4#W?;UsWZviO1v|Osnud9VtJMTEYLEzA67G)!pP6S(AbvzBf280&q4(H( zl=1Zk77f!~UI^D_+rOU|`%4&suT#kHm9f|g3xPAtA|CGl|M1FhE;_yGm37ZO`Q+1E zUU-3tc+9}7>$j_)=${C{0^DW-0Byy}Rd4ke%Jt=6eKmG!5HKCedW)m&2dLSpb}Z;qJsnn?8*{&YDz>8Dbh8G6`0WT(s|*;04D7)j2Iq(01Z1`*v5k9-yJ8wEsHQy4#NQ87!_W%3;L|YgPWD6CnEjx8_$!`YQzA>s(*U&QENw z+@Jvglp&=A=o4yiq);u-&O_@LDzRR)X1y>#+tJGU0;CDB>FM5HB7l1Xh!!L|fPnxw zz{G8&!1{jAUgO|oDyh9plvLnOe-JVZQFccg!kB52V<4Nzr)-TUKg zCiR#mAK?#&Y~&x0&|zW*7)xAFX_h3`pH#sUh?^0{_RpYbea~)@n(C!56ZvKwhxDg+}WlfwccX zE@(s+)I{L0Fn~e=%ow%eSVL<7zu>1f2V(1~KUe|)=fyz>L@XpkLS+fWAOZLVfB-6z z$YQ!Eq}WcWVF0Te8(|O$R6GasT&D&&)$dH7{$s}=4Hdha3+ERWNDju_3k3rS>Ph`E z?^+asp#w+_TAw>f{SOymQLrFKq(C)*D4>BPp|Zdzpa8w_{EcfW8;ArPy4caun>RE7 z+&VOb-48$rY(_S)8R2k(0N4n$FF_e7AIHyv1XPyQ<}Y3L(kstzvK08@&hyxE>OU30 zPvq33Q=wcJ57B-NJz>+r)JCVWi2XMz*l$TV`({p`d>Wv#A-rCM4^VW}d6+oZYv%0f zy#NrWu|p{MVR~=pJx1dV_U>nAYfmeF{QJMp zwEsC6B^?18ep(rdZxrCYsQbHSW{Cgyut0pkPipF52z;L(xAqs_MD#BXn0}sP3Jo@_ zL!?>Z;Q^ct3Q>AHFLVHu{cr_{oCWnKN}4A7*^mXUWMAkHwg1_f){^0V{B?3+o|Ogl z03vE}>r*4o_jBhHy-(=QE#3R^{h5>>pJ%Obday{? zYp5Xb?@CyKH&Z~t8$5U>Lcovz^v!2>uGsdNwD&ge&uYEE+#Bml6SZ0Re*CL3{v#z)IP`YKlo0hAGI{P<|Lc zHQWa)(Sm{#%!4q4nBPv}EHPLqKPi4cBqP|w5Daw$C7eqXDQ5F0!%?E(2;Di5^3mc% zN}NJ`vmJU}Jw25eb7AKRkp8k0SacbnfGD3f59c>042VZ4i+X^_KT4vK01yNwRUk}Y zewe!BMWGRppTB%#evVlsYfc{RF6%3A$&&|kqt~zMX(q!JXQ@! z2v$%IGQj>$a zlmv#T6p(DrvcoWO1t~dGb9Q@Mv=h+5jm&CS+>4ah1m7Cqqk*eG43usRk&VH%OZE|1 zcI>9?umX~zYaAF-#_wnH#WOGd!`=mv637M6#vSV)b^A2uF98D_Jcf8+V0|;S!3V0L zLI(Z>D*YmfdI}v0OUfa*AZ`XHU?>nr!+%Q?SxrJgprA97&8I=(-om@FGX1pqI@{Sg z02cR5yaWP#SL^3#UQI$}exLS#fm);V#J2HXn$KZ@KC9~|br(MsZr60)SN8=x4bB1t z)M!Qgyz;KjKnD=8U}6Aj@f>Cn0zNWxQ_J_I@A1Ffdt!sXiT(f4&K+x?Tl%!&e}1l( z5qNcd{gkI{)yMow*wF>x*pbbisrjj~M_$VIfAZS7bEDJK!@z)dCPxlW+7KvL*C^g> zf37_{r1G@#ySN)YyL0gdS|p~4$@64`mIELN7`=l=5V!=sVX?XcJa9iXoCOAJnkWw# z9pG842Cy)D&IJBm;c`+LFkFyj!3akb6^2A48UUQ&&k;>XqQrJKaz=QlT|l6BDwu)& zY`=)1O)z7uuR%eOI{N#ub;beLk_Rw8a3O;LYSjV3zPTub^UG-~X+Nr?@`Bb^ z_btlBzD#vJQG<-9vh&kV7JZULLHHoIqVB*|HbuY=W0O^0rQ{hgPg@2rkW|>Xe!;NV+u_amU)biKs;=FxmfOLKt%XO$MHN?AwT4 zT#qH?uOtVONi9hhv^&s4qF@FcFl+NmGF_=OFPNu^FG}<(xBV4*0Forh0$IPHH~|y= zDJO~#W;5EbSWEg*K@7u<)IHVwlW$m*LJXXVwSuheM-~F7;2aY18!>R2nKv3w54^eQ z&(^-UbMLmb%a(50lD~X8V%F&4UMlRHI(F~gy~Bjvno1*Cyn#KoT60)kfKzDNFtn~} zdUW*KwJTpuBX&8)JUi2Kl7ic31ACj+3z}1{F$>`@2k!qhb_L$&9Qjxr z^My++C@34uBDx^OkaMUc(E!~zEfnI>;;DgVBHBjgZuhy{i`n~=$Gl=DIJJ> zpML!uZ&xS_8lIri20k8bxdNcS+A>;-oEx;>1|c25)4SFjJ$m!ZnjGqcc}E5)0}LG* zn7{?x#^pd!keM)XyT5r59Dp341DX;b05yTCw*2KUKevDFGuu{x1HuSSVv~-l&gi|t z=uLcdEBu^|N3l2WgHZc16uslk*`|=%bdnXxmf7LnO@H*`Pc1v8`m`}AQGvz_0mFdHzQ;K95w4gMEXAuta@#c#=cRus#(p|f@Y+1f$%^Fm$9EeBidk+c!Mopv9 zs+_wePm_S55kzdr0j5W*hDWcQyD~mKI!INZ_s*T^b`X23eHtCaJjJ>V!kyr$j2}sT zD&lF3Zu=~}N)rZ+#VaFbM1Yav666wSug)K`fM9!oZm5yqop13IO;8K)Gu9FeHlhj0 zP$2q)MgL6#%>C^RupSuHWYj@wj4Vpxzsixt@C>0IKw^r4hY}A#IjfJ|?!R4*Oh1Hu zHe>pOfkoKq*VomPA|V-w$v4;-yq-b;;XVVxfd-)bR8%^4tYSSoKc~v7wzlr7N@oaE zVEyzA;SOro;{diSU$|%EnT>0}6YBE-SC0&^j!+4hC`Tp~N`P{KEu5yx)eu3?Y_PPU zw5zqgva)T>mR-x%F)et(terau%kR&PPYN|@Yv(1VNZ~sD!%Sg*s`ralvDH{L;0qYZ zo6eMz-tW!%t`*x}eDTGdi`;$Wli@A+0M_r%*NPW8A~fa+S_b@95J1EXvsv^WNaP7_;3Y+{5~=IX!+xq zqI`@$g9Go+2a2N(Ac>4~$(4--zoI&@lw*Hz^aKU)&r|ykx&X(F4-7`Z5iXz(L50v% z)er}KZhzypHwFjC#Y15K@dF)HMf zCBZsP<>h$>_aR1WL|`1xN&vONE4e?hq$^2R{uftg8=K`FmGLUWO_@q?fo61WC2iAU z10}7{N};qsX(^1*mj`INT1t5WG!$q;vC;H}wCiZWcG6OC_Cq^`4=Rx@Xfj_Cs>Yct zCTb*cF>(9km&KT=F($_N`qcocBZ5U2k_n8`uYUH^m@1aUDJ9~aBSJ2 zU!#u9KI(Qz04NHGQEHhOy5vCD{ut-QfyeE8^#WuNVtcIDIt(tWF;dDqsT$${3@X|! z876UHck%gxfd&HB1-MEl5=?-I5a0p59nQh}qaVis_5na+#3p+YG_o4ud{MG;Rk~P$ z&mz#n(mebW)v%HlsDRbf`I~Cs06T7!d4Lr8s`p(#T3rM*0O=R@4{d=Y1oDQr!T1pb zC~|c8q2et?`-&j~Q3fd10Wed%m@7pEV{8(Da@hZL{c@gDTY@{sN&PW>P8kwLqcRg( z5D2X4FFv!QeE;Atf5e{=V*Rjm)6}@cy$wBE#R6cChwPmAvfTi-^@UKae}Hxn;~UHq zjVR66N&0yPONH4^k#V5>==Qdq)~rcpv3a2^-|1hm>4Od-XhX3EO7%M@P`O)g1+5v+c#MMLw*1)s#WbW>K zJzaml6fY+LNIk#-Fs4;sLZFYt{p>`<0q_r&lz`r3jW>}|9*~9rF1h*C8x{cUs0ggZ z-H3v7tv2>#0CVofMn0+2x0QQDmJ6y)O*j3E}&Evvu9iA3$O@SL_Kh}1!NzN|M3$CHTddc zw1PzVwonrQ6bOP~3=rb1c3U+=M$Z)$7Fq*5|hTAc5aigJ8w+_0|%iw*X=xw!Ufe<~Q+9gaF9i_98Bbm0Fr3fKS!FlFx#OntR*sKYZ|DAb9{TW@KPCiGPVo=mVg^ z=G~Wn<-#Y;UFF+50stU3ZMC?y>*@6JOM82|0mZdXHGdWBbFVfR^1b=VlJTm^+S$iX zo;pS4%lEK zQdqzXIN#K9B}@>XA?lda@p-b2XW3bUYfzuiy1zbcmla^5uIt~VSv~u6kefv(3O;i(i6|m%w zE>m(G%}ul6b%^~JdVm1N>%}3*RWPgXQLnJpmV)Z)GybnN)Xks9AnDGhtb;&B#`o72 zGl9SuC8;Vfr(%JI0g+~=DbjKAk;E|<=TwX4iQtUhJJmIRD#_09=B$Rz{&epr(Sr~q zKu6_Ah8%EtFzIOnR8iOLmXAw9wF(SCXxa0|`%D}+nSTEl-}n)NSM)0HZB{3M&CH#f zZM>PWXAocH_nJIUEB7BY{^S767m@>DXV4H$gG2k};hFhZ+z=jXVdV*{l_JK15j0~t6Zg?cW(kM)^CTBOqXpd|t zpJ`Z{@d*ttAp?Qw8=mP>Ig9WhrT?0LY7@v(F+M9Z{iFq&;r|e^qTH9NeQ|B44xsDS zAO7lx1B$o%u0RPn2ux)7KkTi5|BloRHus(yEZ=dy<9tWOmVK0{3i)gP{fjR~>FqJ{ zX9^G%bC7=*bsK%UOXUNtGq<{aX}qbHM%Bfh8Fn+{U;+yZ09j~-p$$lIP9|+^Zg6ju zzqeF5PZm~ePqg2NoOoPiP)v=L9DB=3{w50+@)ZDvKERoTUkinH8SoyG0Ps326=OCk z*emK!v@aeQbq7r!e;^}3UN9Hs2Jv)+412lA&mvS1jAD9)2{_D8aBxN?>=`LW2(lLn zGEU91+!~X6*uxQSx$}3p0muL?wJmr4^v)ex1YiXs2}Zv!XD|Vj0Z|i#wqiMh%ow8| zh?1eJef73#VyxmV6=$Ga9kmu%e7vZ**j|7!6aQ%$fZz^Q5D{n(hJ{!k8~}R(m}&wc z(3Zm4+voP3sW{(p>EU1-|IK6pU%0fm#Jidc0VHW(9t6K9%?jZe)+}EGH08ngbC0w% z^TX7CYKu^y0BX+Oi?)#%IO{bY(PvJKId3&k{*V};4e}8O#6W$*XHCSBVu2<`Rm{I! zGSUri{L~wp?`>q-t|9hjF0uUo^c#!^0IL5{3VA=~(ylH4-)Z1kYM;1)5f`KvCkohP>iyo}3#Pa0LVQaU2|PF+;6m5# z-d@W8moA-W+j5W^b@7%D?p%5LVww(s2#Q0Ii=|O$Je2w{51MK6W2?gAB~6nvle4um zwKKEh=9Z{$ai3M99fb4*@u z!Z`=Mht&t6Ko@*x5u5F>hiqsNLPsLixjfUlAn1zHFAvih^mlhIUS#oaYPsSVVY$G& z7%;33pj;q`!Oji-^bQPQ!YNq6;Q(kSaIlb_0LY1Msi@d@zOeP^^`mDBr4kfO@aeDs zr*lshMki1wShE?h%FzJ92kM(tsi+n{@MH<|1{QBquQ__OqOzfDzL^0X#sH4%j`C7S zR{MZ0CenF5DS<{3kg#hvy+c9)U7@3I#Na<^(PRpgvPpZ}zDRX~t#(#dp(i%tI8xMa;gBADb9=mK!rH`1556g(%f)i-?1rVogQz|aY_0IR1>q8{Hc zx$(X;oSEv$CQU-iRKU-*9E%Of&wy1h;~)TA#MkyQ&m!Qy6IfgOC!hetlD1VhI!B=KP+>LVUw3H!-= zYZ~zh2SX}UZ2*UjnksxySh#OrF)uq_u@6eX(K8>O`OTw91oan?X^{KF2aK-7s=gQX z(2pP>00b<}NdY7YKtN=%_Hk{~wi-ad>tqmTI<_}Z3#7celj)t{`zanYxYq+{+<^{; zz(?Z#KN7Kv#viY`u*a5AVo>AW(>sYQ1&%l-w)xKH-MeA^5oXA;|viQQ+Z^Kp4Q%QWp>@_v^1i+xby>fSEkFUAjI*0M2lQhvU405bi2RdfNfJ!j*f z=Mi7bq(Zl*11^9#Zuel_?oY6P@<#ivR~~pW&u8?4@oG zB|zjI7;u5&6yjMmI03i-?3R}J17!hQ9T^cml&5D&FD3&h+P9bBJtRbN1G0M5{Nkc8 zzGXfxb1B4xz#PQ~QX3)PmR#cSFAxyywc_Vx8w89JuwOtt1O}|}^9I}nwFNwl9Dqav zL^{ynp%Mf`98_`wFC+!A$RMjQ~*%n8OwOnjuzm0Lj$9J*b7Zdlp{L>`_1X#SyZo!Zc0tBRqkh2z=3ae=gCKRCI z5^6-SSr;aYV;UiZQdH6Wx4nIBx`P@q02fc29zJ*ojT)z+5G+&(vpF4#6Wgm{2o ztz>w^<+=;a_te}79KeWB^R?=4p9x_6o3`6H?AIQD1T8XN0PXr2!C$o5w*c1lV&Khf zyN_Hz3Y>YMy&*yM#U~CU^*u}p=sI%0>yQ8F{!ZS)(-D6<6hAot)8a?81q%!)J48SI zCJ?b;rQix!x%g9Oq6t=KH4Yoa{6a=N4e`!I!3F=a>7E(z$2s@^qY&+Zri%alHZ$R7K8x>1(6hp4RDck0GB|( zf+UEzMnIb;{Au8dZXu*XxxmqJEKwbZ=_$-oKHaMW_?IO7FVx9=l{P|h9_tF_@oMRM z0&6e!VqooVdV-Jy=7g)h)&iiLRg?_EGoRusp@H6g-tf(HCL{G&c_p>^Og-ko>=}s7UiCqb2^IQ1knxbGn@aehkP=+rTpT|Pyi-f{%&nUw zC#@O>0jz=uK!nv8IOKZ^_Y4zg7e@4>U3p>Pl4r1mS6RW) zbQ?URk{?Elwdw!}(oGg(=$9dd1q4k0a1$Ga6ik&N55RY9U;4l*47im|J8?x zUGH36TOOML`kNZV2#!hXmwK;8hxszq`F1-4k|@9evig%%OHalpaRvN|&g4R5<5=K@ zg!eAqs&Hz8j_4-O9#P%Dbj1lBrF@VC9Ls--fnjfYf_6*}+l!SRJH}f16m2v$31it2 z^gEFZH^B1}2(WL&K7Gy#fEb{Kz(_#I05UNUv;lAfNk%jY2iqKAFM`1cWl4VyH+~MW zMg5-I#ySiVfl_(8W1yU+V+&A&`iKFTBiaNZI7tQ53fL$Jz$kG?h@l!(L<~SLATS|0 z0FeMY+sLEJR7qv z{ja{_sBQ;&Y6yT6sNTQ2{lWG#+Xr^{HZuiaXBs^pfIUDozNyY2q5k)8{uECX2mw$IG~5>hH2K&-uCtXNMf}g; zKC}v~4Ojuq1fCxidxPHP~Fcd|I3m; zg4K>t0Gt9Nm!i*NJ_Q{pudY3nz?mmK91yIXTv*Dluh3!dOh21=Z0gg>tpBV1FBCwp z%@l40sb5`znjoNooPjm+J)AEl(oeO(NubgNzaD@% zfEVy;%-^Lyj=VNRLz6VQUV)t-B=5+|frUwO0^<;10H>WXl^6#=QgG7WAX*W`9|;1c zUaEnHMhb~~-`Of81Mbw+G+y}tqi=-r2fROur&N)4L6X>0jwGZAPWrpr>v%? zb`b*Lxti*teNeU>IbL*>Nir{L)_t<#l1fML!rd?eT-l9e%44b)w-*`j}h zdq{#u!d4vbJ?PR_!;sxJ9(5zY$vfD)?*famAP2{xwD$X;fu1rz5pQ5&R$vmS?;gm_YdQO zMfxln%H}89*RDhA7YZ-Ud|Y7UAOXRfcs(PZLnY9h;O!rS%t^_WWHOmL&j8 zod6&%1~8~{G6@0#fmT%o0b9TW2%-LnS#WuZ!?y(|JYlQ~m$5=2L1pC%wN{ z>fNCN#?Ih996-hu76S~fR>=NmnjokC{5N{Ff44YzHwwt^x&jX#-3 z0QUcDmzn9%cE17gUvc4?GZlpuw5UyclP>G4Zg+tYW%~UU3}3=&;nDq2+#oj|3NhYZfpS* zWPiMZ>ZNEop-IQ;QsEO?B<cr9e53m09 zvp@KQXG73Pj31@{r5U%bbiK@~f8N$h(UB_m25{uw-i*+myh})smd(SaPmgXeU6YV`43!xmA?Yg{({#G_p$1?5+s?;FXGG`AZ8wMf7k5Bw#x9M zaY?!}?i2UwziF~y>DJ)@+yeYdTwYB6U%uJQaJA;s7<5pC!+Rz#kW@L6AYeApXG3xv zoJMxiru{aC04`4<_JiUFC;+Q8^muFTrpN5;-%<77hFwgVLcsI_2sJqX0l-3M z=fcFuY7Y8<7&m^QCUUWcZOdu<<%;3A|Z|Fb=!`kwS2Y zh$y@hq0n)(GSzN9(hH!BZ!1z@H{sj--lO@kk%>_#dyW@miWt1yUE=!(G5kC4Z7Ie9 z)Y2;0RHIw~L4QADAa`2mAV6YOEkRzotFgMehLnLoe#^e2trgek3@Vsxnk*}mO$F_T zi6IOqfb@?fS#8UelBU_}+G-jLXZ!mdI=qkYzv%io7{vbPA>wkkZjjXlVh-cFK<=TN zv~>*(bbTcHuKDw)ssnI~k+RJCb9GVX2e2v3+cl=`$g_vf{@^!0o}a(pYxO9CRSSl0 zKmZx(Jn7W2rc)54`WQ%nTTqYo@V$})ctk6)xiXijnMq6zn~y%bhy^l+F4oU28UWzw zdQ43K9+KH7`}Z_Sek69krXIZhZ0RzY5w>FiT|9kMFWr=fS9uGzbC&zO0ArrCdY{pYFX8jg*T%4AYB=7 zNe=i1Oip0X5@042ZiWyzZHRhuCGmCS|D~`!Yvbp#r9=Q)^ZR7jN%Z@9bE5n0^a))- zvhv6TJhxnsMh7MB@O${FkAAuCmnr|Z{Ypb+Ma34V>8wCooM3t7DT8Qobo#HZOhD>K zCX1C055VA0C-iWaeA~`}NS*Z>W$m#ET7T#PoU5N(RUbrsaGUFo+5Yc+hZf6nkfmij>QuvOM;llYMMu7j` zO~YfTA5yW034ydfg#gfgiu_Clhw;x6UyV#Kz&}aBu0w~mPzppwv<40_+yThL00s^i z+eOpGzM~a|m6a82IDo~OKD57KR@C#s3@xVlA3cI`<^h$IEH0KT(p9+cTz@qR1V@1l zi(7FXg=b&_>|h(H>%s$Uc&GQmg1y8j)0?$^({FkAvdE%Ldr+gl zj11odpttP^v;RN-_~YNW4?EwQmAD_ZyCi+DreM%iQZ`{3;q>!1`c5buMk3%Qb;b~Y z8w7zb_h33Yprlv)KIY|u8$~v|2eJOp1Xl~3{62U9Ji@v)f8u}5KeRu6XfH6<&jxFl z_c`p#<~`bm9W2IG!9bY65`ekKbl~{#&>eqWI8!CM!BdA$~LE*G!FM;I@7I9RHX)9|YY~79X~beOt!Ya+6r7=I>Fp zpL8`A=uG0nbA19A@GU%uI6_vIX796ZJm{UDe}39@Ky9`kLVweYZ+yU4Mjy67=(ZX0 zkHNp){m~~B0d`!fY~6?XS3pBy!X;zC`>JewN98}KCgq62A5jmdLyinxRxm-bMR}n5 zy*34-tk0a#i5%cRu|HD-S0^y-2m$tjAV&L7MBRL0V$cK0`_U3wSrJ2+h++eoHY+V8 z;IO`xQtIxsGKis&$R+kt#!zi~t$uJeut z0;NrrKp4GKNnLjc8vp&}M>L z%SMuoNqG`2I&yc+06c`oH{JYyAFY2`@^42l8&As@B5$yKfHkWy_&vcp^g&dBsVAE5 z8}c&&$c#eX|La_fq2wO?ejJQMqC<$_!2uwY`v?-g_sWlo6sAGJ5QF6_hA((mz+W~X z`io#1iY;ah0qJ@JlQ=z{=>~8ivJ8=7dVwzd8ku45M}wDl zoUiQYfQnwh-;vWT8Gcz-m8)7>$XC@XnZJ&45}c1Pj*En!(-Bu{`SAk!#Kvm%6U)y{ zcr)k&0MzR3@ zA+Md#qgD{1!vvydNIO?Z&=xCz0TKa+2iMlh4cJwEqT=X@8;6c=$BK^@)IjqEP;t?h z^ouv3@rT?W>~9ufq5ee@fO8-R;0M>Q-@eYSnC)6cVe5mdR|f_L7!}aQvVHn5b~nrO zYMW<~XJ|+i{x@s0JThtQH2`RH*$&Qds8ht)86YAzd;srEu(4$Hpr|-TKm@5bD^b~# zu7r5v40+iVpEg1h=0N)1bfMSE&6T4DMLfVcuZS7lN^oRdI;k3fX|t0D%o?6X{$)|W ztAE&k#_*NZYZQRZ;#G+5fA+_406&hJ0O!7zS9?n`U~+;?0kUQw7(uBpsHJmNV}}@s z1ITEAdMNk-Tt*q#M**Vv`z2N|K8%M+b1mip1f%L5R(PxYPy-F8g!zP6ErD8>$r^Dm zZ&Q6XE?KiPiO+h4BU2DS%zE{mC?GLQ;+__q?3>qSPC!@A{}fwD{eANJsqOf4QHTQoX?cmuUX!Xchh|EG$06m}>^&P4Xh3%Tap@!>KMGtx zh)nuGR)|2rIUlR+xgJWrlpZjDX#`gNDfHRQdsw^00YoVfGq%x~54G;cbFRxN_c!r( zk=C#^{)7UW9k>%3;Qd_GfA|99hPTafa8Shd6C>SLCe=Z}{@LRgNO@E!*M-4tluOLJ zB@QbdI8C9h3=n{_{~Zkd+KG(Z| z%FVe!2MxZq0K>Lc6d)heG+A5QL+amyClL7uNWcL!N&IQ7K~TIOvK9frJ_JUt|LXN$ zJxA{bb;OPxS07$|IBNJL?|kEOp%B$0;&20f%rp=naJ%N`>`wc9>PnBQ zV3Lo`{($0OecC_0tSaRz9q$$W##|+Du-lwcfpCJ8@|4VXjkE|LPVVNy$tB-Iqam%c zQU;aXd&AWh4}nNu#No2_1^uyjW&E;10BJlJDSf8)Y2F{AzOUgnuz%bet^5T0aDfO0 zG++V=BL-Un5}W|Am_FaNpOPSZ9nWhd0bJ))cTm8?J>SP89RZpE7{2D8h=3Y<5QFUs zlK5-BquOawU_m53XgnHlD26BUWkl{-cd%B-i?U8&ssFC@ShxtCJ|M(^eLv@HAr0_q z*|&n1iu-6b~>q z_Hx4T`Q^k*q5^Y;?ruVO$Myt&$CTcAp>H1p_OrOC!0!|{#CS>_`4{?}eBzWVF}!1f z0FZe?2c^VPB`*mAn7eDf;0jz(bUY0kchXQ4`6361Sz%mFIDk6gf&N1XOp!v&4i48q zI1^w4bVQj7!ATnN=W!-6L5Pr~$RvI&ew)LAXW#}cL>`-RnCIFs34QgU510~m?Zl;< z9hHSpt4^Fap#&Hm0H5DD_yP1FrixubP2^(Bm9rO{N^1LY08|HY0gMnh(@HU>ps8kY z@%HVxd*(?46w*_L8(f4Bw3008@LpF4Wys4(EZEv=V2ZVeCv z4DH`f6F6%RIKb)6*%;bjlqt&_i;2OyTHb*rXA1Kf@l5JqS~4W4b7SV5%X`%C)xr%E z-nmuUKhH-Ops*63M;D@<0NGSh3;l@{7z?NFfN%i5Ji27e+T=yb0}HmtEG|d@rszT3 zw{b6qc!&Y$0`vwx457bl3&5}-q4_55@O-TY!tY1fFJyjwO9+YvOMq&iAx3C`8gTFc zrQZbbJFQ$JH<(bhSfBD1k{~7kIxx!u;mo+hh*JGbkOTYnQb-#AFo{Lm^~;{in9LAu z09@dZh5wp4Cqevoy^nLfK#~gaB|;2F|Kfl~3mM>-vHqgHzyVFd40?gjF#lejWg}?g zDE|o3ktv?VVBc;GmpYU${BqrHeE)o11I44(6RoYSJ1Q&KYyW3kN+?1-eq4gE$HE+X zf2-XN0jCj$@Sg=6>rfTMVnFxa!yH(#M@F!GsC-NagTcOv95A*E2N3j3w$EO{d$su0 z2TY@Wl;XstIN&Rkc-ztB2W^a;frQ`sLX_b62*(IuuzY}~YzD5~#}v>Y1BwJ$;WD?;H1@T>+C&0khCaD1P3fvE{f&q^|&dL-}1!n@2rDg%8yw#K= z=@j!G^8u=x$B+xi^%TA@;*P=1{Y$ife8_{yCYTtJz8 zK5#wu77l47D6Xg|x?a$PozE`L^w0DlKPpPd;IJ~>Ky6KRf9+y_4Jbe{;z5iSqxeVu zZ!a;Aizx(dzxv?T!=ZtJy21J84SS$@;{l}aB#EjRAgs9)a0ZZ^#gdx|5A+bKeWr%8 zSbAcUva5(Bt^BOqd&Iuv1NvF%G!qgRr?%xlyB7Bg2M|>NalRjza@d=H>cqz~g>1bs zCQn_tuNPo`4hPFveVA`H-8(D!zmf#xvXDL|{xb>&l0rmS4}^e+(0`Z#BpqROL&8}A z1PBk}Ba;4akOp{b3@j$a=Te$fB;L=dE(zAZy#O}<0uZPHps=cy49(r&O!~j>F2K{o zt7IQ@Lj3(Ls(8~Syw5W3Nw|i<_kNi5PaTDWMjA@Z{&$1`K(q#=HXe;URsFrW#q|`} zd;K$`04u+Gh|_X>DFw7W`}1dXh*9cIi@z-$YA@Ffo`T60;vbc*6%~~okVpzywNSae z0;3Ho|Kph_Nu_9GqgT z0fHw06R;H}EllBwoIvQ$4lXIHS+!z4i&??Rk(U#r%THU7$Zje&p8dg=+HsC0I#3Bev}Os0ZK00A3V5q>+040`v>eo+xTTK%>fpi zGsz17Hlx6FIu(rqWv0p;P_$1bxGM31f?rO#D3If=l>ELr{naBvZJy0Y#3rf90q_7q z06@4Ya@>uLfgBOjwM9m4Z5xFgAnZD)oVFf7;SFpvlKuj*A2^^MJ!yXX65IgA!TS%Nb+uvju^FY| zCNBJn17%_VT^9atMf{7u*NO^~$m84I0{|?8&EK>1?USAvw7r(-?58$l>c&hgg;ysg>DrUg2L_-hFez@2=1 zYK8ED4>7-*{|W#lCLaMtU~(96Wc5OP1_}`e z`~VD!^NSGhmOtYb?j-U8b;%4a5F?OQs8n$~Kh{avzO!)rOC9GcX|6s|*m3jjrH6|#nH~zG+jB% zT;LC!Bmg)lyuYvTXu)jtZ6baEg4u1%3?)M7VK*~5Qziz$ts4EXbKn5QhZreRY%}n= zb4OcSA#zp%12$~G`tadEc^&`pJ0CrQqJzCj0(K)uH4=3>$tpu)(pfFa=-{2OY_|}d z%@6=_;}|ag)mKc!&E!Lz80b@q2gWQK?7$J>0%Jma*8eH~**Z()Z^a>~08Dv3U703WtJsSt!Yvn&1`>7XjOCo^LCZx9s z+ke&WvxkJoqJ$>TE|Py9*`Gk8a+E7A55#C7dYJeJNXozIm-Kw25rn$unXqDN5e&Vh zI8C!cle4vg63c!T0fZySB7meKETkHDJEVEim@KW|q47uBrfG8;{C_h5sNQ;jk3Tj7 zARf^C(<>kxMIk533E6v^kJLQ_{cE1@Do6Zxdn*k9=Q}DpDq1^EeEvbBgK(RW_Mdt3 z?USX)zx(Tt$Cv8o7IMr01+FI(I5uKlO#1HP&W*X{q`^U zfs`FnE!>u+V@d=$xGXvs6&8x0>@dtAYNGk!zKdnGMM$cjKYxks>uc9KF5OgR;b3D8 z8jFPOMTh7I1^_VsZ|W;KTQyE_4~J+QtNXdxZAS}<07wjWZBy&V456%Xwy6oz@0o0> z@tN6b0)7}k(g1KZJlJ>UI(fjEefxOWvE#vZLV&B62L|u}2zT%N1k9JUpXFE3%6v8B zgT-G;l9>r&TlS}!E$$&2DVoLEE9SU3bZSxgjhe!~i7#(kQb* zCxHF9CK^ABmPMcu(Sp`yI38FaQGm88izj0KEb0dYOf|q{{Mmefk)HeVq9$0#8-Hjf`W{0#j}wFD1nlu1^KUrg$vZBwkVHL_}78rt9iL40JQ-f zDiVUdb``K;xXsNh<3hVBfIBTf6Tske*armv8hY@7vQ3m%?~(C_+kUxj9@uxj?()^` zX#bG<-#BsYTE(@m&leutMQ{oUb@uj?Ck*(QnR)WB|FQTOjqka+T%HwrN(&{o$JAN( zwRy#UPC6-sl{-xfG)9=BUWie*TjI0n?|`iXR@r&ANFLN(nLyl)5vJ6@mI%Z=QGx~x z9vkDM<0c7lFQGRbz*$QmW^OjG#BT=d`H;ISxP?ERK%AQ=xX)-nDIwrF1Vt*n+U^*hF5b2#PXhH%$No|oHW_ykW1AoqlK$$}!jQlVI!jGS* zX#M(YP=K$moxjHI(vFUO)!S-e2o>$yLN`!LNtty3b-fw-eX#|UuZfPK>SCZhK*0BQ z5#?VmLPs1G!LqWNYT|$<8V*}103a!B5#U^Lb$@a7K4pZ8hykjrIR_1ZN7S){8~`NX z>cgSIQ_TqPefdfA>95Sp0|F4Y3LrlfkmUGA^J-G1PBj2gs94#fdLRIx2jDSQzabcY z?x7Oii?|@9ZX9VHEk-dmL&z^{lG#Ji|EFn0wtJ3ushpY02Psevy?t#zQg8u1Oc@If z*$+j*(vqJYk|L11=nqs6lxRWy0EIqcsgJe*)BQA2j4dqRvux#mOQ=sbkjeh@!vh6s z^f9G8EBxE_^FEs|r;ou){z~|^82$SL-3g%60LLvg1cdd+?7$?*WLdxfo(w6p{oG{y zy79;eD-H6M(P5agW@dQlhw=7T^Z9Q1O#k1Is37W<19$B>~75fxG2L_}Mmj#pv z8}OWVfShA+x^t@p7wYm0HS%uR09G2*{>SWy8x9!7W26ofNeI3mzj|Vp08&}Ni=1+9 zk^prmxrpQhG0WC2CBHLwE@_3xrFxDg2O=~+;~ zlCqZJ&ZkcsKRA04JMZ7tQoC(mK@nzMNU&hX@UF5l!u}=@0lfeWfOEw~)c(#@Lj(f- zyI#aeKiNPL+m=HWXW(?u6NVEH3g_L_#F)OHF4u$9Rb#XsmuGp`GmHf z&q|(GW=~*18W8ps{9HHF{;cGma(?YU^8=c^M_mIaKO@W^DL)B;Nq&fjq|%=i1CoFI z0N4Ej0mulzTDz{`HB$Vo%^Th*AmHb+m_QJdf)cF850Qz20Lpz4JO!p{Qw6b(K2T?< zQwys%6iB;B|4nt+k`%agR?)vSvNP)t(6$r1$BvBfyQzriOHB~MBilaoL)_mBJL`FG z7U5@UKsu)`fD2Ij>;HEG`5{{Oa2-=%fWQU_;37WSy*g$9fr+(a`l<=+J;?+wmU3mg zgb;`z1QW5}===-GK)%v60C~Y4bpc$_xH)mv=N5GUb~*vs|E~QafE;lEq6gOXOlTZ_ zX7qxCQ?{(#00*{$r~nuUKRKYJCCv~dRVc-`DQK$F$wa=hzNzM5(TT6W#`M4btG~K& zhG(_!+{21VqF@TaV*~-pM)% z?Jou-{1SDMN2Uobj}HIJ8t5_o@IE1%p(DYnqGq522=F~h^oH)1`xOIAIDkhNeb3mv zA`2QM_?Z4dA-tp$?m{1GVt$O1!Q~2bH9lF|K$Zkx)87m4WB2b8){}|64+oIEKM(*N zK(_Hnu%Eq6{z>^K8wEs9Q24r#22o?tx~CaHwvK0mnSy_ZUE*4!E09NSKGHFSrk9t4 z`s?0<_rwy2_#Lj6R<8ZuzmEqH!i8>_m2rX_0RIU2AE&ei`8VC&XJDbizrRwq+Xl`f z`>&e*@2Dheqylh_O7-WT?gD3Gv6)=_-P)Skni+6_$B%0#d+-C}%F*R>aDBCMj8X$% zha)?7lEu(_yS~7JIXK?jlPQS7fcp7`(G_BQ&~$Qoa%f&

_uWRf<0nZVZmde`0Kk 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 KMNUMnLSTX%jO#rB literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/work.png b/dot-line-system/public/images/work.png new file mode 100644 index 0000000000000000000000000000000000000000..8943c4f1693a29c6bf6a229681c15faec67b16da GIT binary patch literal 26783 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 DF8tPA literal 0 HcmV?d00001 diff --git a/dot-line-system/public/vite.svg b/dot-line-system/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/dot-line-system/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dot-line-system/readme.md b/dot-line-system/readme.md new file mode 100644 index 0000000..2d357b2 --- /dev/null +++ b/dot-line-system/readme.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/src/ConnectedDotsVisualization.ts b/dot-line-system/src/ConnectedDotsVisualization.ts new file mode 100644 index 0000000..090a043 --- /dev/null +++ b/dot-line-system/src/ConnectedDotsVisualization.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 || 80; + 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 = ` + + // `; + 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 / 1.625; + // Calculate raw Y position + // height of the amplitude + const rawY = centerY - (value / 3) * ((this.config.height / 2) * 0.7); + // 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 * 500; // Scale tension for Bezier curve Rundung Kurve + // 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 - 10; // 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", "0"); // 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 + // Create a container div + const imageContainer = document.createElement("div"); + imageContainer.classList.add("image_container"); // Add image_container class + + // Define a variable for handling case with or without link + let imgWrapper: HTMLElement; + + // if (dot.imageUrl) { + if (dot.link) { + const link = document.createElement("a"); + link.href = dot.link; + link.target = "_self"; // Opens in the same window + + const imgElement = document.createElement("img"); + imgElement.src = dot.imageUrl; + imgElement.classList.add("tooltip-image"); + + // Append the image element to the link + link.appendChild(imgElement); + imgWrapper = link; // Use the link as the wrapper + + // Add the event listener to the link + link.addEventListener("click", () => { + if (dot.link) { + window.location.href = dot.link; + } else { + console.error("Dot has no link"); + throw new Error("Dot has no link"); + } + }); + } else { + const img = document.createElement("img"); + img.src = dot.imageUrl; + img.classList.add("tooltip-image"); + imgWrapper = img; // Use the image directly as the wrapper + } + // } else { + // console.error("Dot has no image URL"); + // throw new Error("Dot has no image URL"); + // } + + // Append imageWrapper to the container + imageContainer.appendChild(imgWrapper); + + + // Append the image container to the main div + div.appendChild(imageContainer); + + const arrow = document.createElement("div"); + + arrow.classList.add("tooltip-arrow"); + + div.appendChild(arrow); // Append the arrow to the tooltip-content div + + contentContainer.appendChild(div); + tooltip.appendChild(contentContainer); + + 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/src/main.ts b/dot-line-system/src/main.ts new file mode 100644 index 0000000..cac6f1b --- /dev/null +++ b/dot-line-system/src/main.ts @@ -0,0 +1,320 @@ +import { + ConnectedDotsVisualization, + type DotConfig, +} from "./ConnectedDotsVisualization"; + +import '../src/style.css'; + +/* + * If you are using a bundler like Vite, Webpack, or similar, you need to ensure that the `src/images` folder is included in your build output. + * + * For Vite: + * - Place your images in the `public/images` directory instead of `src/images`. + * - Reference them as `/images/filename.png` in your code. + * + * For Webpack: + * - Use `require` or `import` for images, or configure `copy-webpack-plugin` to copy the `src/images` folder to your output directory. + * + * For static hosting (e.g., GitHub Pages, Netlify): + * - Make sure the images are in the output directory (e.g., `dist/images`) after build. + * + * Example for Vite: + * Move images to `public/images` and update imageUrl paths to `/images/filename.png`. + */ + +// Sample dot configurations +const sampleDots: DotConfig[] = [ + { + id: 1, + value: -1.8, + x: -2, + imageUrl: + "/images/0_3.png", + title: "Beginn des neuen Abenteuers", + description: "01.10.2024", + link: "/page1", + }, + { + id: 2, + value: 1.2, + x: 0, + imageUrl: + "/images/0_2.png", + title: "Omas Annis Geburtstag", + description: "02.10.2024", + link: "/page2", + }, + { + id: 3, + value: -0.6, + x: 2, + imageUrl: + "/images/disco.png", + title: "Konzertbesuch mit Freunden", + description: "03.10.2024", + link: "/page3", + }, + { + id: 4, + value:3, + x: 4, + imageUrl: + "/images/pferd.png", + title: "Wanderreiten in den Bergen", + description: "04.10.2024", + link: "/page4", + }, + { + id: 5, + value: 1, + x: 6, + imageUrl: + "/images/gpt.png", + title: "Ruhiger Tag zu Hause", + description: "05.10.2024", + link: "/page5", + }, + { + id: 6, + value: -3, + x: 8, + imageUrl: + "/images/oma.png", + title: "Oma Erna verstorben", + description: "06.10.2024", + link: "/page6", + }, + { + id: 7, + value: 1.5, + x: 10, + imageUrl: + "/images/see.png", + title: "Erholungsausflug zum See", + description: "07.10.2024", + link: "/page7", + }, + { + id: 8, + value: 0, + x: 12, + imageUrl: + "/images/feier.png", + title: "Kleine Wochenendsfeier", + description: "08.10.2024", + link: "/page8", + }, + { + id: 9, + value: 3, + x: 14, + imageUrl: + "/images/hochzeit.png", + title: "Hochzeit von Cousine Tatjana", + description: "09.10.2024", + link: "/page9", + }, + { + id: 10, + value: 1, + x: 16, + imageUrl: + "/images/work.png", + title: "Erster Tag im neuen Job", + description: "10.10.2024", + link: "/page10", + }, + { + id: 11, + value: -1.2, + x: 18, + imageUrl: + "/images/klasse.png", + title: "Klassentreffen nach vielen Jahren", + description: "11.10.2024", + link: "/page11", + }, + { + id: 12, + value: -0.6, + x: 20, + imageUrl: + "/images/familie.png", + title: "Familienabendessen", + description: "12.10.2024", + link: "/page12", + }, + { + id: 13, + value: 2.7, + x: 22, + imageUrl: + "/images/kinobesuch.png", + title: "Kinobesuch mit der ganzen Familie", + description: "13.10.2024", + link: "/page13", + }, + { + id: 14, + value: 0, + x: 24, + imageUrl: + "/images/entspannung.png", + title: "Entspannung", + description: "14.10.2024", + link: "/page14", + }, + { + id: 15, + value: -2.9, + x: 26, + imageUrl: "/images/sonntag.png", + title: "Geruhsamer Sonntag", + description: "15.10.2024", + link: "/page15", + }, + { + id: 16, + value: 1.5, + x: 28, + imageUrl: + "/images/kindergeburtstag.png", + title: "Kindergeburtstag", + description: "16.10.2024", + link: "/page16", + }, + { + id: 17, + value: 0, + x: 30, + imageUrl: + "/images/familie2.png", + title: "Spaziergang mit der Familie", + description: "17.10.2024", + link: "/page17", + }, + { + id: 18, + value: 2.1, + x: 32, + imageUrl: + "/images/grosseltern.png", + title: "Familienfeier bei den Großeltern", + description: "18.10.2024", + link: "/page18", + }, +]; + +// Wait for DOM to be fully loaded +document.addEventListener("DOMContentLoaded", () => { + // Initialize the visualization with the sample dots + const visualization = new ConnectedDotsVisualization( + "scroll-container", + sampleDots, + { + // Optional custom configuration + dotRadius: 8, + // tooltipWidth: 100, + // tooltipHeight: 100, + } + ); + + // Handle window resize + window.addEventListener("resize", () => { + visualization.resize(); + }); + + // Example of updating dots dynamically (if needed) + /* + const updateButton = document.createElement('button'); + updateButton.textContent = 'Update Data'; + updateButton.classList.add('button'); + updateButton.style.marginTop = '10px'; + document.body.appendChild(updateButton); + + updateButton.addEventListener('click', () => { + // Generate some new random data with image tooltips + const newDots: DotConfig[] = Array.from({ length: 9 }, (_, i) => ({ + id: i + 1, + value: Math.random() * 6 - 3, // Random value between -3 and 3 + x: i - 3, + imageUrl: `https://picsum.photos/200/150?random=${i+10}`, + title: `Point ${i+1}`, + description: `This is data point ${i+1} with value ${(Math.random() * 6 - 3).toFixed(1)}` + })); + visualization.updateDots(newDots); + }); + */ + + const scrollContainer = document.querySelector(".scroll-container") as HTMLElement; + + let isDown = false; + let startX: number; + let scrollLeft: number; + + + // Mouse events + scrollContainer.addEventListener("mousedown", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + + // Remove smooth scrolling while dragging + scrollContainer.classList.remove("smooth-scroll"); + }); + + scrollContainer.addEventListener("mouseleave", () => { + if (!isDown) return; + isDown = false; + scrollContainer.classList.remove("active"); + + // Add smooth scrolling after dragging + scrollContainer.classList.add("smooth-scroll"); + }); + + scrollContainer.addEventListener("mouseup", () => { + if (!isDown) return; + isDown = false; + scrollContainer.classList.remove("active"); + + // Add smooth scrolling after dragging + scrollContainer.classList.add("smooth-scroll"); + }); + + scrollContainer.addEventListener("mousemove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; // Multiply by a number to speed up scrolling + scrollContainer.scrollLeft = scrollLeft - walk; + }); + + // Touch events + scrollContainer.addEventListener("touchstart", (e) => { + isDown = true; + scrollContainer.classList.add("active"); + startX = e.touches[0].pageX - scrollContainer.offsetLeft; + scrollLeft = scrollContainer.scrollLeft; + + // Remove smooth scrolling while dragging + scrollContainer.classList.remove("smooth-scroll"); + }); + + scrollContainer.addEventListener("touchend", () => { + if (!isDown) return; + isDown = false; + scrollContainer.classList.remove("active"); + + // Add smooth scrolling after dragging + scrollContainer.classList.add("smooth-scroll"); + }); + + scrollContainer.addEventListener("touchmove", (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.touches[0].pageX - scrollContainer.offsetLeft; + const walk = (x - startX) * 3; + scrollContainer.scrollLeft = scrollLeft - walk; + }); +}); diff --git a/dot-line-system/src/style.css b/dot-line-system/src/style.css new file mode 100644 index 0000000..9d13415 --- /dev/null +++ b/dot-line-system/src/style.css @@ -0,0 +1,203 @@ + body { + font-family: "Barlow Condensed", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + min-height: 100vh; + margin: 0; + padding: 0; + box-sizing: border-box; + } + + .controls { + display: flex; + justify-content: space-between; + width: 100%; + max-width: 500px; + margin-bottom: 10px; + } + + .button { + padding: 6px 12px; + background-color: #4f46e5; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + } + + .visualization-container { + position: absolute; + width: 100vw; + height: 100vh; + left: 0; + right: 0; + margin-left: calc(-50vw + 50%); + } + + .gradient-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + min-width: 100%; + z-index: -1; + background: linear-gradient(45deg, #8634f9, #ffab1a, #ff2fa2); + background-size: 200% 200%; + animation: gradientAnimation 20s ease infinite; + } + + @keyframes gradientAnimation { + 0% { + background-position: 0% 0%; + } + + 25% { + background-position: 100% 0%; + } + + 50% { + background-position: 100% 100%; + } + + 75% { + background-position: 0% 100%; + } + + 100% { + background-position: 0% 0%; + } + } + + .median { + position: fixed; + top: 61.5%; + left: 0; + height: 1px; + border-top: 1px dashed rgba(255, 255, 255, 0.2); + width: 100%; + z-index: -1; + /* background-color: rgba(255, 255, 255, 0.2); */ + } + + .scroll-container { + position: relative; + height: 100%; + overflow-x: hidden; + overflow-y: hidden; + scroll-behavior: smooth; + box-sizing: border-box; + } + + .scroll-container:active { + cursor: grabbing; + /* Change cursor when active */ + } + + .smooth-scroll { + transition: scroll-left 0.5s ease-out; /* Add easing on scroll-left */ +} + + .spacer { + height: 100vh; + } + + .dot-tooltip .tooltip-background { + fill: rgba(0, 0, 0, 0.0); + } + + .dot-tooltip .tooltip-content { + display: flex; + flex-direction: column; + justify-content: center; + /* Center vertically */ + align-items: center; + /* Center horizontally */ + width: 100%; + height: 100%; + color: white; + /* Text color */ + } + + .dot-tooltip .image_container { + margin-top: 8px; + /* box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); */ + box-shadow: 0 0 20px 0 rgba(255, 255, 255, 0.25); + transition: box-shadow 0.25s ease-in-out; + } + + .dot-tooltip .image_container:hover { + + box-shadow: 0 0 30px 0 rgba(255, 255, 255, 0.8); + /* box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.5); */ + transition: box-shadow 0.25s ease-in-out; + + } + + .dot-tooltip .tooltip-image { + width: 100%; + height: auto; + display: block; + pointer-events: auto; + + } + + .dot-tooltip .tooltip-title { + font-size: 14px; + font-weight: 400; + margin-bottom: 2px; + text-align: center; + text-wrap: balance; + hyphens: auto; + line-height: 1.1; + } + + .dot-tooltip .tooltip-description { + font-size: 12px; + font-weight: 300; + } + + .dot-tooltip .image_container { + width: 80px; + height: 80px; + overflow: hidden; + border-radius: 50%; + border: 2px solid white; + display: flex; + justify-content: center; + } + + .dot-tooltip .tooltip-arrow { + width: 1px; + height: 30px; + background: linear-gradient(to bottom, transparent, rgba(255, 255, 255, 0.5), transparent); + } + + .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; + } \ No newline at end of file diff --git a/dot-line-system/src/typescript.svg b/dot-line-system/src/typescript.svg new file mode 100644 index 0000000..d91c910 --- /dev/null +++ b/dot-line-system/src/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dot-line-system/src/vite-env.d.ts b/dot-line-system/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/dot-line-system/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dot-line-system/tsconfig.json b/dot-line-system/tsconfig.json new file mode 100644 index 0000000..a4883f2 --- /dev/null +++ b/dot-line-system/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/README.md b/frontend/README.md index 1c77bf6..83105c4 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -10,6 +10,13 @@ yarn npm install ``` +## erste installation +npm i -g @quasar/cli +npm init quasar@latest + +## Now, do you want to be able to run Quasar CLI commands directly (eg. $ quasar dev/build) + npm i -g @quasar/cli + ### Start the app in development mode (hot-code reloading, error reporting, etc.) ```bash diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3db3b25..801f1d6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -628,19 +628,22 @@ "license": "MIT" }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -713,9 +716,9 @@ "license": "MIT" }, "node_modules/@eslint/js": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", - "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, "license": "MIT", "engines": { @@ -736,13 +739,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -1981,9 +1984,7 @@ "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==", "dev": true, "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.29" - }, + "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" }, @@ -2183,6 +2184,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2569,6 +2571,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.2", "caniuse-lite": "^1.0.30001741", @@ -3474,20 +3477,21 @@ } }, "node_modules/eslint": { - "version": "9.35.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", - "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.35.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3540,6 +3544,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -5241,6 +5246,7 @@ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -5442,6 +5448,7 @@ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz", "integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.2" }, @@ -5489,6 +5496,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5535,6 +5543,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5620,6 +5629,7 @@ "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.18.2.tgz", "integrity": "sha512-SeSAamH4vgYH9alLTdVL2o1fTTwz7VZnS2+gvIwt6qsH3ndrn/tQW64sWE78VSvrHlWINYbXESVF/cvWEuTYxg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 10.18.1", "npm": ">= 6.13.4", @@ -5817,6 +5827,7 @@ "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6991,6 +7002,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7206,18 +7218,19 @@ } }, "node_modules/vite": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", - "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.0.tgz", + "integrity": "sha512-oLnWs9Hak/LOlKjeSpOwD6JMks8BeICEdYMJBf6P4Lac/pO9tKiv/XhXnAM7nNfSkZahjlCZu9sS50zL8fSnsw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -7438,6 +7451,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7457,6 +7471,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.21.tgz", "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.21", "@vue/compiler-sfc": "3.5.21", diff --git a/frontend/quasar.config.js b/frontend/quasar.config.js index b2a85eb..e26694a 100644 --- a/frontend/quasar.config.js +++ b/frontend/quasar.config.js @@ -72,7 +72,8 @@ export default defineConfig((/* ctx */) => { // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#devserver devServer: { // https: true, - open: true // opens browser window automatically + open: true, // opens browser window automatically + allowedHosts: ['app.thats-me.test'] }, // https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework diff --git a/thats-me.test.code-workspace b/thats-me.test.code-workspace index 876a149..665b01c 100644 --- a/thats-me.test.code-workspace +++ b/thats-me.test.code-workspace @@ -1,8 +1,31 @@ { + "name": "Thats Me [Dev Container]", "folders": [ { + "name": "🚀 Backend (Laravel)", + "path": "./backend" + }, + { + "name": "⚡ Frontend (Quasar)", + "path": "./frontend" + }, + { + "name": "📦 Projekt Root", "path": "." } ], - "settings": {} + "settings": { + "files.associations": { + "*.blade.php": "blade" + } + }, + "extensions": { + "recommendations": [ + "bmewburn.vscode-intelephense-client", + "onecentlin.laravel-blade", + "bradlc.vscode-tailwindcss", + "dbaeumer.vscode-eslint", + "Vue.volar" + ] + } } \ No newline at end of file

VP$gQfttN>RXj@}vZGNFGJS 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 U{r|A+ga7~l07*qoM6N<$g7f#KQ~&?~ literal 0 HcmV?d00001 diff --git a/dot-line-system/public/images/pferd.png b/dot-line-system/public/images/pferd.png new file mode 100644 index 0000000000000000000000000000000000000000..5992d1995240edeb32f27771875615a104739ac7 GIT binary patch literal 280884 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%0AgS=8?+VXGAYpc=ch750Z{yTTOcVy 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%0AV&+gGV-Ava43FJ^`~i+3wQvr3N(Qf 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