blanchon commited on
Commit
0f00bbc
·
1 Parent(s): 8e5c0f0

Match table: prepend match_id column, truncate event; add stats section

Browse files

- New MatchIdCell + matchIdColumn helper, prepended to all 3 column sets
(rounds / maps / matches). Match id rendered as monospace tabular-nums.
- eventColumn helper with meta.cellClass = max-w-[16rem] so long event
names truncate; EventCell wrapper also gets max-w-[16rem].
- match-table.svelte: thread column meta.cellClass / meta.headClass
through Table.Cell + Table.Head so per-column widths actually apply.
- New StatsSection between the table and BibTeX:
* Maps distribution: horizontal bars per CS2 map (count + share %),
colored using mapColorClasses for visual tie-in with badges.
* Rounds-per-map histogram: 4-round buckets. Restructure layout —
bars are direct children of h-32 flex items-end so % heights resolve
against a definite parent height; labels render in a separate row
below to avoid collapsing to zero in flex-col wrappers.

bun.lock CHANGED
@@ -23,6 +23,7 @@
23
  "clsx": "^2.1.1",
24
  "eslint": "^10.2.1",
25
  "eslint-plugin-better-tailwindcss": "^4.5.0",
 
26
  "mode-watcher": "^1.1.0",
27
  "phosphor-svelte": "^3.1.0",
28
  "prettier": "^3.8.3",
@@ -43,6 +44,10 @@
43
  },
44
  },
45
  "packages": {
 
 
 
 
46
  "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "2.8.1" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
47
 
48
  "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@@ -97,6 +102,14 @@
97
 
98
  "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
99
 
 
 
 
 
 
 
 
 
100
  "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "0.10.1" }, "peerDependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
101
 
102
  "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
@@ -185,6 +198,10 @@
185
 
186
  "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
187
 
 
 
 
 
188
  "@types/dom-mediacapture-transform": ["@types/dom-mediacapture-transform@0.1.11", "", { "dependencies": { "@types/dom-webcodecs": "0.1.13" } }, "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ=="],
189
 
190
  "@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="],
@@ -193,6 +210,8 @@
193
 
194
  "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
195
 
 
 
196
  "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
197
 
198
  "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="],
@@ -257,12 +276,66 @@
257
 
258
  "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
261
 
262
  "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
263
 
264
  "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
265
 
 
 
266
  "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
267
 
268
  "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
@@ -329,12 +402,16 @@
329
 
330
  "hysnappy": ["hysnappy@1.0.0", "", {}, "sha512-MNrC4NfwDGPb889O6gIfEtbvEZCSWUsSEhsz4Oq2FRcpGtXHfeVz3KciSPp5Pnnz1NjFMgDQNfxdJozymJEDDA=="],
331
 
 
 
332
  "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
333
 
334
  "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
335
 
336
  "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
337
 
 
 
338
  "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
339
 
340
  "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
@@ -357,6 +434,8 @@
357
 
358
  "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
359
 
 
 
360
  "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
361
 
362
  "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
@@ -395,6 +474,10 @@
395
 
396
  "mediabunny": ["mediabunny@1.42.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-s9ypTqLi6kbh95gC+YaJlG0PkLvMxu37Q/wO/pFZx0fUCA5Ym5mp+2dWoa83mKQ3Uo18aNlgev5iJ5ESZqWwgQ=="],
397
 
 
 
 
 
398
  "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
399
 
400
  "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -447,12 +530,18 @@
447
 
448
  "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
449
 
 
 
450
  "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="],
451
 
452
  "runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "2.0.3", "esm-env": "1.2.2", "lz-string": "1.5.0" }, "optionalDependencies": { "@sveltejs/kit": "2.58.0" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
453
 
 
 
454
  "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "1.2.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
455
 
 
 
456
  "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
457
 
458
  "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
@@ -541,6 +630,14 @@
541
 
542
  "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
543
 
 
 
 
 
 
 
 
 
544
  "mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "1.2.2" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="],
545
 
546
  "mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "2.1.1", "runed": "0.23.4", "style-to-object": "1.0.14" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
@@ -553,6 +650,10 @@
553
 
554
  "svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "1.2.2" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
555
 
 
 
 
 
556
  "mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "1.2.2" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
557
  }
558
  }
 
23
  "clsx": "^2.1.1",
24
  "eslint": "^10.2.1",
25
  "eslint-plugin-better-tailwindcss": "^4.5.0",
26
+ "layerchart": "2.0.0-next.48",
27
  "mode-watcher": "^1.1.0",
28
  "phosphor-svelte": "^3.1.0",
29
  "prettier": "^3.8.3",
 
44
  },
45
  },
