diff --git a/README.md b/README.md index b31a328..150e3a4 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,13 @@ Visit : https://tamimehsan.github.io/AlgorithmVisualizer/ ![Visitor Count](https://visitor-badge.laobi.icu/badge?page_id=TamimEhsan.AlgorithmVisualizer) -Explore more than 24 algorithms with step by step visualization simplifying the learning process and making it more engaging for a better understanding. +Explore more than 30 algorithms with step by step visualization simplifying the learning process and making it more engaging for a better understanding. -So far there are 6 segments +So far there are these segments - Pathfinder +- Graph Traversal (BFS / DFS) +- Shortest Path +- Minimum Spanning Tree - Prime Numbers - Sorting Algorithms - N Queen @@ -19,8 +22,10 @@ So far there are 6 segments - Binary Search Game - Recursion Tree - Turing Machine +- Game of Life +- Linked List -I have implemented a total of `24 algorithms` so far. And will try to add more later. +I have implemented a total of `30+ algorithms` so far. And will try to add more later. ## Algorithms implemented @@ -30,6 +35,17 @@ I have implemented a total of `24 algorithms` so far. And will try to add more l - Dijkstra - A star - Recursive Maze Creation +- Data Structures + - Linked List (insert, delete, search, reverse — singly & doubly) +- Graph Traversal + - BFS + - DFS +- Single Source Shortest Path + - Dijkstra + - Bellman-Ford +- Minimum Spanning Tree + - Kruskal + - Prim - Sorting - Bubble sort - Selection sort @@ -37,8 +53,9 @@ I have implemented a total of `24 algorithms` so far. And will try to add more l - Heap sort - Merge sort - Quick sort -- Sieve of Eratosthenes -- Archimedes Spiral +- Prime Numbers + - Sieve or Eratosthenes + - Archimedes Spiral - N Queen Backtracking - Graham Scan for Convex Hull - Binary Search @@ -48,7 +65,6 @@ I have implemented a total of `24 algorithms` so far. And will try to add more l - Derangement - Fast Exponentiation - Stirling Number of Second Kind - - Turing Machine - Bitwise NOT - Increment one @@ -64,6 +80,9 @@ I am not sure if anyone would like to contribute to this project or not. But any - Commit 16: Added Flip Move animation to inplace sorting components - Commit 20: Added Tree Structure - 13 Dec 2024: Release v2.0.0: Migrate the project from legacy project to next js with shadcn ui [#3](https://github.com/TamimEhsan/AlgorithmVisualizer/pull/3) +- Jun 2026: Added Linked List visualizer (singly & doubly) with staged insert/delete animations +- Jun 2026: Added interactive Graph Traversal (BFS / DFS) built on React Flow +- Jun 2026: Added Shortest Path (Dijkstra / Bellman-Ford) and Minimum Spanning Tree (Kruskal / Prim) on a shared, reusable graph workspace ### Acknowledgement diff --git a/package-lock.json b/package-lock.json index e440f64..7592adb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@testing-library/jest-dom": "^5.11.5", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "@xyflow/react": "^12.11.0", "autoprefixer": "^10.4.20", "bootstrap": "^5.3.3", "class-variance-authority": "^0.7.1", @@ -261,23 +262,21 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.0.tgz", - "integrity": "sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.7.tgz", + "integrity": "sha512-ppj9ouYku+RX0ljtgZd+KMO5mkM2bCqg8H2PYAFWnLsHEIKIdRojqbJ2i3eVHrisuxy7nOFCmngTDdWtUCdXUQ==", + "license": "MIT", "dependencies": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" + "core-js-pure": "^3.48.0" }, "engines": { "node": ">=6.9.0" @@ -673,10 +672,20 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -692,13 +701,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -714,13 +723,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -734,9 +743,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -750,9 +759,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -766,9 +775,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -781,10 +790,42 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -798,9 +839,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -814,9 +855,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -830,9 +871,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -846,9 +887,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -864,13 +905,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -886,13 +927,57 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -908,13 +993,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -930,13 +1015,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -952,13 +1037,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -974,20 +1059,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.2.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -996,10 +1081,29 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -1016,9 +1120,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -1234,9 +1338,9 @@ } }, "node_modules/@next/env": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.0.tgz", - "integrity": "sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.19.tgz", + "integrity": "sha512-sWWluFvcv5v3Fxznmf2ZfjyoVQt/64oCnYqS90inQWGzMPK1VjvekPiz3OPHKmFT30EnHrjlbyaHLt3M0vWabw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1267,9 +1371,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.0.tgz", - "integrity": "sha512-ZU8d7xxpX14uIaFC3nsr4L++5ZS/AkWDm1PzPO6gD9xWhFkOj2hzSbSIxoncsnlJXB1CbLOfGVN4Zk9tg83PUw==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.19.tgz", + "integrity": "sha512-jx9wWlTKueHKPvVOndyr7WuaevWCkuYqsQ8gC0TMPKAVWG3MhcdMrjfo9tvIZNXd0QOUYXXvAcZ325y8Uq7uzg==", "cpu": [ "arm64" ], @@ -1283,9 +1387,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.0.tgz", - "integrity": "sha512-DQ3RiUoW2XC9FcSM4ffpfndq1EsLV0fj0/UY33i7eklW5akPUCo6OX2qkcLXZ3jyPdo4sf2flwAED3AAq3Om2Q==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.19.tgz", + "integrity": "sha512-291KFcsIQ3OenRdiUDFOR6W3wezzH4auENXm1gbm1Bjd4ANMMRgxPrWTUztQN43BnVoVuMnHCrLeECIMwgFKbA==", "cpu": [ "x64" ], @@ -1299,9 +1403,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.0.tgz", - "integrity": "sha512-M+vhTovRS2F//LMx9KtxbkWk627l5Q7AqXWWWrfIzNIaUFiz2/NkOFkxCFyNyGACi5YbA8aekzCLtbDyfF/v5Q==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.19.tgz", + "integrity": "sha512-WeH+nelQyyMeE2f8FxBRZNrGipya5zHZV2vjzfCOAYyiI6am+NbnWAAldOBFQBB2w0DjJcsvrKqoFT2b7+5YoA==", "cpu": [ "arm64" ], @@ -1315,9 +1419,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.0.tgz", - "integrity": "sha512-Qn6vOuwaTCx3pNwygpSGtdIu0TfS1KiaYLYXLH5zq1scoTXdwYfdZtwvJTpB1WrLgiQE2Ne2kt8MZok3HlFqmg==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.19.tgz", + "integrity": "sha512-5xTOE0lDlDCSSfp+BAif7j17VRRCjWp//ZPZy6NI0QpdrhxtQnsZguSx0xAAZ0c9XZLrLLwCe/XVe5YPrRilKw==", "cpu": [ "arm64" ], @@ -1331,9 +1435,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.0.tgz", - "integrity": "sha512-yeNh9ofMqzOZ5yTOk+2rwncBzucc6a1lyqtg8xZv0rH5znyjxHOWsoUtSq4cUTeeBIiXXX51QOOe+VoCjdXJRw==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.19.tgz", + "integrity": "sha512-LTxRmMgqqMv05Had879W00Fm53quiJd3Zuz8h1JSNJ3nGSlbZ/7Tjs1tKyScgN3Au3t3MyPsjPlq60fMmSHLsg==", "cpu": [ "x64" ], @@ -1347,9 +1451,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.0.tgz", - "integrity": "sha512-t9IfNkHQs/uKgPoyEtU912MG6a1j7Had37cSUyLTKx9MnUpjj+ZDKw9OyqTI9OwIIv0wmkr1pkZy+3T5pxhJPg==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.19.tgz", + "integrity": "sha512-eoNQSpA5PQfB9wBO4RA47MTDXWz1fizy9Y3Z6e4DetYIF3dvjuu8sj7aIGn/bFCU6lnFzTK34NtCaffP4NsQ7Q==", "cpu": [ "x64" ], @@ -1363,9 +1467,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.0.tgz", - "integrity": "sha512-WEAoHyG14t5sTavZa1c6BnOIEukll9iqFRTavqRVPfYmfegOAd5MaZfXgOGG6kGo1RduyGdTHD4+YZQSdsNZXg==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.19.tgz", + "integrity": "sha512-6UNt2dFuCHOe446sm/Kp69nUe8/wIhnh9bm6Xcqw4qEWCOppLMOvhTBVgvM7invVUNr4SPpP6NOQsACtn2IN9Q==", "cpu": [ "arm64" ], @@ -1379,9 +1483,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.0.tgz", - "integrity": "sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.19.tgz", + "integrity": "sha512-PhmojAHyqMne56HBLGu9dhDnHPuFmEjrXSQMM/nW0J6j849lk3ESrVtqNJcCk8CKOV7brpTTbaYAjwKPzKM69w==", "cpu": [ "x64" ], @@ -1446,16 +1550,6 @@ "node": ">=14" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@radix-ui/number": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", @@ -2226,9 +2320,10 @@ "license": "MIT" }, "node_modules/@remix-run/router": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz", - "integrity": "sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -2245,12 +2340,6 @@ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2260,25 +2349,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@testing-library/jest-dom": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", @@ -2431,11 +2501,54 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "peer": true + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } }, "node_modules/@types/estree": { "version": "1.0.9", @@ -2525,39 +2638,6 @@ "undici-types": "~6.20.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "optional": true, - "peer": true - }, - "node_modules/@types/react": { - "version": "17.0.83", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.83.tgz", - "integrity": "sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw==", - "optional": true, - "peer": true, - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "^0.16", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react/node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "optional": true, - "peer": true - }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "optional": true, - "peer": true - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3192,6 +3272,48 @@ "win32" ] }, + "node_modules/@xyflow/react": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.0.tgz", + "integrity": "sha512-na4IO33FSs2OS72hASgZDmTYwFAkef7Z74uBUVrong3ARmQQHfnRUVaCFn1kTt5LbS6pK03TbYjCPGLjLFfziA==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.77", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.77", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.77.tgz", + "integrity": "sha512-qCDCMCQAAgUu8yHnhloHG9F5mwPX5E+Wl8McpYIOPSSXfzFJJoZcwOcsDiAjitVKIg2de1WmJbCHfpcvxprsgg==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -3608,9 +3730,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -3658,17 +3780,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -3822,6 +3933,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3837,20 +3954,6 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3867,17 +3970,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -3908,10 +4000,11 @@ "license": "MIT" }, "node_modules/core-js-pure": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.39.0.tgz", - "integrity": "sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -3948,6 +4041,111 @@ "node": ">=4" } }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -4087,9 +4285,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "optional": true, "engines": { @@ -5442,9 +5640,10 @@ "integrity": "sha512-TV3YgAKda5hPz75n7QXmGCsSzgVya1vvmBieebg3EB5ScmashTZ0FldViG1aU2d4V5rcAGrtQ7k5uAaCo0A4PA==" }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -6551,9 +6750,10 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -6650,12 +6850,12 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -6702,15 +6902,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -6742,15 +6943,13 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/next/-/next-15.1.0.tgz", - "integrity": "sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==", + "version": "15.5.19", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.19.tgz", + "integrity": "sha512-xNOW6tYshGX1/Oi3F8uuk4gpDeWsSUE/1Z0G5uUMekIxaQ0xc03UXd9II0VQHYMWviMeA0OHpJFAKsHf8bTYVg==", "license": "MIT", "dependencies": { - "@next/env": "15.1.0", - "@swc/counter": "0.1.3", + "@next/env": "15.5.19", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -6762,19 +6961,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.1.0", - "@next/swc-darwin-x64": "15.1.0", - "@next/swc-linux-arm64-gnu": "15.1.0", - "@next/swc-linux-arm64-musl": "15.1.0", - "@next/swc-linux-x64-gnu": "15.1.0", - "@next/swc-linux-x64-musl": "15.1.0", - "@next/swc-win32-arm64-msvc": "15.1.0", - "@next/swc-win32-x64-msvc": "15.1.0", - "sharp": "^0.33.5" + "@next/swc-darwin-arm64": "15.5.19", + "@next/swc-darwin-x64": "15.5.19", + "@next/swc-linux-arm64-gnu": "15.5.19", + "@next/swc-linux-arm64-musl": "15.5.19", + "@next/swc-linux-x64-gnu": "15.5.19", + "@next/swc-linux-x64-musl": "15.5.19", + "@next/swc-win32-arm64-msvc": "15.5.19", + "@next/swc-win32-x64-msvc": "15.5.19", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -7140,9 +7339,10 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -7191,9 +7391,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -7210,7 +7410,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -7342,32 +7542,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7494,11 +7668,12 @@ } }, "node_modules/react-router": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.0.tgz", - "integrity": "sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.21.0" + "@remix-run/router": "1.23.3" }, "engines": { "node": ">=14.0.0" @@ -7617,11 +7792,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -7839,16 +8009,16 @@ } }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -7857,31 +8027,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/sharp/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.3.tgz", + "integrity": "sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==", "license": "ISC", "optional": true, "bin": { @@ -8000,23 +8175,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8081,14 +8239,6 @@ "node": ">= 0.4" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -8696,21 +8846,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/typescript-eslint": { "version": "8.59.4", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", @@ -8845,6 +8980,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9001,15 +9145,18 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yocto-queue": { @@ -9047,6 +9194,34 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 2a472c3..801af91 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@testing-library/jest-dom": "^5.11.5", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "@xyflow/react": "^12.11.0", "autoprefixer": "^10.4.20", "bootstrap": "^5.3.3", "class-variance-authority": "^0.7.1", diff --git a/public/images/graph-traversal.png b/public/images/graph-traversal.png new file mode 100644 index 0000000..ad68804 Binary files /dev/null and b/public/images/graph-traversal.png differ diff --git a/public/images/graph.png b/public/images/graph.png index ad68804..2e9a19a 100644 Binary files a/public/images/graph.png and b/public/images/graph.png differ diff --git a/public/images/mst.png b/public/images/mst.png new file mode 100644 index 0000000..4382e78 Binary files /dev/null and b/public/images/mst.png differ diff --git a/public/images/shortest-path.png b/public/images/shortest-path.png new file mode 100644 index 0000000..503d710 Binary files /dev/null and b/public/images/shortest-path.png differ diff --git a/src/app/about/page.jsx b/src/app/about/page.jsx index 767d5e5..8d6df05 100644 --- a/src/app/about/page.jsx +++ b/src/app/about/page.jsx @@ -9,6 +9,20 @@ const algorithms = [ category: "Graph Search", items: ["DFS", "BFS", "Dijkstra", "A*", "Recursive Maze Generation"], }, + { + category: "Data Structures", + items: [ + "Linked List — insert, delete, search, reverse (singly & doubly)", + ], + }, + { + category: "Interactive Graphs", + items: [ + "Graph Traversal — BFS / DFS", + "Shortest Path — Dijkstra & Bellman-Ford (with negative-cycle detection)", + "Minimum Spanning Tree — Kruskal & Prim", + ], + }, { category: "Sorting", items: [ diff --git a/src/app/components/algorithm-cards.jsx b/src/app/components/algorithm-cards.jsx index dfd9534..26d35c1 100644 --- a/src/app/components/algorithm-cards.jsx +++ b/src/app/components/algorithm-cards.jsx @@ -10,6 +10,21 @@ const algorithms = [ title: "Pathfinder", description: "Visualize graph algorithms like dijkstra, BFS, DFS", image: '/AlgorithmVisualizer/images/graph.png?height=200&width=300' + },{ + id: 'graph', + title: 'Graph Traversal', + description: "Build a graph and watch BFS and DFS explore it node by node", + image: '/AlgorithmVisualizer/images/graph-traversal.png?height=200&width=300' + },{ + id: 'shortest-path', + title: 'Shortest Path', + description: "Weighted graphs with Dijkstra and Bellman-Ford, including negative-cycle detection", + image: '/AlgorithmVisualizer/images/shortest-path.png?height=200&width=300' + },{ + id: 'mst', + title: 'Minimum Spanning Tree', + description: "Build a weighted graph and watch Kruskal and Prim grow the minimum spanning tree", + image: '/AlgorithmVisualizer/images/mst.png?height=200&width=300' }, { id: 'recursion-tree', diff --git a/src/app/graph/page.jsx b/src/app/graph/page.jsx new file mode 100644 index 0000000..6ff3f90 --- /dev/null +++ b/src/app/graph/page.jsx @@ -0,0 +1,49 @@ +"use client"; + +import { useState } from 'react'; +import { ReactFlowProvider } from '@xyflow/react'; +import Navbar from '@/components/navbar'; +import { PRESETS, adjacency, bfsActions, dfsActions } from '@/lib/algorithms/graph'; +import { useGraphEditor } from '@/components/graph/use-graph-editor'; +import GraphCanvas from '@/components/graph/graph-canvas'; +import GraphMenu from '@/components/graph/graph-menu'; + +function GraphInner() { + const g = useGraphEditor({ initialPreset: PRESETS[0] }); + const [algo, setAlgo] = useState(0); + + const onVisualize = () => { + const { edges, directed, startId, finishId } = g.getContext(); + const adj = adjacency(edges, directed); + g.run(algo === 1 ? dfsActions(adj, startId, finishId) : bfsActions(adj, startId, finishId)); + }; + + return ( +
+ +
+ g.loadPreset(PRESETS[i])} + onSpeedChange={g.setSpeed} + onVisualize={onVisualize} + onClear={g.clear} + /> + +
+
+ ); +} + +export default function Graph() { + return ( + + + + ); +} diff --git a/src/app/mst/page.jsx b/src/app/mst/page.jsx new file mode 100644 index 0000000..f4cbc36 --- /dev/null +++ b/src/app/mst/page.jsx @@ -0,0 +1,52 @@ +"use client"; + +import { useState } from 'react'; +import { ReactFlowProvider } from '@xyflow/react'; +import Navbar from '@/components/navbar'; +import { weightedAdjacency } from '@/lib/algorithms/graph'; +import { MST_PRESETS, kruskalActions, primActions } from '@/lib/algorithms/mst'; +import { useGraphEditor } from '@/components/graph/use-graph-editor'; +import GraphCanvas from '@/components/graph/graph-canvas'; +import GraphMenu from '@/components/graph/graph-menu'; + +function MstInner() { + const g = useGraphEditor({ weighted: true, initialPreset: MST_PRESETS[0] }); + const [algo, setAlgo] = useState(0); + + const onVisualize = () => { + const { nodes, edges, startId } = g.getContext(); + g.run(algo === 1 + ? primActions(weightedAdjacency(edges, false), startId, nodes) + : kruskalActions(nodes, edges)); + }; + + return ( +
+ +
+ g.loadPreset(MST_PRESETS[i])} + onSpeedChange={g.setSpeed} + onVisualize={onVisualize} + onClear={g.clear} + /> + +
+
+ ); +} + +export default function Mst() { + return ( + + + + ); +} diff --git a/src/app/shortest-path/page.jsx b/src/app/shortest-path/page.jsx new file mode 100644 index 0000000..4fed441 --- /dev/null +++ b/src/app/shortest-path/page.jsx @@ -0,0 +1,60 @@ +"use client"; + +import { useState } from 'react'; +import { ReactFlowProvider } from '@xyflow/react'; +import Navbar from '@/components/navbar'; +import { + SP_PRESETS, + weightedAdjacency, + edgeList, + dijkstraActions, + bellmanFordActions, +} from '@/lib/algorithms/shortestPath'; +import { useGraphEditor } from '@/components/graph/use-graph-editor'; +import GraphCanvas from '@/components/graph/graph-canvas'; +import GraphMenu from '@/components/graph/graph-menu'; + +function ShortestPathInner() { + const g = useGraphEditor({ weighted: true, initialPreset: SP_PRESETS[0] }); + const [algo, setAlgo] = useState(0); + + const onVisualize = () => { + const { nodes, edges, directed, startId, finishId } = g.getContext(); + const ids = nodes.map((n) => n.id); + if (algo === 1) { + g.run(bellmanFordActions(edgeList(edges, directed), startId, finishId, ids)); + } else { + g.run(dijkstraActions(weightedAdjacency(edges, directed), startId, finishId, ids)); + } + }; + + return ( +
+ +
+ g.loadPreset(SP_PRESETS[i])} + onSpeedChange={g.setSpeed} + onVisualize={onVisualize} + onClear={g.clear} + /> + +
+
+ ); +} + +export default function ShortestPath() { + return ( + + + + ); +} diff --git a/src/components/graph/floating-edge.jsx b/src/components/graph/floating-edge.jsx new file mode 100644 index 0000000..40e21d2 --- /dev/null +++ b/src/components/graph/floating-edge.jsx @@ -0,0 +1,148 @@ +import { createContext, useContext, useState } from 'react'; +import { BaseEdge, EdgeLabelRenderer, getStraightPath, useInternalNode } from '@xyflow/react'; + +// Edge that connects node centers (no handles), trimmed to each node's border. +// Stroke color encodes the edge state; during traversal a big arrow is drawn at +// the edge midpoint pointing in the direction of travel. When data.weight is set +// an editable weight label is shown (used by the shortest-path visualizer). + +// Provided only by pages that allow weight editing; null elsewhere. +export const EdgeWeightContext = createContext(null); + +const R = 22; // node radius (node is 44px) + +const STROKE = { + relax: '#fbbf24', + tree: '#f59e0b', + path: '#10b981', + negcycle: '#f43f5e', + reject: '#cbd5e1', + normal: '#64748b', +}; + +function center(node) { + const { x, y } = node.internals.positionAbsolute; + const w = node.measured?.width ?? 44; + const h = node.measured?.height ?? 44; + return { x: x + w / 2, y: y + h / 2 }; +} + +export default function FloatingEdge({ id, source, target, markerEnd, data, selected }) { + const setWeight = useContext(EdgeWeightContext); + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(''); + + const sourceNode = useInternalNode(source); + const targetNode = useInternalNode(target); + if (!sourceNode || !targetNode) return null; + + const sc = center(sourceNode); + const tc = center(targetNode); + + const state = data?.state || 'normal'; + const travelTo = data?.travelTo; + + // Orient along the travel direction so the midpoint arrow points the way we + // move (handles edges stored opposite to the traversal direction). + let from = sc; + let to = tc; + if (travelTo === source) { from = tc; to = sc; } + + const dx = to.x - from.x; + const dy = to.y - from.y; + const len = Math.hypot(dx, dy) || 1; + const ux = dx / len; + const uy = dy / len; + + const sx = from.x + ux * R; + const sy = from.y + uy * R; + const ex = to.x - ux * R; + const ey = to.y - uy * R; + const [path, labelX, labelY] = getStraightPath({ sourceX: sx, sourceY: sy, targetX: ex, targetY: ey }); + + // Selection highlights the edge (sky blue, thicker) for editing; otherwise + // the stroke encodes state at a constant width so the directed arrowhead + // (sized in stroke-width units) doesn't grow during traversal. + const stroke = selected ? '#0ea5e9' : (STROKE[state] || STROKE.normal); + const strokeWidth = selected ? 3.5 : 2; + + // only show the direction arrow when a travel direction was set (BFS/DFS/SSSP); + // undirected uses like MST leave travelTo null -> no arrow + const showTravelArrow = (state === 'tree' || state === 'path') && travelTo != null; + const mx = (sx + ex) / 2; + const my = (sy + ey) / 2; + const deg = (Math.atan2(uy, ux) * 180) / Math.PI; + + // weight label offset perpendicular to the edge so it clears the line/arrow. + // Use the stable source->target orientation (not the travel-flipped one) so + // the label stays on the same side during traversal. + const hasWeight = data?.weight != null; + const sdx = tc.x - sc.x; + const sdy = tc.y - sc.y; + const slen = Math.hypot(sdx, sdy) || 1; + const lx = labelX + (-sdy / slen) * 9; + const ly = labelY + (sdx / slen) * 9; + + const commit = () => { + setEditing(false); + const v = Number(draft); + if (setWeight && draft.trim() !== '' && Number.isFinite(v)) setWeight(id, v); + }; + + return ( + <> + + {showTravelArrow && ( + + )} + {hasWeight && ( + +
{ + if (!setWeight) return; + e.stopPropagation(); + setDraft(String(data.weight)); + setEditing(true); + }} + > + {editing ? ( + setDraft(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { + if (e.key === 'Enter') commit(); + else if (e.key === 'Escape') setEditing(false); + }} + style={{ width: 36, border: 'none', outline: 'none', font: 'inherit', textAlign: 'center' }} + /> + ) : ( + data.weight + )} +
+
+ )} + + ); +} diff --git a/src/components/graph/graph-canvas.jsx b/src/components/graph/graph-canvas.jsx new file mode 100644 index 0000000..8a7a948 --- /dev/null +++ b/src/components/graph/graph-canvas.jsx @@ -0,0 +1,98 @@ +import { ReactFlow, Background, Controls } from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import GraphNode from './graph-node'; +import FloatingEdge, { EdgeWeightContext } from './floating-edge'; +import { Key } from './kbd'; + +// Renders the editable graph for a useGraphEditor() instance: the React Flow +// canvas, a context-sensitive command bar + status overlay, and (for weighted +// editors) the EdgeWeightContext provider that enables inline weight editing. + +const nodeTypes = { graphNode: GraphNode }; +const edgeTypes = { floating: FloatingEdge }; + +// Build the command bar: a label + a list of {keys?, text} actions, chosen by +// the current mode and what (if anything) is selected. +function buildHint(editor) { + const { mode, directed, weighted } = editor; + if (mode === 'add-node') return { label: 'Add-node', actions: [{ text: 'click to drop nodes' }, { keys: ['Esc'], text: 'exit' }] }; + if (mode === 'add-edge') return { label: 'Add-edge', actions: [{ text: 'click nodes to chain' }, { keys: ['Esc'], text: 'exit' }] }; + if (mode === 'delete') return { label: 'Delete', actions: [{ text: 'click a node or edge' }, { keys: ['Esc'], text: 'exit' }] }; + + const selNode = editor.nodes.find((n) => n.selected); + const selEdge = editor.edges.find((e) => e.selected); + if (selNode) { + return { + label: 'Node', + actions: [ + { keys: ['S'], text: 'mark start' }, + { keys: ['F'], text: 'mark finish' }, + { keys: ['Del'], text: 'delete' }, + { keys: ['Esc'], text: 'cancel' }, + ], + }; + } + if (selEdge) { + return { + label: 'Edge', + actions: [ + ...(directed ? [{ keys: ['X'], text: 'reverse' }] : []), + { keys: ['Del'], text: 'delete' }, + { keys: ['Esc'], text: 'cancel' }, + ], + }; + } + return { + label: 'Select', + actions: [ + { keys: ['N'], text: 'add node' }, + { keys: ['E'], text: 'add edge' }, + { keys: ['D'], text: 'delete' }, + ...(weighted ? [{ text: '· click a weight to edit' }] : []), + ], + }; +} + +export default function GraphCanvas({ editor }) { + const hint = buildHint(editor); + + return ( +
+
+
+ {hint.label} + {hint.actions.map((a, i) => ( + + {a.keys && a.keys.map((k) => {k})} + {a.text && {a.text}} + + ))} +
+ {editor.status && ( +
+ {editor.status} +
+ )} +
+ + + + + + +
+ ); +} diff --git a/src/components/graph/graph-menu.jsx b/src/components/graph/graph-menu.jsx new file mode 100644 index 0000000..1daaf43 --- /dev/null +++ b/src/components/graph/graph-menu.jsx @@ -0,0 +1,108 @@ +import { CustomSelect } from '@/components/custom-select'; +import { CustomSlider } from '@/components/custom-slider'; +import { CustomToggle } from '@/components/custom-toggle'; +import { Button } from '@/components/ui/button'; +import { Play, RotateCcw, MousePointerClick } from 'lucide-react'; +import { Key } from './kbd'; + +// Shared sidebar for the graph visualizers. The page owns the menu and wires +// these callbacks to a useGraphEditor() instance + its chosen algorithms. + +// Each control: optional `click` (mouse icon) + `text` prefix, `keys` caps, desc. +// Per-element actions (S/F/Del/X) are surfaced in the on-canvas command bar when +// a node/edge is selected, so the menu only points you there. +const CONTROLS = [ + { keys: ['N'], desc: 'add-node mode' }, + { keys: ['E'], desc: 'add-edge (chain)' }, + { keys: ['D'], desc: 'delete mode' }, + { keys: ['Esc'], desc: 'select mode' }, + { click: true, text: 'Node / Edge', desc: 'select for options' }, + { text: 'drag', desc: 'move node' }, +]; + +export default function GraphMenu({ + title, + algorithms, + presets, + hideDirected = false, + disabled, + onDirectedChange, + onAlgorithmChange, + onPresetChange, + onSpeedChange, + onVisualize, + onClear, +}) { + const controls = CONTROLS; + + return ( +
+

