From c9d44c4414ac15bcde3a7634a86f785e59ab31bb Mon Sep 17 00:00:00 2001 From: amtoaer Date: Mon, 13 Jul 2026 14:28:40 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=20layerchart=20?= =?UTF-8?q?=E5=B9=B6=E5=81=9A=E5=AF=B9=E5=BA=94=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/bun.lock | 32 +++++---- web/package.json | 2 +- .../components/custom/my-chart-tooltip.svelte | 47 +++++++++----- .../ui/chart/chart-container.svelte | 5 +- .../components/ui/chart/chart-style.svelte | 7 +- .../components/ui/chart/chart-tooltip.svelte | 65 +++++++++++++------ .../lib/components/ui/chart/chart-utils.ts | 30 +++++---- 7 files changed, 117 insertions(+), 71 deletions(-) diff --git a/web/bun.lock b/web/bun.lock index 0f3270c..13c91ec 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -27,7 +27,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.20.0", "globals": "^17.7.0", - "layerchart": "2.0.0-next.43", + "layerchart": "^2.0.1", "mode-watcher": "^1.1.0", "prettier": "^3.9.5", "prettier-plugin-organize-imports": "^4.3.0", @@ -47,9 +47,9 @@ }, }, "packages": { - "@dagrejs/dagre": ["@dagrejs/dagre@1.1.8", "", { "dependencies": { "@dagrejs/graphlib": "2.2.4" } }, "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw=="], + "@dagrejs/dagre": ["@dagrejs/dagre@2.0.4", "", { "dependencies": { "@dagrejs/graphlib": "3.0.4" } }, "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA=="], - "@dagrejs/graphlib": ["@dagrejs/graphlib@2.2.4", "", {}, "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw=="], + "@dagrejs/graphlib": ["@dagrejs/graphlib@3.0.4", "", {}, "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg=="], "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], @@ -103,13 +103,13 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@layerstack/svelte-actions": ["@layerstack/svelte-actions@1.0.1-next.14", "", { "dependencies": { "@floating-ui/dom": "^1.7.0", "@layerstack/utils": "2.0.0-next.14", "d3-scale": "^4.0.2" } }, "sha512-MPBmVaB+GfNHvBkg5nJkPG18smoXKvsvJRpsdWnrUBfca+TieZLoaEzNxDH+9LG11dIXP9gghsXt1mUqbbyAsA=="], + "@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=="], - "@layerstack/svelte-state": ["@layerstack/svelte-state@0.1.0-next.19", "", { "dependencies": { "@layerstack/utils": "2.0.0-next.14" } }, "sha512-yCYoQAIbeP8y1xmOB/r0+UundgP4JFnpNURgMki+26TotzoqrZ5oLpHvhPSVm60ks+buR3ebDBTeUFdHzxwzQQ=="], + "@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=="], - "@layerstack/tailwind": ["@layerstack/tailwind@2.0.0-next.17", "", { "dependencies": { "@layerstack/utils": "^2.0.0-next.14", "clsx": "^2.1.1", "d3-array": "^3.2.4", "lodash-es": "^4.17.21", "tailwind-merge": "^3.2.0" } }, "sha512-ZSn6ouqpnzB6DKzSKLVwrUBOQsrzpDA/By2/ba9ApxgTGnaD1nyqNwrvmZ+kswdAwB4YnrGEAE4VZkKrB2+DaQ=="], + "@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=="], - "@layerstack/utils": ["@layerstack/utils@2.0.0-next.14", "", { "dependencies": { "d3-array": "^3.2.4", "d3-time": "^3.1.0", "d3-time-format": "^4.1.0", "lodash-es": "^4.17.21" } }, "sha512-1I2CS0Cwgs53W35qVg1eBdYhB/CiPvL3s0XE61b8jWkTHxgjBF65yYNgXjW74kv7WI7GsJcWMNBufPd0rnu9kA=="], + "@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=="], "@lucide/svelte": ["@lucide/svelte@1.24.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-yXwewA7ANQ5hfaSDrvsecosWjZn5RglzeXUZRSnxeANBskpNwblOkEJTqD0ujDdNKIKL8E9eVc2U/P3ziJr7OA=="], @@ -203,6 +203,10 @@ "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], @@ -213,6 +217,8 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/node": ["@types/node@25.0.6", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q=="], @@ -291,8 +297,12 @@ "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], @@ -451,7 +461,7 @@ "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="], - "layerchart": ["layerchart@2.0.0-next.43", "", { "dependencies": { "@dagrejs/dagre": "^1.1.5", "@layerstack/svelte-actions": "1.0.1-next.14", "@layerstack/svelte-state": "0.1.0-next.19", "@layerstack/tailwind": "2.0.0-next.17", "@layerstack/utils": "2.0.0-next.14", "d3-array": "^3.2.4", "d3-color": "^3.1.0", "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", "lodash-es": "^4.17.21", "memoize": "^10.1.0", "runed": "^0.31.1" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-1Ywm38NdzHWKwgaAHq3EcqshIgsq+pylntSnVWAVazXUk/NsxPcxdpR3tMt3ySjWV0ZPBBgLs78sdVf7FTgd+g=="], + "layerchart": ["layerchart@2.0.1", "", { "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-ss19hUjjZawMni60dgYsm+ZT/Fq/HYh8YSQt6D9xhN+Dc9HB6TH77FOeOl1avf7Wlpc2MG7X16SFuMGh8nR+Eg=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -485,8 +495,6 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -677,8 +685,6 @@ "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - "@layerstack/tailwind/@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=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], @@ -707,7 +713,7 @@ "espree/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "layerchart/runed": ["runed@0.31.1", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-v3czcTnO+EJjiPvD4dwIqfTdHLZ8oH0zJheKqAHh9QMViY7Qb29UlAMRpX7ZtHh7AFqV60KmfxaJ9QMy+L1igQ=="], + "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=="], "mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="], diff --git a/web/package.json b/web/package.json index c1fa936..2e4cf42 100644 --- a/web/package.json +++ b/web/package.json @@ -20,7 +20,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.20.0", "globals": "^17.7.0", - "layerchart": "2.0.0-next.43", + "layerchart": "^2.0.1", "mode-watcher": "^1.1.0", "prettier": "^3.9.5", "prettier-plugin-organize-imports": "^4.3.0", diff --git a/web/src/lib/components/custom/my-chart-tooltip.svelte b/web/src/lib/components/custom/my-chart-tooltip.svelte index 95264c4..1f263cd 100644 --- a/web/src/lib/components/custom/my-chart-tooltip.svelte +++ b/web/src/lib/components/custom/my-chart-tooltip.svelte @@ -6,7 +6,7 @@ useChart, type TooltipPayload } from '../ui/chart/chart-utils.js'; - import { getTooltipContext, Tooltip as TooltipPrimitive } from 'layerchart'; + import { getChartContext, Tooltip as TooltipPrimitive } from 'layerchart'; import type { Snippet } from 'svelte'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -61,27 +61,34 @@ } = $props(); const chart = useChart(); - const tooltipCtx = getTooltipContext(); + const chartCtx = getChartContext(); + const visibleSeries = $derived( + chartCtx.tooltip.series.filter((series: TooltipPayload) => series.value !== undefined) + ); const formattedLabel = $derived.by(() => { - if (hideLabel || !tooltipCtx.payload?.length) return null; + if (hideLabel || !visibleSeries.length) return null; - const [item] = tooltipCtx.payload; - const key = labelKey ?? item?.label ?? item?.name ?? 'value'; + const [item] = visibleSeries; + const tooltipData = chartCtx.tooltip.data; + const dataLabel = tooltipData != null ? chartCtx.x(tooltipData) : undefined; + const key = labelKey ?? item?.label ?? item?.key ?? 'value'; - const itemConfig = getPayloadConfigFromPayload(chart.config, item, key); + const itemConfig = getPayloadConfigFromPayload(chart.config, item, key, tooltipData); const value = !labelKey && typeof label === 'string' ? (chart.config[label as keyof typeof chart.config]?.label ?? label) - : (itemConfig?.label ?? item.label); + : labelKey + ? (itemConfig?.label ?? dataLabel) + : dataLabel; if (value === undefined) return null; if (!labelFormatter) return value; - return labelFormatter(value, tooltipCtx.payload); + return labelFormatter(value, visibleSeries); }); - const nestLabel = $derived(tooltipCtx.payload.length === 1 && indicator !== 'dot'); + const nestLabel = $derived(visibleSeries.length === 1 && indicator !== 'dot'); {#snippet TooltipLabel()} @@ -98,6 +105,7 @@
- {#each tooltipCtx.payload as item, i (item.key + i)} - {@const key = `${nameKey || item.key || item.name || 'value'}`} - {@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)} - {@const indicatorColor = color || item.payload?.color || item.color} + {#each visibleSeries as item, i (item.key + i)} + {@const key = `${nameKey || item.key || item.label || 'value'}`} + {@const itemConfig = getPayloadConfigFromPayload( + chart.config, + item, + key, + chartCtx.tooltip.data + )} + {@const indicatorColor = color || item.config?.color || item.color}
svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5', indicator === 'dot' && 'items-center' )} > - {#if formatter && item.value !== undefined && item.name} + {#if formatter && item.value !== undefined && item.label} {@render formatter({ value: item.value, - name: item.name, + name: item.label, item, index: i, - payload: tooltipCtx.payload + payload: visibleSeries })} {:else} {#if itemConfig?.icon} @@ -151,7 +164,7 @@ {@render TooltipLabel()} {/if} - {itemConfig?.label || item.name} + {itemConfig?.label || item.label}
{#if item.value !== undefined} diff --git a/web/src/lib/components/ui/chart/chart-container.svelte b/web/src/lib/components/ui/chart/chart-container.svelte index 31146c9..4734286 100644 --- a/web/src/lib/components/ui/chart/chart-container.svelte +++ b/web/src/lib/components/ui/chart/chart-container.svelte @@ -17,8 +17,7 @@ config: ChartConfig; } = $props(); - // svelte-ignore state_referenced_locally - const chartId = `chart-${id || uid.replace(/:/g, '')}`; + const chartId = $derived(`chart-${id || uid.replace(/:/g, '')}`); setChartContext({ get config() { @@ -36,7 +35,7 @@ // Overrides // // Stroke around dots/marks when hovering - '[&_.stroke-white]:stroke-transparent', + '[&_.lc-highlight-point]:stroke-transparent', // override the default stroke color of lines '[&_.lc-line]:stroke-border/50', diff --git a/web/src/lib/components/ui/chart/chart-style.svelte b/web/src/lib/components/ui/chart/chart-style.svelte index a2a6eb6..18efa41 100644 --- a/web/src/lib/components/ui/chart/chart-style.svelte +++ b/web/src/lib/components/ui/chart/chart-style.svelte @@ -11,7 +11,7 @@ if (!colorConfig || !colorConfig.length) return; const themeContents = []; - for (let [_theme, prefix] of Object.entries(THEMES)) { + for (const [_theme, prefix] of Object.entries(THEMES)) { let content = `${prefix} [data-chart=${id}] {\n`; const color = colorConfig.map(([key, itemConfig]) => { const theme = _theme as keyof typeof itemConfig.theme; @@ -30,7 +30,8 @@ {#if themeContents} {#key id} - - {@html ``} + + {themeContents} + {/key} {/if} diff --git a/web/src/lib/components/ui/chart/chart-tooltip.svelte b/web/src/lib/components/ui/chart/chart-tooltip.svelte index c1f79af..404aa9c 100644 --- a/web/src/lib/components/ui/chart/chart-tooltip.svelte +++ b/web/src/lib/components/ui/chart/chart-tooltip.svelte @@ -2,7 +2,7 @@ import { cn, type WithElementRef, type WithoutChildren } from '$lib/utils.js'; import type { HTMLAttributes } from 'svelte/elements'; import { getPayloadConfigFromPayload, useChart, type TooltipPayload } from './chart-utils.js'; - import { getTooltipContext, Tooltip as TooltipPrimitive } from 'layerchart'; + import { getChartContext, Tooltip as TooltipPrimitive } from 'layerchart'; import type { Snippet } from 'svelte'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -49,27 +49,46 @@ } = $props(); const chart = useChart(); - const tooltipCtx = getTooltipContext(); + const chartCtx = getChartContext(); + + // Filter to series with defined values (important for item-based charts like Pie/Arc + // where only the hovered item has a value) + const visibleSeries = $derived( + chartCtx.tooltip.series.filter((s: TooltipPayload) => s.value !== undefined) + ); const formattedLabel = $derived.by(() => { - if (hideLabel || !tooltipCtx.payload?.length) return null; + if (hideLabel || !visibleSeries?.length) return null; - const [item] = tooltipCtx.payload; - const key = labelKey ?? item?.label ?? item?.name ?? 'value'; + const [item] = visibleSeries; + const tooltipData = chartCtx.tooltip.data; - const itemConfig = getPayloadConfigFromPayload(chart.config, item, key); + // Get the x-axis label value from the raw tooltip data (e.g. a Date or month string) + const dataLabel = tooltipData != null ? chartCtx.x(tooltipData) : undefined; - const value = - !labelKey && typeof label === 'string' - ? (chart.config[label as keyof typeof chart.config]?.label ?? label) - : (itemConfig?.label ?? item.label); + const key = labelKey ?? item?.label ?? item?.key ?? 'value'; + const itemConfig = getPayloadConfigFromPayload( + chart.config, + item, + key, + tooltipData as Record | null + ); + + let value: unknown; + if (!labelKey && typeof label === 'string') { + value = chart.config[label as keyof typeof chart.config]?.label ?? label; + } else if (labelKey) { + value = itemConfig?.label ?? dataLabel; + } else { + value = dataLabel; + } if (value === undefined) return null; if (!labelFormatter) return value; - return labelFormatter(value, tooltipCtx.payload); + return labelFormatter(value, visibleSeries); }); - const nestLabel = $derived(tooltipCtx.payload.length === 1 && indicator !== 'dot'); + const nestLabel = $derived(visibleSeries.length === 1 && indicator !== 'dot'); {#snippet TooltipLabel()} @@ -86,6 +105,7 @@
- {#each tooltipCtx.payload as item, i (item.key + i)} - {@const key = `${nameKey || item.key || item.name || 'value'}`} - {@const itemConfig = getPayloadConfigFromPayload(chart.config, item, key)} - {@const indicatorColor = color || item.payload?.color || item.color} + {#each visibleSeries as item, i (item.key + i)} + {@const key = `${nameKey || item.key || item.label || 'value'}`} + {@const itemConfig = getPayloadConfigFromPayload( + chart.config, + item, + key, + chartCtx.tooltip.data + )} + {@const indicatorColor = color || item.config?.color || item.color}
svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5', indicator === 'dot' && 'items-center' )} > - {#if formatter && item.value !== undefined && item.name} + {#if formatter && item.value !== undefined && item.label} {@render formatter({ value: item.value, - name: item.name, + name: item.label, item, index: i, - payload: tooltipCtx.payload + payload: visibleSeries })} {:else} {#if itemConfig?.icon} @@ -139,7 +164,7 @@ {@render TooltipLabel()} {/if} - {itemConfig?.label || item.name} + {itemConfig?.label || item.label}
{#if item.value !== undefined} diff --git a/web/src/lib/components/ui/chart/chart-utils.ts b/web/src/lib/components/ui/chart/chart-utils.ts index f2e682d..3c0cced 100644 --- a/web/src/lib/components/ui/chart/chart-utils.ts +++ b/web/src/lib/components/ui/chart/chart-utils.ts @@ -1,5 +1,5 @@ import type { Tooltip } from 'layerchart'; -import { getContext, setContext, type Component, type ComponentProps, type Snippet } from 'svelte'; +import { getContext, setContext, type Component, type Snippet } from 'svelte'; export const THEMES = { light: '', dark: '.dark' } as const; @@ -15,37 +15,39 @@ export type ChartConfig = { export type ExtractSnippetParams = T extends Snippet<[infer P]> ? P : never; -export type TooltipPayload = ExtractSnippetParams< - ComponentProps['children'] ->['payload'][number]; +export type TooltipPayload = Tooltip.TooltipSeries; // Helper to extract item config from a payload. export function getPayloadConfigFromPayload( config: ChartConfig, payload: TooltipPayload, - key: string + key: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: Record | null ) { if (typeof payload !== 'object' || payload === null) return undefined; - const payloadPayload = - 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null - ? payload.payload + const payloadConfig = + 'config' in payload && typeof payload.config === 'object' && payload.config !== null + ? payload.config : undefined; let configLabelKey: string = key; if (payload.key === key) { configLabelKey = payload.key; - } else if (payload.name === key) { - configLabelKey = payload.name; + } else if (payload.label === key) { + configLabelKey = payload.label; } else if (key in payload && typeof payload[key as keyof typeof payload] === 'string') { configLabelKey = payload[key as keyof typeof payload] as string; } else if ( - payloadPayload !== undefined && - key in payloadPayload && - typeof payloadPayload[key as keyof typeof payloadPayload] === 'string' + payloadConfig !== undefined && + key in payloadConfig && + typeof payloadConfig[key as keyof typeof payloadConfig] === 'string' ) { - configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string; + configLabelKey = payloadConfig[key as keyof typeof payloadConfig] as string; + } else if (data != null && key in data && typeof data[key] === 'string') { + configLabelKey = data[key] as string; } return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];