46
  "packages": {
47
+ "@dagrejs/dagre": ["@dagrejs/dagre@2.0.4", "", { "dependencies": { "@dagrejs/graphlib": "3.0.4" } }, "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA=="],
48
+
49
+ "@dagrejs/graphlib": ["@dagrejs/graphlib@3.0.4", "", {}, "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg=="],
50
+
51
  "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "2.8.1" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
52
 
53
  "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
 
102
 
103
  "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "3.1.2", "@jridgewell/sourcemap-codec": "1.5.5" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
104
 
105
+ "@layerstack/svelte-actions": ["@layerstack/svelte-actions@1.0.1-next.18", "", { "dependencies": { "@floating-ui/dom": "^1.7.0", "@layerstack/utils": "2.0.0-next.18", "d3-scale": "^4.0.2" } }, "sha512-gxPzCnJ1c9LTfWtRqLUzefCx+k59ZpxDUQ2XB+LokveZQPe7IDSOwHaBOEMlaGoGrtwc3Ft8dSZq+2WT2o9u/g=="],
106
+
107
+ "@layerstack/svelte-state": ["@layerstack/svelte-state@0.1.0-next.23", "", { "dependencies": { "@layerstack/utils": "2.0.0-next.18" } }, "sha512-7O4umv+gXwFfs3/vjzFWYHNXGwYnnjBapWJ5Y+9u99F4eVk6rh4ocNwqkqQNkpMZ5tUJBlRTWjPE1So6+hEzIg=="],
108
+
109
+ "@layerstack/tailwind": ["@layerstack/tailwind@2.0.0-next.21", "", { "dependencies": { "@layerstack/utils": "^2.0.0-next.18", "clsx": "^2.1.1", "d3-array": "^3.2.4", "tailwind-merge": "^3.2.0" } }, "sha512-Qgp2EpmEHmjtura8MQzWicR6ztBRSsRvddakFtx9ShrLMz6jWzd6bCMVVRu44Q3ZOrtXmSu4QxjCZWu1ytvuPg=="],
110
+
111
+ "@layerstack/utils": ["@layerstack/utils@2.0.0-next.18", "", { "dependencies": { "d3-array": "^3.2.4", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0" } }, "sha512-EYILHpfBRYMMEahajInu9C2AXQom5IcAEdtCeucD3QIl/fdDgRbtzn6/8QW9ewumfyNZetdUvitOksmI1+gZYQ=="],
112
+
113
  "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "0.10.1" }, "peerDependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
114
 
115
  "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
 
198
 
199
  "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
200
 
201
+ "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
202
+
203
+ "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
204
+
205
  "@types/dom-mediacapture-transform": ["@types/dom-mediacapture-transform@0.1.11", "", { "dependencies": { "@types/dom-webcodecs": "0.1.13" } }, "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ=="],
206
 
207
  "@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="],
 
210
 
211
  "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
212
 
213
+ "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
214
+
215
  "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
216
 
217
  "@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="],
 
276
 
277
  "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
278
 
279
+ "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
280
+
281
+ "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
282
+
283
+ "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
284
+
285
+ "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
286
+
287
+ "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
288
+
289
+ "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
290
+
291
+ "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
292
+
293
+ "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
294
+
295
+ "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
296
+
297
+ "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
298
+
299
+ "d3-geo-voronoi": ["d3-geo-voronoi@2.1.0", "", { "dependencies": { "d3-array": "3", "d3-delaunay": "6", "d3-geo": "3", "d3-tricontour": "1" } }, "sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q=="],
300
+
301
+ "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
302
+
303
+ "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
304
+
305
+ "d3-interpolate-path": ["d3-interpolate-path@2.3.0", "", {}, "sha512-tZYtGXxBmbgHsIc9Wms6LS5u4w6KbP8C09a4/ZYc4KLMYYqub57rRBUgpUr2CIarIrJEpdAWWxWQvofgaMpbKQ=="],
306
+
307
+ "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
308
+
309
+ "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
310
+
311
+ "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
312
+
313
+ "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
314
+
315
+ "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
316
+
317
+ "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
318
+
319
+ "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
320
+
321
+ "d3-tile": ["d3-tile@1.0.0", "", {}, "sha512-79fnTKpPMPDS5xQ0xuS9ir0165NEwwkFpe/DSOmc2Gl9ldYzKKRDWogmTTE8wAJ8NA7PMapNfEcyKhI9Lxdu5Q=="],
322
+
323
+ "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
324
+
325
+ "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
326
+
327
+ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
328
+
329
+ "d3-tricontour": ["d3-tricontour@1.1.0", "", { "dependencies": { "d3-delaunay": "6", "d3-scale": "4" } }, "sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ=="],
330
+
331
  "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
332
 
333
  "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
334
 
335
  "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
336
 
337
+ "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
338
+
339
  "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
340
 
341
  "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
 
402
 
403
  "hysnappy": ["hysnappy@1.0.0", "", {}, "sha512-MNrC4NfwDGPb889O6gIfEtbvEZCSWUsSEhsz4Oq2FRcpGtXHfeVz3KciSPp5Pnnz1NjFMgDQNfxdJozymJEDDA=="],
404
 
405
+ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
406
+
407
  "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
408
 
409
  "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
410
 
411
  "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
412
 
413
+ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
414
+
415
  "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
416
 
417
  "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
 
434
 
435
  "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
436
 
437
+ "layerchart": ["layerchart@2.0.0-next.48", "", { "dependencies": { "@dagrejs/dagre": "^2.0.4", "@layerstack/svelte-actions": "1.0.1-next.18", "@layerstack/svelte-state": "0.1.0-next.23", "@layerstack/tailwind": "2.0.0-next.21", "@layerstack/utils": "2.0.0-next.18", "@types/d3-contour": "^3.0.6", "d3-array": "^3.2.4", "d3-chord": "^3.0.1", "d3-color": "^3.1.0", "d3-contour": "^4.0.2", "d3-delaunay": "^6.0.4", "d3-dsv": "^3.0.1", "d3-force": "^3.0.0", "d3-geo": "^3.1.1", "d3-geo-voronoi": "^2.1.0", "d3-hierarchy": "^3.1.2", "d3-interpolate": "^3.0.1", "d3-interpolate-path": "^2.3.0", "d3-path": "^3.1.0", "d3-quadtree": "^3.0.1", "d3-random": "^3.0.1", "d3-sankey": "^0.12.3", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.1.0", "d3-shape": "^3.2.0", "d3-tile": "^1.0.0", "d3-time": "^3.1.0", "memoize": "^10.2.0", "runed": "^0.37.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-XoEYBztamA8lMxtF/Jz3aDX0HMk8dI+o4fK9fSl8ecT2Tdx3DQUjtKGtlQAOFdwC/AWifeLmKq5cMTQt9COZPQ=="],
438
+
439
  "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
440
 
441
  "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
 
474
 
475
  "mediabunny": ["mediabunny@1.42.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-s9ypTqLi6kbh95gC+YaJlG0PkLvMxu37Q/wO/pFZx0fUCA5Ym5mp+2dWoa83mKQ3Uo18aNlgev5iJ5ESZqWwgQ=="],
476
 
477
+ "memoize": ["memoize@10.2.0", "", { "dependencies": { "mimic-function": "^5.0.1" } }, "sha512-DeC6b7QBrZsRs3Y02A6A7lQyzFbsQbqgjI6UW0GigGWV+u1s25TycMr0XHZE4cJce7rY/vyw2ctMQqfDkIhUEA=="],
478
+
479
+ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
480
+
481
  "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
482
 
483
  "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
 
530
 
531
  "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
532
 
533
+ "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
534
+
535
  "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="],
536
 
537
  "runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "2.0.3", "esm-env": "1.2.2", "lz-string": "1.5.0" }, "optionalDependencies": { "@sveltejs/kit": "2.58.0" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
538
 
539
+ "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
540
+
541
  "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "1.2.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
542
 
543
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
544
+
545
  "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
546
 
547
  "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
 
630
 
631
  "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
632
 
633
+ "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
634
+
635
+ "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
636
+
637
+ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
638
+
639
+ "layerchart/runed": ["runed@0.37.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0", "zod": "^4.1.0" }, "optionalPeers": ["@sveltejs/kit", "zod"] }, "sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA=="],
640
+
641
  "mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "1.2.2" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="],
642
 
643
  "mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "2.1.1", "runed": "0.23.4", "style-to-object": "1.0.14" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
 
650
 
651
  "svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "1.2.2" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="],