{title}

+ +
+
+
+ Config +
+
+ {!hideDirected && ( + + )} + + p.name)} + onChange={onPresetChange} + disabled={disabled} + /> + +
+ +
+
+
+ Actions +
+
+ + +
+ +
+
+
+ Controls +
+
+
+ {controls.map((c, i) => ( +
+
+ {c.click && } + {c.text && {c.text}} + {c.keys && c.keys.map((k) => {k})} +
+
{c.desc}
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/graph/graph-node.jsx b/src/components/graph/graph-node.jsx new file mode 100644 index 0000000..f7271e8 --- /dev/null +++ b/src/components/graph/graph-node.jsx @@ -0,0 +1,74 @@ +import { memo } from 'react'; +import { Handle, Position } from '@xyflow/react'; + +// Clean circular node. Fill encodes traversal state; an outer ring marks the +// start/finish role. Hidden handles satisfy React Flow's edge anchoring +// (edges are floating and created via keyboard, not handle dragging). + +const FILL = { + normal: ['#0d9488', '#0f766e'], + current: ['#f59e0b', '#d97706'], + frontier: ['#4f46e5', '#4338ca'], + visited: ['#334155', '#475569'], + path: ['#10b981', '#059669'], + negcycle: ['#f43f5e', '#be123c'], +}; + +function GraphNode({ data, selected }) { + const [bg, border] = FILL[data.state] || FILL.normal; + const ring = data.role === 'start' ? '#10b981' : data.role === 'finish' ? '#f43f5e' : null; + + // stack rings: role ring (inner) + selection ring (outer) + const layers = []; + if (ring) layers.push(`0 0 0 3px ${ring}`); + if (selected) layers.push(`0 0 0 ${ring ? 5 : 3}px #0ea5e9`); + const boxShadow = layers.length ? layers.join(', ') : '0 1px 3px rgba(0,0,0,0.3)'; + + return ( +
+ + {data.label} + + {ring && ( + + {data.role} + + )} + {data.dist !== undefined && ( + + {data.dist === Infinity || data.dist == null ? '∞' : data.dist} + + )} +
+ ); +} + +export default memo(GraphNode); diff --git a/src/components/graph/kbd.jsx b/src/components/graph/kbd.jsx new file mode 100644 index 0000000..66a960e --- /dev/null +++ b/src/components/graph/kbd.jsx @@ -0,0 +1,8 @@ +// Small keycap, shared by the graph menu and the on-canvas command bar. +export function Key({ children }) { + return ( + + {children} + + ); +} diff --git a/src/components/graph/use-graph-editor.js b/src/components/graph/use-graph-editor.js new file mode 100644 index 0000000..65951dd --- /dev/null +++ b/src/components/graph/use-graph-editor.js @@ -0,0 +1,252 @@ +import { useEffect, useRef, useState } from 'react'; +import { MarkerType, useNodesState, useEdgesState, useReactFlow } from '@xyflow/react'; +import { newNodeId, edgeId, toFlow } from '@/lib/algorithms/graph'; + +// Reusable graph editor + action-log executor for the graph visualizers. +// Owns all editing (keyboard add/delete, set start/finish, drag, directed +// toggle, presets, clear) and the executor (run/applyAction). It is +// algorithm-agnostic: callers build an action list and hand it to `run`. +// +// Must be used inside (uses useReactFlow). + +const ARROW = { type: MarkerType.ArrowClosed, color: '#64748b', width: 16, height: 16 }; +const toDelay = (s) => 1100 - s * 10; + +// preset -> { nodes, edges, startId } with the first node marked as start +function seed(preset) { + const { nodes, edges } = toFlow(preset); + if (nodes[0]) nodes[0] = { ...nodes[0], data: { ...nodes[0].data, role: 'start' } }; + return { nodes, edges, startId: nodes[0]?.id ?? null }; +} + +export function useGraphEditor({ weighted = false, initialPreset }) { + const [initial] = useState(() => seed(initialPreset)); + const [nodes, setNodes, onNodesChange] = useNodesState(initial.nodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initial.edges); + const [directed, setDirectedState] = useState(false); + const [mode, setMode] = useState('idle'); + const [status, setStatus] = useState(''); + const [isRunning, setIsRunning] = useState(false); + + const { screenToFlowPosition } = useReactFlow(); + + const nodesRef = useRef(initial.nodes); + const edgesRef = useRef(initial.edges); + const isRunningRef = useRef(false); + const speedRef = useRef(toDelay(50)); + const modeRef = useRef('idle'); + const pendingEdgeRef = useRef(null); + const directedRef = useRef(false); + const startIdRef = useRef(initial.startId); + const finishIdRef = useRef(null); + const labelRef = useRef(0); + + useEffect(() => { nodesRef.current = nodes; }, [nodes]); + useEffect(() => { edgesRef.current = edges; }, [edges]); + const setModeBoth = (m) => { modeRef.current = m; setMode(m); }; + + // --- action appliers --- + const markNode = (id, state) => + setNodes((ns) => ns.map((n) => (n.id === id ? { ...n, data: { ...n.data, state } } : n))); + const markEdge = (id, state, to) => + setEdges((es) => es.map((e) => (e.id === id ? { ...e, data: { ...e.data, state, travelTo: to ?? null } } : e))); + const setDist = (id, dist) => + setNodes((ns) => ns.map((n) => (n.id === id ? { ...n, data: { ...n.data, dist } } : n))); + const clearMarks = () => { + setNodes((ns) => ns.map((n) => ({ ...n, data: { ...n.data, state: 'normal', dist: undefined } }))); + setEdges((es) => es.map((e) => ({ ...e, data: { ...e.data, state: 'normal', travelTo: null } }))); + setStatus(''); + }; + + const applyAction = (action) => { + if (action.type === 'markNode') markNode(action.id, action.state); + else if (action.type === 'markEdge') markEdge(action.id, action.state, action.to); + else if (action.type === 'setDist') setDist(action.id, action.dist); + else if (action.type === 'status') setStatus(action.text); + else if (action.type === 'clear') clearMarks(); + }; + + const run = async (actions) => { + if (!actions || !actions.length || isRunningRef.current) return; + isRunningRef.current = true; + setIsRunning(true); + clearMarks(); + for (const action of actions) { + applyAction(action); + await sleep(speedRef.current); + } + isRunningRef.current = false; + setIsRunning(false); + }; + + // authoritative graph snapshot for building actions at click time + const getContext = () => ({ + nodes: nodesRef.current, + edges: edgesRef.current, + directed: directedRef.current, + startId: startIdRef.current, + finishId: finishIdRef.current, + }); + + // --- editing --- + const addNodeAt = (position) => { + const id = newNodeId(); + labelRef.current += 1; + const label = String(labelRef.current); + setNodes((ns) => [ + ...ns, + { id, type: 'graphNode', position, data: { label, state: 'normal', role: null } }, + ]); + }; + + const addEdge = (a, b) => { + if (a === b) return; + const exists = edgesRef.current.some( + (e) => + (e.source === a && e.target === b) || + (!directedRef.current && e.source === b && e.target === a), + ); + if (exists) return; + const data = { state: 'normal' }; + if (weighted) data.weight = 1; + setEdges((es) => [ + ...es, + { id: edgeId(a, b), source: a, target: b, type: 'floating', data, markerEnd: directedRef.current ? ARROW : undefined }, + ]); + }; + + const setWeight = (id, w) => { + if (!weighted || isRunningRef.current) return; + setEdges((es) => es.map((e) => (e.id === id ? { ...e, data: { ...e.data, weight: w } } : e))); + }; + + const setRole = (id, role) => { + if (role === 'start') { startIdRef.current = id; if (finishIdRef.current === id) finishIdRef.current = null; } + if (role === 'finish') { finishIdRef.current = id; if (startIdRef.current === id) startIdRef.current = null; } + setNodes((ns) => + ns.map((n) => { + if (n.id === id) return { ...n, data: { ...n.data, role } }; + if (n.data.role === role) return { ...n, data: { ...n.data, role: null } }; + return n; + }), + ); + }; + + const deleteNode = (id) => { + setNodes((ns) => ns.filter((n) => n.id !== id)); + setEdges((es) => es.filter((e) => e.source !== id && e.target !== id)); + if (startIdRef.current === id) startIdRef.current = null; + if (finishIdRef.current === id) finishIdRef.current = null; + if (pendingEdgeRef.current === id) pendingEdgeRef.current = null; + }; + + const deleteEdge = (id) => setEdges((es) => es.filter((e) => e.id !== id)); + + // directed only: swap endpoints so the arrow flips (id/key kept stable) + const reverseEdge = (id) => { + if (!directedRef.current) return; + setEdges((es) => es.map((e) => (e.id === id ? { ...e, source: e.target, target: e.source } : e))); + }; + + // Persistent modes: add-node drops a node on every pane click; add-edge + // chains edges through consecutively clicked nodes; delete removes the + // clicked node/edge. Esc returns to select. + const onPaneClick = (event) => { + if (isRunningRef.current) return; + if (modeRef.current === 'add-node') { + addNodeAt(screenToFlowPosition({ x: event.clientX, y: event.clientY })); + } else { + pendingEdgeRef.current = null; // clicking empty lifts the edge anchor + } + }; + + const onNodeClick = (_event, node) => { + if (isRunningRef.current) return; + const m = modeRef.current; + if (m === 'add-edge') { + if (pendingEdgeRef.current == null) { + pendingEdgeRef.current = node.id; + } else { + addEdge(pendingEdgeRef.current, node.id); + pendingEdgeRef.current = node.id; // chain: anchor moves to this node + } + } else if (m === 'delete') { + deleteNode(node.id); + } + // idle: React Flow handles selection + }; + + const onEdgeClick = (_event, edge) => { + if (isRunningRef.current) return; + if (modeRef.current === 'delete') deleteEdge(edge.id); + // idle: React Flow handles selection + }; + + useEffect(() => { + const onKey = (e) => { + if (isRunningRef.current) return; + const el = e.target; + if (el && el.closest && el.closest('input,select,textarea,[role="combobox"],button')) return; + + const selNode = () => nodesRef.current.find((n) => n.selected); + const selEdge = () => edgesRef.current.find((n) => n.selected); + const k = e.key.toLowerCase(); + + if (k === 'n') { setModeBoth('add-node'); pendingEdgeRef.current = null; } + else if (k === 'e') { setModeBoth('add-edge'); pendingEdgeRef.current = null; } + else if (k === 'd') { setModeBoth('delete'); pendingEdgeRef.current = null; } + else if (e.key === 'Escape') { setModeBoth('idle'); pendingEdgeRef.current = null; } + else if (k === 's') { const n = selNode(); if (n) setRole(n.id, 'start'); } + else if (k === 'f') { const n = selNode(); if (n) setRole(n.id, 'finish'); } + else if (k === 'x') { const ed = selEdge(); if (ed) reverseEdge(ed.id); } + else if (e.key === 'Delete' || e.key === 'Backspace') { + const n = selNode(); + const ed = selEdge(); + if (n) deleteNode(n.id); + else if (ed) deleteEdge(ed.id); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // --- menu-facing controls --- + const setDirected = (val) => { + directedRef.current = val; + setDirectedState(val); + setEdges((es) => es.map((e) => ({ ...e, markerEnd: val ? ARROW : undefined }))); + }; + const loadPreset = (preset) => { + if (isRunningRef.current) return; + const g = seed(preset); + const e = directedRef.current ? g.edges.map((x) => ({ ...x, markerEnd: ARROW })) : g.edges; + nodesRef.current = g.nodes; edgesRef.current = e; + startIdRef.current = g.startId; finishIdRef.current = null; + labelRef.current = 0; + setNodes(g.nodes); setEdges(e); + setModeBoth('idle'); setStatus(''); pendingEdgeRef.current = null; + }; + const clear = () => { + if (isRunningRef.current) return; + nodesRef.current = []; edgesRef.current = []; + startIdRef.current = null; finishIdRef.current = null; + labelRef.current = 0; + setNodes([]); setEdges([]); + setModeBoth('idle'); setStatus(''); pendingEdgeRef.current = null; + }; + const setSpeed = (s) => { speedRef.current = toDelay(s); }; + + return { + // reactive + nodes, edges, directed, mode, status, isRunning, weighted, + // react flow wiring + onNodesChange, onEdgesChange, onPaneClick, onNodeClick, onEdgeClick, + // methods + run, getContext, setWeight, setDirected, loadPreset, clear, setSpeed, + }; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/src/lib/algorithms/graph.js b/src/lib/algorithms/graph.js new file mode 100644 index 0000000..e42d48e --- /dev/null +++ b/src/lib/algorithms/graph.js @@ -0,0 +1,210 @@ +// Graph visualizer — presets, adjacency, and BFS/DFS action planners. +// +// The page is a generic executor: planners return a flat list of visual ACTIONS +// and the page applies each with a delay (same pattern as the linked list). +// +// Action shapes: +// { type: 'markNode', id, state } -> 'current' | 'frontier' | 'visited' | 'path' | 'normal' +// { type: 'markEdge', id, state } -> 'tree' | 'path' | 'normal' +// { type: 'clear' } -> reset traversal marks (start/finish rings kept) + +let userCounter = 0; +export const newNodeId = () => `u${++userCounter}`; +export const edgeId = (source, target) => `e_${source}_${target}`; + +// --- presets (hand-positioned, pixel coordinates) --- + +export const PRESETS = [ + { + name: 'Tree', + nodes: [ + { id: 'n1', x: 250, y: 30, label: 'A' }, + { id: 'n2', x: 140, y: 140, label: 'B' }, + { id: 'n3', x: 360, y: 140, label: 'C' }, + { id: 'n4', x: 80, y: 250, label: 'D' }, + { id: 'n5', x: 210, y: 250, label: 'E' }, + { id: 'n6', x: 410, y: 250, label: 'F' }, + ], + edges: [['n1', 'n2'], ['n1', 'n3'], ['n2', 'n4'], ['n2', 'n5'], ['n3', 'n6']], + }, + { + name: 'Cycle', + nodes: [ + { id: 'n1', x: 250, y: 40, label: 'A' }, + { id: 'n2', x: 360, y: 110, label: 'B' }, + { id: 'n3', x: 360, y: 240, label: 'C' }, + { id: 'n4', x: 250, y: 300, label: 'D' }, + { id: 'n5', x: 140, y: 240, label: 'E' }, + { id: 'n6', x: 140, y: 110, label: 'F' }, + ], + edges: [['n1', 'n2'], ['n2', 'n3'], ['n3', 'n4'], ['n4', 'n5'], ['n5', 'n6'], ['n6', 'n1']], + }, + { + name: 'Dense', + nodes: [ + { id: 'n1', x: 120, y: 70, label: 'A' }, + { id: 'n2', x: 290, y: 50, label: 'B' }, + { id: 'n3', x: 440, y: 100, label: 'C' }, + { id: 'n4', x: 160, y: 240, label: 'D' }, + { id: 'n5', x: 320, y: 250, label: 'E' }, + { id: 'n6', x: 460, y: 240, label: 'F' }, + ], + edges: [ + ['n1', 'n2'], ['n2', 'n3'], ['n1', 'n4'], ['n2', 'n4'], + ['n2', 'n5'], ['n3', 'n5'], ['n3', 'n6'], ['n4', 'n5'], ['n5', 'n6'], + ], + }, +]; + +// Convert a preset to React Flow nodes/edges. Edge entries are [source, target] +// or [source, target, weight]; the weight (when present) is carried in data so +// the same helper serves both unweighted and weighted visualizers. +export function toFlow(preset) { + const nodes = preset.nodes.map((n) => ({ + id: n.id, + type: 'graphNode', + position: { x: n.x, y: n.y }, + data: { label: n.label, state: 'normal' }, + })); + const edges = preset.edges.map(([source, target, weight]) => { + const data = { state: 'normal' }; + if (weight !== undefined) data.weight = weight; + return { id: edgeId(source, target), source, target, type: 'floating', data }; + }); + return { nodes, edges }; +} + +// adjacency: id -> [{ node, edge }], sorted by neighbor id for determinism. +export function adjacency(edges, directed) { + const adj = {}; + const add = (a, b, id) => { + (adj[a] = adj[a] || []).push({ node: b, edge: id }); + }; + for (const e of edges) { + add(e.source, e.target, e.id); + if (!directed) add(e.target, e.source, e.id); + } + for (const k of Object.keys(adj)) { + adj[k].sort((x, y) => (x.node < y.node ? -1 : x.node > y.node ? 1 : 0)); + } + return adj; +} + +// Weighted adjacency: id -> [{ node, edge, weight }] (used by shortest-path / MST). +export function weightedAdjacency(edges, directed) { + const adj = {}; + const add = (a, b, id, w) => { (adj[a] = adj[a] || []).push({ node: b, edge: id, weight: w }); }; + for (const e of edges) { + const w = e.data?.weight ?? 1; + add(e.source, e.target, e.id, w); + if (!directed) add(e.target, e.source, e.id, w); + } + for (const k of Object.keys(adj)) { + adj[k].sort((x, y) => (x.node < y.node ? -1 : x.node > y.node ? 1 : 0)); + } + return adj; +} + +// Flat weighted edge list: [{ u, v, w, id }] — both directions when undirected. +export function edgeList(edges, directed) { + const list = []; + for (const e of edges) { + const w = e.data?.weight ?? 1; + list.push({ u: e.source, v: e.target, w, id: e.id }); + if (!directed) list.push({ u: e.target, v: e.source, w, id: e.id }); + } + return list; +} + +// Reconstruct start->finish path actions from parent maps. +function pathActions(parent, parentEdge, startId, endId) { + const nodes = []; + const edgeSteps = []; + let cur = endId; + while (cur !== undefined && cur !== null) { + nodes.push(cur); + if (parentEdge[cur] != null) edgeSteps.push({ id: parentEdge[cur], from: parent[cur], to: cur }); + if (cur === startId) break; + cur = parent[cur]; + } + nodes.reverse(); + edgeSteps.reverse(); + return [ + ...nodes.map((id) => ({ type: 'markNode', id, state: 'path' })), + ...edgeSteps.map((e) => ({ type: 'markEdge', id: e.id, state: 'path', from: e.from, to: e.to })), + ]; +} + +export function bfsActions(adj, startId, finishId) { + const actions = []; + if (startId == null) return actions; + + const visited = new Set(); + const seen = new Set([startId]); + const parent = {}; + const parentEdge = {}; + const queue = [startId]; + actions.push({ type: 'markNode', id: startId, state: 'frontier' }); + + while (queue.length) { + const u = queue.shift(); + // highlight the tree edge as we process the node (consistent with DFS), + // not when it was first discovered + if (parentEdge[u] != null) { + actions.push({ type: 'markEdge', id: parentEdge[u], state: 'tree', from: parent[u], to: u }); + } + actions.push({ type: 'markNode', id: u, state: 'current' }); + if (u === finishId) { + actions.push(...pathActions(parent, parentEdge, startId, u)); + return actions; + } + visited.add(u); + for (const { node: v, edge } of adj[u] || []) { + if (!seen.has(v)) { + seen.add(v); + parent[v] = u; + parentEdge[v] = edge; + actions.push({ type: 'markNode', id: v, state: 'frontier' }); + queue.push(v); + } + } + actions.push({ type: 'markNode', id: u, state: 'visited' }); + } + return actions; +} + +export function dfsActions(adj, startId, finishId) { + const actions = []; + if (startId == null) return actions; + + const visited = new Set(); + const parent = {}; + const parentEdge = {}; + // stack holds {node, from, viaEdge} so the tree edge is the one actually + // descended through when the node is popped (not its first discovery). + const stack = [{ node: startId, from: null, viaEdge: null }]; + actions.push({ type: 'markNode', id: startId, state: 'frontier' }); + + while (stack.length) { + const { node: u, from, viaEdge } = stack.pop(); + if (visited.has(u)) continue; + if (from !== null) { + parent[u] = from; + parentEdge[u] = viaEdge; + actions.push({ type: 'markEdge', id: viaEdge, state: 'tree', from, to: u }); + } + actions.push({ type: 'markNode', id: u, state: 'current' }); + if (u === finishId) { + actions.push(...pathActions(parent, parentEdge, startId, u)); + return actions; + } + visited.add(u); + for (const { node: v, edge } of adj[u] || []) { + if (visited.has(v)) continue; + actions.push({ type: 'markNode', id: v, state: 'frontier' }); + stack.push({ node: v, from: u, viaEdge: edge }); + } + actions.push({ type: 'markNode', id: u, state: 'visited' }); + } + return actions; +} diff --git a/src/lib/algorithms/mst.js b/src/lib/algorithms/mst.js new file mode 100644 index 0000000..8386934 --- /dev/null +++ b/src/lib/algorithms/mst.js @@ -0,0 +1,119 @@ +// Minimum Spanning Tree — presets and Kruskal/Prim planners. +// +// Reuses the action-log pattern and the weighted graph workspace. MST is +// undirected; planners treat the graph as undirected. Edge/node states reused: +// relax -> edge being considered +// path -> edge/node in the spanning tree +// reject -> edge skipped (would form a cycle) +// plus { type: 'status', text } for the running/total weight. + +export const MST_PRESETS = [ + { + name: 'Mesh', + nodes: [ + { id: 'n1', x: 120, y: 70, label: 'A' }, + { id: 'n2', x: 290, y: 50, label: 'B' }, + { id: 'n3', x: 450, y: 100, label: 'C' }, + { id: 'n4', x: 160, y: 240, label: 'D' }, + { id: 'n5', x: 320, y: 250, label: 'E' }, + { id: 'n6', x: 470, y: 240, label: 'F' }, + ], + edges: [ + ['n1', 'n2', 4], ['n2', 'n3', 3], ['n1', 'n4', 2], ['n2', 'n4', 5], + ['n2', 'n5', 10], ['n3', 'n5', 6], ['n3', 'n6', 1], ['n4', 'n5', 4], ['n5', 'n6', 2], + ], + }, + { + name: 'Ring', + nodes: [ + { id: 'n1', x: 250, y: 40, label: 'A' }, + { id: 'n2', x: 360, y: 110, label: 'B' }, + { id: 'n3', x: 360, y: 240, label: 'C' }, + { id: 'n4', x: 250, y: 300, label: 'D' }, + { id: 'n5', x: 140, y: 240, label: 'E' }, + { id: 'n6', x: 140, y: 110, label: 'F' }, + ], + edges: [ + ['n1', 'n2', 3], ['n2', 'n3', 5], ['n3', 'n4', 2], ['n4', 'n5', 6], + ['n5', 'n6', 4], ['n6', 'n1', 1], ['n1', 'n4', 7], ['n2', 'n5', 8], + ], + }, +]; + +// each undirected edge once +function weightedEdges(edges) { + return edges.map((e) => ({ u: e.source, v: e.target, w: e.data?.weight ?? 1, id: e.id })); +} + +export function kruskalActions(nodes, edges) { + const actions = []; + const V = nodes.length; + if (V === 0) return actions; + + const sorted = weightedEdges(edges).sort((a, b) => (a.w - b.w) || (a.id < b.id ? -1 : 1)); + + const parent = {}; + for (const n of nodes) parent[n.id] = n.id; + const find = (x) => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; }; + + let total = 0; + let accepted = 0; + for (const { u, v, w, id } of sorted) { + actions.push({ type: 'markEdge', id, state: 'relax' }); + const ru = find(u); + const rv = find(v); + if (ru !== rv) { + parent[ru] = rv; + total += w; + accepted += 1; + actions.push({ type: 'markEdge', id, state: 'path' }); + actions.push({ type: 'markNode', id: u, state: 'path' }); + actions.push({ type: 'markNode', id: v, state: 'path' }); + actions.push({ type: 'status', text: `MST weight: ${total}` }); + } else { + actions.push({ type: 'markEdge', id, state: 'reject' }); + } + } + actions.push({ + type: 'status', + text: accepted === V - 1 ? `MST complete · weight ${total}` : `Not connected · forest weight ${total}`, + }); + return actions; +} + +export function primActions(adj, startId, nodes) { + const actions = []; + const ids = nodes.map((n) => n.id); + const V = ids.length; + const start = startId ?? ids[0]; + if (start == null) return actions; + + const visited = new Set([start]); + actions.push({ type: 'markNode', id: start, state: 'path' }); + let total = 0; + + while (visited.size < V) { + // best crossing edge from the visited set to an unvisited node + let best = null; + for (const u of visited) { + for (const { node: v, edge, weight } of adj[u] || []) { + if (visited.has(v)) continue; + if (best == null || weight < best.weight) best = { u, v, edge, weight }; + } + } + if (best == null) break; // disconnected + + actions.push({ type: 'markEdge', id: best.edge, state: 'relax' }); + actions.push({ type: 'markEdge', id: best.edge, state: 'path' }); + actions.push({ type: 'markNode', id: best.v, state: 'path' }); + visited.add(best.v); + total += best.weight; + actions.push({ type: 'status', text: `MST weight: ${total}` }); + } + + actions.push({ + type: 'status', + text: visited.size === V ? `MST complete · weight ${total}` : `Not connected · weight ${total}`, + }); + return actions; +} diff --git a/src/lib/algorithms/shortestPath.js b/src/lib/algorithms/shortestPath.js new file mode 100644 index 0000000..e41e24c --- /dev/null +++ b/src/lib/algorithms/shortestPath.js @@ -0,0 +1,204 @@ +// Single-Source Shortest Path — weighted presets, adjacency, and planners. +// +// Reuses the action-log pattern: planners return a flat list of actions that the +// page applies with a delay. Extends the graph action set with: +// { type: 'setDist', id, dist } -> node distance label (Infinity => ∞) +// { type: 'status', text } -> overlay status text +// plus edge states 'relax' | 'tree' | 'path' | 'negcycle'. + +import { toFlow, weightedAdjacency, edgeList } from './graph'; + +// re-exported for back-compat with existing imports from this module +export { toFlow, weightedAdjacency, edgeList }; + +export const SP_PRESETS = [ + { + name: 'Basic', + nodes: [ + { id: 'n1', x: 120, y: 70, label: 'A' }, + { id: 'n2', x: 290, y: 50, label: 'B' }, + { id: 'n3', x: 450, y: 100, label: 'C' }, + { id: 'n4', x: 160, y: 240, label: 'D' }, + { id: 'n5', x: 320, y: 250, label: 'E' }, + { id: 'n6', x: 470, y: 240, label: 'F' }, + ], + edges: [ + ['n1', 'n2', 4], ['n2', 'n3', 3], ['n1', 'n4', 2], ['n2', 'n4', 5], + ['n2', 'n5', 10], ['n3', 'n5', 6], ['n3', 'n6', 1], ['n4', 'n5', 4], ['n5', 'n6', 2], + ], + }, + { + name: 'Tree', + nodes: [ + { id: 'n1', x: 250, y: 30, label: 'A' }, + { id: 'n2', x: 140, y: 140, label: 'B' }, + { id: 'n3', x: 360, y: 140, label: 'C' }, + { id: 'n4', x: 80, y: 250, label: 'D' }, + { id: 'n5', x: 210, y: 250, label: 'E' }, + { id: 'n6', x: 410, y: 250, label: 'F' }, + ], + edges: [['n1', 'n2', 3], ['n1', 'n3', 6], ['n2', 'n4', 1], ['n2', 'n5', 4], ['n3', 'n6', 2]], + }, + { + name: 'Negative (use Directed)', + nodes: [ + { id: 'n1', x: 120, y: 80, label: 'A' }, + { id: 'n2', x: 300, y: 60, label: 'B' }, + { id: 'n3', x: 300, y: 230, label: 'C' }, + { id: 'n4', x: 480, y: 150, label: 'D' }, + ], + edges: [['n1', 'n2', 4], ['n1', 'n3', 5], ['n2', 'n3', -3], ['n3', 'n4', 2], ['n2', 'n4', 6]], + }, +]; + +function pathActions(parent, parentEdge, startId, endId) { + const nodes = []; + const edgeSteps = []; + let cur = endId; + while (cur !== undefined && cur !== null) { + nodes.push(cur); + if (parentEdge[cur] != null) edgeSteps.push({ id: parentEdge[cur], from: parent[cur], to: cur }); + if (cur === startId) break; + cur = parent[cur]; + } + nodes.reverse(); + edgeSteps.reverse(); + return [ + ...nodes.map((id) => ({ type: 'markNode', id, state: 'path' })), + ...edgeSteps.map((e) => ({ type: 'markEdge', id: e.id, state: 'path', from: e.from, to: e.to })), + ]; +} + +export function dijkstraActions(adj, startId, finishId, nodeIds) { + const actions = []; + if (startId == null) return actions; + + const hasNeg = Object.values(adj).some((list) => list.some((e) => e.weight < 0)); + if (hasNeg) actions.push({ type: 'status', text: 'Dijkstra assumes non-negative weights' }); + + const dist = {}; + const parent = {}; + const parentEdge = {}; + const visited = new Set(); + for (const id of nodeIds) dist[id] = Infinity; + dist[startId] = 0; + actions.push({ type: 'setDist', id: startId, dist: 0 }); + actions.push({ type: 'markNode', id: startId, state: 'frontier' }); + + while (true) { + let u = null; + let best = Infinity; + for (const id of nodeIds) { + if (!visited.has(id) && dist[id] < best) { best = dist[id]; u = id; } + } + if (u == null) break; // remaining nodes unreachable + + actions.push({ type: 'markNode', id: u, state: 'current' }); + if (u === finishId) { + visited.add(u); + actions.push({ type: 'markNode', id: u, state: 'visited' }); + break; + } + for (const { node: v, edge, weight } of adj[u] || []) { + if (visited.has(v)) continue; + actions.push({ type: 'markEdge', id: edge, state: 'relax', from: u, to: v }); + if (dist[u] + weight < dist[v]) { + if (parentEdge[v] != null) actions.push({ type: 'markEdge', id: parentEdge[v], state: 'normal' }); + dist[v] = dist[u] + weight; + parent[v] = u; + parentEdge[v] = edge; + actions.push({ type: 'setDist', id: v, dist: dist[v] }); + actions.push({ type: 'markEdge', id: edge, state: 'tree', from: u, to: v }); + actions.push({ type: 'markNode', id: v, state: 'frontier' }); + } else { + actions.push({ type: 'markEdge', id: edge, state: 'normal' }); + } + } + visited.add(u); + actions.push({ type: 'markNode', id: u, state: 'visited' }); + } + + if (finishId != null && dist[finishId] < Infinity) { + actions.push(...pathActions(parent, parentEdge, startId, finishId)); + } + return actions; +} + +function negCycleActions(parent, parentEdge, startNode, V) { + let y = startNode; + for (let i = 0; i < V; i++) { + if (parent[y] == null) break; + y = parent[y]; + } + const nodes = []; + const edges = []; + let cur = y; + do { + nodes.push(cur); + if (parentEdge[cur] != null) edges.push(parentEdge[cur]); + cur = parent[cur]; + } while (cur != null && cur !== y && nodes.length <= V + 1); + return [ + ...nodes.map((id) => ({ type: 'markNode', id, state: 'negcycle' })), + ...edges.map((id) => ({ type: 'markEdge', id, state: 'negcycle' })), + ]; +} + +export function bellmanFordActions(edges, startId, finishId, nodeIds) { + const actions = []; + if (startId == null) return actions; + + const dist = {}; + const parent = {}; + const parentEdge = {}; + for (const id of nodeIds) dist[id] = Infinity; + dist[startId] = 0; + actions.push({ type: 'setDist', id: startId, dist: 0 }); + actions.push({ type: 'markNode', id: startId, state: 'frontier' }); + + const V = nodeIds.length; + for (let p = 1; p <= V - 1; p++) { + actions.push({ type: 'status', text: `Pass ${p} / ${V - 1}` }); + let changed = false; + for (const { u, v, w, id } of edges) { + if (dist[u] === Infinity) continue; + actions.push({ type: 'markEdge', id, state: 'relax', from: u, to: v }); + if (dist[u] + w < dist[v]) { + dist[v] = dist[u] + w; + parent[v] = u; + parentEdge[v] = id; + changed = true; + actions.push({ type: 'setDist', id: v, dist: dist[v] }); + actions.push({ type: 'markNode', id: v, state: 'frontier' }); + actions.push({ type: 'markEdge', id, state: 'tree', from: u, to: v }); + } else { + actions.push({ type: 'markEdge', id, state: 'normal' }); + } + } + if (!changed) break; // converged early + } + + // re-assert the final shortest-path tree so it stays highlighted + for (const id of nodeIds) { + if (parentEdge[id] != null) { + actions.push({ type: 'markEdge', id: parentEdge[id], state: 'tree', from: parent[id], to: id }); + } + } + + // detection pass + actions.push({ type: 'status', text: 'Checking for a negative cycle…' }); + let relaxable = null; + for (const { u, v, w } of edges) { + if (dist[u] !== Infinity && dist[u] + w < dist[v]) { relaxable = v; break; } + } + if (relaxable != null) { + actions.push({ type: 'status', text: 'Negative cycle detected' }); + actions.push(...negCycleActions(parent, parentEdge, relaxable, V)); + } else { + actions.push({ type: 'status', text: 'Done' }); + if (finishId != null && dist[finishId] < Infinity) { + actions.push(...pathActions(parent, parentEdge, startId, finishId)); + } + } + return actions; +}