652
 
653
+ "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
654
+
655
+ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
656
+
657
  "mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "1.2.2" }, "peerDependencies": { "svelte": "5.55.5" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
658
  }
659
  }
package.json CHANGED
@@ -32,6 +32,7 @@
32
  "clsx": "^2.1.1",
33
  "eslint": "^10.2.1",
34
  "eslint-plugin-better-tailwindcss": "^4.5.0",
 
35
  "mode-watcher": "^1.1.0",
36
  "phosphor-svelte": "^3.1.0",
37
  "prettier": "^3.8.3",
 
32
  "clsx": "^2.1.1",
33
  "eslint": "^10.2.1",
34
  "eslint-plugin-better-tailwindcss": "^4.5.0",
35
+ "layerchart": "2.0.0-next.48",
36
  "mode-watcher": "^1.1.0",
37
  "phosphor-svelte": "^3.1.0",
38
  "prettier": "^3.8.3",
src/lib/components/match-table/cells/event-cell.svelte CHANGED
@@ -6,7 +6,7 @@
6
  let { event, format }: Props = $props();
7
  </script>
8
 
9
- <div class="flex min-w-0 flex-col">
10
  <span class="truncate text-sm font-medium" title={event}>{event}</span>
11
  {#if format}
12
  <span class="text-[0.65rem] tracking-wide text-muted-foreground/70 uppercase">{format}</span>
 
6
  let { event, format }: Props = $props();
7
  </script>
8
 
9
+ <div class="flex max-w-[16rem] min-w-0 flex-col">
10
  <span class="truncate text-sm font-medium" title={event}>{event}</span>
11
  {#if format}
12
  <span class="text-[0.65rem] tracking-wide text-muted-foreground/70 uppercase">{format}</span>
src/lib/components/match-table/cells/match-id-cell.svelte ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ interface Props {
3
+ matchId: number;
4
+ }
5
+ let { matchId }: Props = $props();
6
+ </script>
7
+
8
+ <span class="font-mono text-xs text-muted-foreground/80 tabular-nums">{matchId}</span>
src/lib/components/match-table/columns.ts CHANGED
@@ -3,6 +3,7 @@ import { renderComponent } from '$lib/components/ui/data-table';
3
  import SortHeader from './data-table-sort-header.svelte';
4
  import PlainHeader from './data-table-plain-header.svelte';
5
  import EventCell from './cells/event-cell.svelte';
 
6
  import TeamsCell from './cells/teams-cell.svelte';
7
  import ScoreCell from './cells/score-cell.svelte';
8
  import MapCell from './cells/map-cell.svelte';
@@ -18,6 +19,8 @@ import type { Match } from '$lib/types';
18
  declare module '@tanstack/table-core' {
19
  interface ColumnMeta<TData extends RowData, TValue> {
20
  label?: string;
 
 
21
  }
22
  }
23
 
@@ -27,15 +30,30 @@ const dateSort = (a: { match_date: string }, b: { match_date: string }) =>
27
  // Cells use `accessorFn` returning the underlying value so global filtering and
28
  // sorting see the right thing; visual rendering happens in `cell` via components.
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  export const roundColumns: ColumnDef<RoundRow>[] = [
31
- {
32
- id: 'event',
33
- accessorKey: 'event',
34
- header: ({ column }) => renderComponent(SortHeader, { column, label: 'Event' }),
35
- cell: ({ row }) =>
36
- renderComponent(EventCell, { event: row.original.event, format: row.original.format }),
37
- meta: { label: 'Event' }
38
- },
39
  {
40
  id: 'map_name',
41
  accessorKey: 'map_name',
@@ -90,14 +108,8 @@ export const roundColumns: ColumnDef<RoundRow>[] = [
90
  ];
91
 
92
  export const mapColumns: ColumnDef<MapRow>[] = [
93
- {
94
- id: 'event',
95
- accessorKey: 'event',
96
- header: ({ column }) => renderComponent(SortHeader, { column, label: 'Event' }),
97
- cell: ({ row }) =>
98
- renderComponent(EventCell, { event: row.original.event, format: row.original.format }),
99
- meta: { label: 'Event' }
100
- },
101
  {
102
  id: 'map_name',
103
  accessorKey: 'map_name',
@@ -177,14 +189,8 @@ export const mapColumns: ColumnDef<MapRow>[] = [
177
  ];
178
 
179
  export const matchColumns: ColumnDef<MatchRow>[] = [
180
- {
181
- id: 'event',
182
- accessorKey: 'event',
183
- header: ({ column }) => renderComponent(SortHeader, { column, label: 'Event' }),
184
- cell: ({ row }) =>
185
- renderComponent(EventCell, { event: row.original.event, format: row.original.format }),
186
- meta: { label: 'Event' }
187
- },
188
  {
189
  id: 'teams',
190
  accessorFn: (r) => `${r.team1} ${r.team2}`,
 
3
  import SortHeader from './data-table-sort-header.svelte';
4
  import PlainHeader from './data-table-plain-header.svelte';
5
  import EventCell from './cells/event-cell.svelte';
6
+ import MatchIdCell from './cells/match-id-cell.svelte';
7
  import TeamsCell from './cells/teams-cell.svelte';
8
  import ScoreCell from './cells/score-cell.svelte';
9
  import MapCell from './cells/map-cell.svelte';
 
19
  declare module '@tanstack/table-core' {
20
  interface ColumnMeta<TData extends RowData, TValue> {
21
  label?: string;
22
+ cellClass?: string;
23
+ headClass?: string;
24
  }
25
  }
26
 
 
30
  // Cells use `accessorFn` returning the underlying value so global filtering and
31
  // sorting see the right thing; visual rendering happens in `cell` via components.
32
 
33
+ const matchIdColumn = <T extends { match_id: number }>(): ColumnDef<T> => ({
34
+ id: 'match_id',
35
+ accessorKey: 'match_id',
36
+ header: ({ column }) => renderComponent(SortHeader, { column, label: 'Match' }),
37
+ cell: ({ row }) => renderComponent(MatchIdCell, { matchId: row.original.match_id }),
38
+ meta: { label: 'Match', cellClass: 'w-[6rem] pr-2', headClass: 'w-[6rem] pr-2' }
39
+ });
40
+
41
+ const eventColumn = <T extends { event: string; format?: string }>(): ColumnDef<T> => ({
42
+ id: 'event',
43
+ accessorKey: 'event',
44
+ header: ({ column }) => renderComponent(SortHeader, { column, label: 'Event' }),
45
+ cell: ({ row }) =>
46
+ renderComponent(EventCell, { event: row.original.event, format: row.original.format }),
47
+ meta: {
48
+ label: 'Event',
49
+ cellClass: 'max-w-[16rem]',
50
+ headClass: 'max-w-[16rem]'
51
+ }
52
+ });
53
+
54
  export const roundColumns: ColumnDef<RoundRow>[] = [
55
+ matchIdColumn<RoundRow>(),
56
+ eventColumn<RoundRow>(),
 
 
 
 
 
 
57
  {
58
  id: 'map_name',
59
  accessorKey: 'map_name',
 
108
  ];
109
 
110
  export const mapColumns: ColumnDef<MapRow>[] = [
111
+ matchIdColumn<MapRow>(),
112
+ eventColumn<MapRow>(),
 
 
 
 
 
 
113
  {
114
  id: 'map_name',
115
  accessorKey: 'map_name',
 
189
  ];
190
 
191
  export const matchColumns: ColumnDef<MatchRow>[] = [
192
+ matchIdColumn<MatchRow>(),
193
+ eventColumn<MatchRow>(),
 
 
 
 
 
 
194
  {
195
  id: 'teams',
196
  accessorFn: (r) => `${r.team1} ${r.team2}`,
src/lib/components/match-table/match-table.svelte CHANGED
@@ -126,7 +126,7 @@
126
  {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
127
  <Table.Row class="bg-muted/40 hover:bg-muted/40">
128
  {#each headerGroup.headers as header (header.id)}
129
- <Table.Head class="h-9 text-xs">
130
  {#if !header.isPlaceholder}
131
  <FlexRender
132
  content={header.column.columnDef.header}
@@ -153,7 +153,7 @@
153
  role={getRowHref ? 'link' : undefined}
154
  >
155
  {#each row.getVisibleCells() as cell (cell.id)}
156
- <Table.Cell class="py-2 align-middle">
157
  <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
158
  </Table.Cell>
159
  {/each}
 
126
  {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
127
  <Table.Row class="bg-muted/40 hover:bg-muted/40">
128
  {#each headerGroup.headers as header (header.id)}
129
+ <Table.Head class={cn('h-9 text-xs', header.column.columnDef.meta?.headClass)}>
130
  {#if !header.isPlaceholder}
131
  <FlexRender
132
  content={header.column.columnDef.header}
 
153
  role={getRowHref ? 'link' : undefined}
154
  >
155
  {#each row.getVisibleCells() as cell (cell.id)}
156
+ <Table.Cell class={cn('py-2 align-middle', cell.column.columnDef.meta?.cellClass)}>
157
  <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
158
  </Table.Cell>
159
  {/each}
src/lib/components/stats-section.svelte ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Match } from '$lib/types';
3
+ import { prettyMap } from '$lib/utils/format';
4
+ import { mapColorClasses } from '$lib/utils/map-colors';
5
+ import { cn } from '$lib/utils';
6
+
7
+ interface Props {
8
+ matches: Match[];
9
+ }
10
+ let { matches }: Props = $props();
11
+
12
+ const mapsDist = $derived.by(() => {
13
+ const counts = new Map<string, number>();
14
+ for (const m of matches) counts.set(m.map_name, (counts.get(m.map_name) ?? 0) + 1);
15
+ return Array.from(counts.entries())
16
+ .map(([name, count]) => ({ name, count }))
17
+ .sort((a, b) => b.count - a.count);
18
+ });
19
+
20
+ const totalMaps = $derived(mapsDist.reduce((s, m) => s + m.count, 0));
21
+ const maxCount = $derived(Math.max(1, ...mapsDist.map((m) => m.count)));
22
+
23
+ // Histogram of rounds-per-match — buckets of 4 rounds.
24
+ const roundsHist = $derived.by(() => {
25
+ const buckets = new Map<string, number>();
26
+ for (const m of matches) {
27
+ const r = m.rounds_played;
28
+ if (!r) continue;
29
+ // pinch into [start, start+3] buckets; e.g. 13-16, 17-20, etc.
30
+ const start = Math.floor((r - 1) / 4) * 4 + 1;
31
+ const key = `${start}-${start + 3}`;
32
+ buckets.set(key, (buckets.get(key) ?? 0) + 1);
33
+ }
34
+ return Array.from(buckets.entries())
35
+ .map(([range, count]) => ({ range, count, sortKey: Number(range.split('-')[0]) }))
36
+ .sort((a, b) => a.sortKey - b.sortKey);
37
+ });
38
+ const roundsMax = $derived(Math.max(1, ...roundsHist.map((b) => b.count)));
39
+ </script>
40
+
41
+ <section class="mx-auto mt-12 max-w-5xl">
42
+ <h2
43
+ class="text-center font-heading text-xs font-semibold tracking-[0.2em] text-muted-foreground uppercase"
44
+ >
45
+ Statistics
46
+ </h2>
47
+
48
+ <div class="mt-6 grid gap-4 lg:grid-cols-2">
49
+ <!-- Maps distribution -->
50
+ <div class="rounded-md border bg-card p-4">
51
+ <div class="mb-3 flex items-baseline justify-between gap-2">
52
+ <div>
53
+ <div class="font-heading text-sm font-semibold">Maps distribution</div>
54
+ <p class="text-[11px] text-muted-foreground">
55
+ Maps rendered per CS2 map ({totalMaps} total)
56
+ </p>
57
+ </div>
58
+ </div>
59
+ <div class="space-y-1">
60
+ {#each mapsDist as m (m.name)}
61
+ {@const pct = (m.count / maxCount) * 100}
62
+ {@const sharePct = totalMaps ? (m.count / totalMaps) * 100 : 0}
63
+ <div class="grid grid-cols-[5.5rem_1fr_3.5rem] items-center gap-2 text-xs">
64
+ <span
65
+ class={cn(
66
+ 'truncate rounded-sm border px-1.5 py-0.5 text-center text-[10px] font-medium capitalize',
67
+ mapColorClasses(m.name)
68
+ )}
69
+ title={m.name}>{prettyMap(m.name)}</span
70
+ >
71
+ <div class="relative h-3 overflow-hidden rounded-sm bg-muted/40">
72
+ <div
73
+ class={cn('absolute inset-y-0 left-0 rounded-sm border-r', mapColorClasses(m.name))}
74
+ style="width: {pct}%"
75
+ ></div>
76
+ </div>
77
+ <span class="text-right font-mono text-muted-foreground tabular-nums">
78
+ {m.count}<span class="ml-1 text-[10px] text-muted-foreground/60"
79
+ >{sharePct.toFixed(0)}%</span
80
+ >
81
+ </span>
82
+ </div>
83
+ {/each}
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Rounds-per-match histogram -->
88
+ <div class="rounded-md border bg-card p-4">
89
+ <div class="mb-3">
90
+ <div class="font-heading text-sm font-semibold">Rounds per map</div>
91
+ <p class="text-[11px] text-muted-foreground">
92
+ How many maps fall into each round-count band (regulation = 24)
93
+ </p>
94
+ </div>
95
+ <!-- Two rows: bars (definite-height parent so the % heights resolve) + labels. -->
96
+ <div class="flex h-32 items-end gap-1.5">
97
+ {#each roundsHist as b (b.range)}
98
+ {@const h = Math.max(2, (b.count / roundsMax) * 100)}
99
+ <div
100
+ class="min-w-0 flex-1 rounded-sm bg-foreground/70 transition-colors hover:bg-foreground"
101
+ style="height: {h}%"
102
+ title={`${b.range} rounds: ${b.count} maps`}
103
+ ></div>
104
+ {/each}
105
+ </div>
106
+ <div class="mt-1 flex gap-1.5">
107
+ {#each roundsHist as b (b.range)}
108
+ <div
109
+ class="min-w-0 flex-1 truncate text-center font-mono text-[10px] text-muted-foreground/80"
110
+ >
111
+ {b.range}
112
+ </div>
113
+ {/each}
114
+ </div>
115
+ <div class="mt-1 text-center text-[10px] text-muted-foreground/70">rounds played</div>
116
+ </div>
117
+ </div>
118
+ </section>
src/lib/components/ui/chart/chart-container.svelte ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef } from '$lib/utils.js';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import ChartStyle from './chart-style.svelte';
5
+ import { setChartContext, type ChartConfig } from './chart-utils.js';
6
+
7
+ const uid = $props.id();
8
+
9
+ let {
10
+ ref = $bindable(null),
11
+ id = uid,
12
+ class: className,
13
+ children,
14
+ config,
15
+ ...restProps
16
+ }: WithElementRef<HTMLAttributes<HTMLElement>> & {
17
+ config: ChartConfig;
18
+ } = $props();
19
+
20
+ const chartId = $derived(`chart-${id || uid.replace(/:/g, '')}`);
21
+
22
+ setChartContext({
23
+ get config() {
24
+ return config;
25
+ }
26
+ });
27
+ </script>
28
+
29
+ <div
30
+ bind:this={ref}
31
+ data-chart={chartId}
32
+ data-slot="chart"
33
+ class={cn(
34
+ 'flex aspect-video justify-center overflow-visible text-xs',
35
+ // Overrides
36
+ //
37
+ // Stroke around dots/marks when hovering
38
+ '[&_.lc-highlight-point]:stroke-transparent',
39
+ // override the default stroke color of lines
40
+ '[&_.lc-line]:stroke-border/50',
41
+
42
+ // by default, layerchart shows a line intersecting the point when hovering, this hides that
43
+ '[&_.lc-highlight-line]:stroke-0',
44
+
45
+ // by default, when you hover a point on a stacked series chart, it will drop the opacity
46
+ // of the other series, this overrides that
47
+ '[&_.lc-area-path]:opacity-100 [&_.lc-highlight-line]:opacity-100 [&_.lc-highlight-point]:opacity-100 [&_.lc-spline-path]:opacity-100 [&_.lc-text]:text-xs [&_.lc-text-svg]:overflow-visible',
48
+
49
+ // We don't want the little tick lines between the axis labels and the chart, so we remove
50
+ // the stroke. The alternative is to manually disable `tickMarks` on the x/y axis of every
51
+ // chart.
52
+ '[&_.lc-axis-tick]:stroke-0',
53
+
54
+ // We don't want to display the rule on the x/y axis, as there is already going to be
55
+ // a grid line there and rule ends up overlapping the marks because it is rendered after
56
+ // the marks
57
+ '[&_.lc-rule-x-line:not(.lc-grid-x-rule)]:stroke-0 [&_.lc-rule-y-line:not(.lc-grid-y-rule)]:stroke-0',
58
+ '[&_.lc-grid-x-radial-line]:stroke-border [&_.lc-grid-x-radial-circle]:stroke-border',
59
+ '[&_.lc-grid-y-radial-line]:stroke-border [&_.lc-grid-y-radial-circle]:stroke-border',
60
+
61
+ // Legend adjustments
62
+ '[&_.lc-legend-swatch-button]:items-center [&_.lc-legend-swatch-button]:gap-1.5',
63
+ '[&_.lc-legend-swatch-group]:items-center [&_.lc-legend-swatch-group]:gap-4',
64
+ '[&_.lc-legend-swatch]:size-2.5 [&_.lc-legend-swatch]:rounded-[2px]',
65
+
66
+ // Labels
67
+ '[&_.lc-labels-text:not([fill])]:fill-foreground [&_text]:stroke-transparent',
68
+
69
+ // Tick labels on th x/y axes
70
+ '[&_.lc-axis-tick-label]:fill-muted-foreground [&_.lc-axis-tick-label]:font-normal',
71
+ '[&_.lc-tooltip-rects-g]:fill-transparent',
72
+ '[&_.lc-layout-svg-g]:fill-transparent',
73
+ '[&_.lc-root-container]:w-full',
74
+ className
75
+ )}
76
+ {...restProps}
77
+ >
78
+ <ChartStyle id={chartId} {config} />
79
+ {@render children?.()}
80
+ </div>
src/lib/components/ui/chart/chart-style.svelte ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { THEMES, type ChartConfig } from './chart-utils.js';
3
+
4
+ let { id, config }: { id: string; config: ChartConfig } = $props();
5
+
6
+ const colorConfig = $derived(
7
+ config ? Object.entries(config).filter(([, config]) => config.theme || config.color) : null
8
+ );
9
+
10
+ const themeContents = $derived.by(() => {
11
+ if (!colorConfig || !colorConfig.length) return;
12
+
13
+ const themeContents = [];
14
+ for (const [_theme, prefix] of Object.entries(THEMES)) {
15
+ let content = `${prefix} [data-chart=${id}] {\n`;
16
+ const color = colorConfig.map(([key, itemConfig]) => {
17
+ const theme = _theme as keyof typeof itemConfig.theme;
18
+ const color = itemConfig.theme?.[theme] || itemConfig.color;
19
+ return color ? `\t--color-${key}: ${color};` : null;
20
+ });
21
+
22
+ content += color.join('\n') + '\n}';
23
+
24
+ themeContents.push(content);
25
+ }
26
+
27
+ return themeContents.join('\n');
28
+ });
29
+ </script>
30
+
31
+ {#if themeContents}
32
+ {#key id}
33
+ <svelte:element this={'style'}>
34
+ {themeContents}
35
+ </svelte:element>
36
+ {/key}
37
+ {/if}
src/lib/components/ui/chart/chart-tooltip.svelte ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils.js';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from './chart-utils.js';
5
+ import { getChartContext, Tooltip as TooltipPrimitive } from 'layerchart';
6
+ import type { Snippet } from 'svelte';
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ function defaultFormatter(value: any, _payload: TooltipPayload[]) {
10
+ return `${value}`;
11
+ }
12
+
13
+ let {
14
+ ref = $bindable(null),
15
+ class: className,
16
+ hideLabel = false,
17
+ indicator = 'dot',
18
+ hideIndicator = false,
19
+ labelKey,
20
+ label,
21
+ labelFormatter = defaultFormatter,
22
+ labelClassName,
23
+ formatter,
24
+ nameKey,
25
+ color,
26
+ ...restProps
27
+ }: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> & {
28
+ hideLabel?: boolean;
29
+ label?: string;
30
+ indicator?: 'line' | 'dot' | 'dashed';
31
+ nameKey?: string;
32
+ labelKey?: string;
33
+ hideIndicator?: boolean;
34
+ labelClassName?: string;
35
+ labelFormatter?: // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ ((value: any, payload: TooltipPayload[]) => string | number | Snippet) | null;
37
+ formatter?: Snippet<
38
+ [
39
+ {
40
+ value: unknown;
41
+ name: string;
42
+ item: TooltipPayload;
43
+ index: number;
44
+ payload: TooltipPayload[];
45
+ }
46
+ ]
47
+ >;
48
+ } = $props();
49
+
50
+ const chart = useChart();
51
+ const chartCtx = getChartContext();
52
+
53
+ // Filter to series with defined values (important for item-based charts like Pie/Arc
54
+ // where only the hovered item has a value)
55
+ const visibleSeries = $derived(
56
+ chartCtx.tooltip.series.filter((s: TooltipPayload) => s.value !== undefined)
57
+ );
58
+
59
+ const formattedLabel = $derived.by(() => {
60
+ if (hideLabel || !visibleSeries?.length) return null;
61
+
62
+ const [item] = visibleSeries;
63
+ const tooltipData = chartCtx.tooltip.data;
64
+
65
+ // Get the x-axis label value from the raw tooltip data (e.g. a Date or month string)
66
+ const dataLabel = tooltipData != null ? chartCtx.x(tooltipData) : undefined;
67
+
68
+ const key = labelKey ?? item?.label ?? item?.key ?? 'value';
69
+ const itemConfig = getPayloadConfigFromPayload(
70
+ chart.config,
71
+ item,
72
+ key,
73
+ tooltipData as Record<string, unknown> | null
74
+ );
75
+
76
+ let value: unknown;
77
+ if (!labelKey && typeof label === 'string') {
78
+ value = chart.config[label as keyof typeof chart.config]?.label ?? label;
79
+ } else if (labelKey) {
80
+ value = itemConfig?.label ?? dataLabel;
81
+ } else {
82
+ value = dataLabel;
83
+ }
84
+
85
+ if (value === undefined) return null;
86
+ if (!labelFormatter) return value;
87
+ return labelFormatter(value, visibleSeries);
88
+ });
89
+
90
+ const nestLabel = $derived(visibleSeries.length === 1 && indicator !== 'dot');
91
+ </script>
92
+
93
+ {#snippet TooltipLabel()}
94
+ {#if formattedLabel}
95
+ <div class={cn('font-medium', labelClassName)}>
96
+ {#if typeof formattedLabel === 'function'}
97
+ {@render formattedLabel()}
98
+ {:else}
99
+ {formattedLabel}
100
+ {/if}
101
+ </div>
102
+ {/if}
103
+ {/snippet}
104
+
105
+ <TooltipPrimitive.Root variant="none">
106
+ <div
107
+ bind:this={ref}
108
+ class={cn(
109
+ 'border-border/50 bg-background grid min-w-[9rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl',
110
+ className
111
+ )}
112
+ {...restProps}
113
+ >
114
+ {#if !nestLabel}
115
+ {@render TooltipLabel()}
116
+ {/if}
117
+ <div class="grid gap-1.5">
118
+ {#each visibleSeries as item, i (item.key + i)}
119
+ {@const key = `${nameKey || item.key || item.label || 'value'}`}
120
+ {@const itemConfig = getPayloadConfigFromPayload(
121
+ chart.config,
122
+ item,
123
+ key,
124
+ chartCtx.tooltip.data
125
+ )}
126
+ {@const indicatorColor = color || item.config?.color || item.color}
127
+ <div
128
+ class={cn(
129
+ '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5',
130
+ indicator === 'dot' && 'items-center'
131
+ )}
132
+ >
133
+ {#if formatter && item.value !== undefined && item.label}
134
+ {@render formatter({
135
+ value: item.value,
136
+ name: item.label,
137
+ item,
138
+ index: i,
139
+ payload: visibleSeries
140
+ })}
141
+ {:else}
142
+ {#if itemConfig?.icon}
143
+ <itemConfig.icon />
144
+ {:else if !hideIndicator}
145
+ <div
146
+ style="--color-bg: {indicatorColor}; --color-border: {indicatorColor};"
147
+ class={cn('shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)', {
148
+ 'size-2.5': indicator === 'dot',
149
+ 'h-full w-1': indicator === 'line',
150
+ 'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed',
151
+ 'my-0.5': nestLabel && indicator === 'dashed'
152
+ })}
153
+ ></div>
154
+ {/if}
155
+ <div
156
+ class={cn(
157
+ 'flex flex-1 shrink-0 justify-between leading-none',
158
+ nestLabel ? 'items-end' : 'items-center'
159
+ )}
160
+ >
161
+ <div class="grid gap-1.5">
162
+ {#if nestLabel}
163
+ {@render TooltipLabel()}
164
+ {/if}
165
+ <span class="text-muted-foreground">
166
+ {itemConfig?.label || item.label}
167
+ </span>
168
+ </div>
169
+ {#if item.value !== undefined}
170
+ <span class="text-foreground font-mono font-medium tabular-nums">
171
+ {item.value.toLocaleString()}
172
+ </span>
173
+ {/if}
174
+ </div>
175
+ {/if}
176
+ </div>
177
+ {/each}
178
+ </div>
179
+ </div>
180
+ </TooltipPrimitive.Root>
src/lib/components/ui/chart/chart-utils.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Tooltip } from 'layerchart';
2
+ import { getContext, setContext, type Component, type Snippet } from 'svelte';
3
+
4
+ export const THEMES = { light: '', dark: '.dark' } as const;
5
+
6
+ export type ChartConfig = {
7
+ [k in string]: {
8
+ label?: string;
9
+ icon?: Component;
10
+ } & (
11
+ | { color?: string; theme?: never }
12
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
13
+ );
14
+ };
15
+
16
+ export type ExtractSnippetParams<T> = T extends Snippet<[infer P]> ? P : never;
17
+
18
+ export type TooltipPayload = Tooltip.TooltipSeries;
19
+
20
+ // Helper to extract item config from a payload.
21
+ export function getPayloadConfigFromPayload(
22
+ config: ChartConfig,
23
+ payload: TooltipPayload,
24
+ key: string,
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
+ data?: Record<string, any> | null
27
+ ) {
28
+ if (typeof payload !== 'object' || payload === null) return undefined;
29
+
30
+ const payloadConfig =
31
+ 'config' in payload && typeof payload.config === 'object' && payload.config !== null
32
+ ? payload.config
33
+ : undefined;
34
+
35
+ let configLabelKey: string = key;
36
+
37
+ if (payload.key === key) {
38
+ configLabelKey = payload.key;
39
+ } else if (payload.label === key) {
40
+ configLabelKey = payload.label;
41
+ } else if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {
42
+ configLabelKey = payload[key as keyof typeof payload] as string;
43
+ } else if (
44
+ payloadConfig !== undefined &&
45
+ key in payloadConfig &&
46
+ typeof payloadConfig[key as keyof typeof payloadConfig] === 'string'
47
+ ) {
48
+ configLabelKey = payloadConfig[key as keyof typeof payloadConfig] as string;
49
+ } else if (data != null && key in data && typeof data[key] === 'string') {
50
+ configLabelKey = data[key] as string;
51
+ }
52
+
53
+ return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
54
+ }
55
+
56
+ type ChartContextValue = {
57
+ config: ChartConfig;
58
+ };
59
+
60
+ const chartContextKey = Symbol('chart-context');
61
+
62
+ export function setChartContext(value: ChartContextValue) {
63
+ return setContext(chartContextKey, value);
64
+ }
65
+
66
+ export function useChart() {
67
+ return getContext<ChartContextValue>(chartContextKey);
68
+ }
src/lib/components/ui/chart/index.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import ChartContainer from './chart-container.svelte';
2
+ import ChartTooltip from './chart-tooltip.svelte';
3
+
4
+ export { getPayloadConfigFromPayload, type ChartConfig } from './chart-utils.js';
5
+
6
+ export { ChartContainer, ChartTooltip, ChartContainer as Container, ChartTooltip as Tooltip };
src/routes/+page.svelte CHANGED
@@ -18,6 +18,7 @@
18
  import SunIcon from 'phosphor-svelte/lib/SunIcon';
19
  import MoonIcon from 'phosphor-svelte/lib/MoonIcon';
20
  import MatchTable from '$lib/components/match-table/match-table.svelte';
 
21
  import { buildMapRows, buildMatchRows, buildRoundRows } from '$lib/components/match-table/rows';
22
  import { mapColumns, matchColumns, roundColumns } from '$lib/components/match-table/columns';
23
  import type {
@@ -413,6 +414,8 @@
413
  </Tabs.Root>
414
  </section>
415
 
 
 
416
  <!-- Citation -->
417
  <section class="mx-auto mt-16 max-w-3xl">
418
  <h2
 
18
  import SunIcon from 'phosphor-svelte/lib/SunIcon';
19
  import MoonIcon from 'phosphor-svelte/lib/MoonIcon';
20
  import MatchTable from '$lib/components/match-table/match-table.svelte';
21
+ import StatsSection from '$lib/components/stats-section.svelte';
22
  import { buildMapRows, buildMatchRows, buildRoundRows } from '$lib/components/match-table/rows';
23
  import { mapColumns, matchColumns, roundColumns } from '$lib/components/match-table/columns';
24
  import type {
 
414
  </Tabs.Root>
415
  </section>
416
 
417
+ <StatsSection matches={data.matches} />
418
+
419
  <!-- Citation -->
420
  <section class="mx-auto mt-16 max-w-3xl">
421
  <h2