From 00ba929d5249607998e1740a8497202efa6325d5 Mon Sep 17 00:00:00 2001 From: Glauco Azevedo Date: Thu, 18 Jun 2026 16:44:36 -0300 Subject: [PATCH 1/3] feat(sankey): arestas de fluxo com borda inferior e bezier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substitui ribbons de stroke por paths preenchidos que ligam as bordas inferiores dos nós com espessura proporcional às cadeiras e curvas Bezier. Fluxos entre anos saltados via buildOrientationFlowLinks. Co-authored-by: Cursor --- src/data/sankeyGraph.ts | 37 ++++----- src/sankey/councilSankey.ts | 32 +++++--- src/sankey/sankeyLinkPath.ts | 147 +++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 32 deletions(-) create mode 100644 src/sankey/sankeyLinkPath.ts diff --git a/src/data/sankeyGraph.ts b/src/data/sankeyGraph.ts index 8f6eefe..8caef0b 100644 --- a/src/data/sankeyGraph.ts +++ b/src/data/sankeyGraph.ts @@ -1,3 +1,7 @@ +import { + buildOrientationFlowLinks, + computeNodeValues, +} from "../sankey/sankeyLinkPath"; import { listarAnos, parseSankeyNodeName } from "../utils/years"; import type { CsvSankeyRow, @@ -27,12 +31,12 @@ export function graphFromCsv( : rows; const nodeNames = new Set(); - const links: SankeyLink[] = []; + const csvLinks: SankeyLink[] = []; for (const row of filteredRows) { nodeNames.add(row.source); nodeNames.add(row.target); - links.push({ + csvLinks.push({ source: row.source, target: row.target, value: Number.parseInt(row.value, 10) || 0, @@ -40,6 +44,8 @@ export function graphFromCsv( } const nodes: SankeyNode[] = [...nodeNames].sort().map((name) => ({ name })); + const nodeValues = computeNodeValues(nodes, csvLinks); + const links = buildOrientationFlowLinks(nodes, nodeValues); return indexGraph({ nodes, links }); } @@ -77,26 +83,15 @@ export function graphFromVereadores( } } - const links: SankeyLink[] = []; - for (let i = 0; i < anos.length - 1; i++) { - const fromYear = anos[i]; - const toYear = anos[i + 1]; - const fromBuckets = bucketsByYear.get(fromYear); - const toBuckets = bucketsByYear.get(toYear); - if (!fromBuckets || !toBuckets) continue; - - for (const [orientacao, value] of fromBuckets) { - if (value <= 0) continue; - const source = `${fromYear}-${String(orientacao).padStart(2, "0")}`; - const target = `${toYear}-${String(orientacao).padStart(2, "0")}`; - const targetValue = toBuckets.get(orientacao) ?? 0; - nodeNames.add(source); - nodeNames.add(target); - links.push({ source, target, value: Math.min(value, targetValue) || value }); - } - } - const nodes: SankeyNode[] = [...nodeNames].sort().map((name) => ({ name })); + const nodeValues = new Map(); + for (const node of nodes) { + const parsed = parseSankeyNodeName(node.name); + if (!parsed) continue; + const buckets = bucketsByYear.get(String(parsed.year)); + nodeValues.set(node.name, buckets?.get(parsed.orientacao) ?? 0); + } + const links = buildOrientationFlowLinks(nodes, nodeValues); return indexGraph({ nodes, links }); } diff --git a/src/sankey/councilSankey.ts b/src/sankey/councilSankey.ts index 8d664c3..39039e2 100644 --- a/src/sankey/councilSankey.ts +++ b/src/sankey/councilSankey.ts @@ -2,7 +2,6 @@ import { drag, format, select, type Selection } from "d3"; import { sankey, sankeyJustify, - sankeyLinkHorizontal, type SankeyLink, type SankeyNode, } from "d3-sankey"; @@ -14,6 +13,7 @@ import { SANKEY_NODE_WIDTH, sankeyLayoutExtent, } from "./sankeyLayout"; +import { sankeyFlowPath } from "./sankeyLinkPath"; type LayoutNode = SankeyNode<{}, {}> & { name: string }; type LayoutLink = SankeyLink; @@ -83,10 +83,10 @@ export class CouncilSankey { .extent(extent); const { nodes, links } = layout(layoutGraph); - const linkPath = sankeyLinkHorizontal(); + const flowPath = (d: LayoutLink) => + sankeyFlowPath(d.source as LayoutNode, d.target as LayoutNode); const activeLinks = links.filter((l) => l.value > 0); const activeNodes = nodes.filter((n) => (n.value ?? 0) > 0); - const graphForUpdate = { nodes: activeNodes, links: activeLinks }; const innerG = this.chartG.append("g").attr("class", "sankey-inner"); const dragContainer = innerG.node()!; @@ -94,17 +94,28 @@ export class CouncilSankey { const linkSelection = innerG .append("g") .attr("class", "sankey-links") - .attr("fill", "none") - .attr("stroke", "#000") - .attr("stroke-opacity", 0.2) .style("pointer-events", "none") .selectAll("path") .data(activeLinks) .join("path") .attr("class", (d) => (d.value ? "link" : "link zero")) - .attr("d", linkPath) - .attr("stroke-width", (d) => Math.max(1, d.width ?? 0)) - .sort((a, b) => (b.width ?? 0) - (a.width ?? 0)); + .attr("d", flowPath) + .attr("stroke", "none") + .style("fill", (d) => + orientacaoToColor( + orientacaoFromSankeyNodeName((d.source as LayoutNode).name), + ), + ) + .style("fill-opacity", 0.45) + .sort((a, b) => { + const aH = + ((a.target as LayoutNode).y1 ?? 0) - + ((a.target as LayoutNode).y0 ?? 0); + const bH = + ((b.target as LayoutNode).y1 ?? 0) - + ((b.target as LayoutNode).y0 ?? 0); + return bH - aH; + }); linkSelection.append("title").text((d) => { const s = d.source as LayoutNode; @@ -158,8 +169,7 @@ export class CouncilSankey { "transform", `translate(${d.x0 ?? 0},${y})`, ); - layout.update(graphForUpdate); - linkSelection.attr("d", linkPath); + linkSelection.attr("d", flowPath); }); nodeSelection.selectAll("rect").call(nodeDrag); diff --git a/src/sankey/sankeyLinkPath.ts b/src/sankey/sankeyLinkPath.ts new file mode 100644 index 0000000..1ff943f --- /dev/null +++ b/src/sankey/sankeyLinkPath.ts @@ -0,0 +1,147 @@ +import { parseSankeyNodeName } from "../utils/years"; + +export interface SankeyFlowNode { + x0?: number; + x1?: number; + y0?: number; + y1?: number; +} + +export function orientationFromNode(name: string): number | null { + return parseSankeyNodeName(name)?.orientacao ?? null; +} + +export function yearFromNode(name: string): number | null { + return parseSankeyNodeName(name)?.year ?? null; +} + +export interface FlowLink { + source: string; + target: string; + value: number; +} + +/** Cadeiras por nó: fluxo de entrada (eleição anterior); na primeira coluna, fluxo de saída. */ +export function computeNodeValues( + nodes: { name: string }[], + links: { + source: string | number; + target: string | number; + value: number; + }[], +): Map { + const values = new Map(); + const incoming = new Map(); + const outgoing = new Map(); + + for (const node of nodes) { + values.set(node.name, 0); + incoming.set(node.name, 0); + outgoing.set(node.name, 0); + } + + for (const link of links) { + const srcName = + typeof link.source === "string" + ? link.source + : nodes[link.source as number]?.name; + const tgtName = + typeof link.target === "string" + ? link.target + : nodes[link.target as number]?.name; + if (!srcName || !tgtName) continue; + const v = link.value; + incoming.set(tgtName, Math.max(incoming.get(tgtName) ?? 0, v)); + outgoing.set(srcName, Math.max(outgoing.get(srcName) ?? 0, v)); + } + + for (const node of nodes) { + const inc = incoming.get(node.name) ?? 0; + const out = outgoing.get(node.name) ?? 0; + const hasIncomingLink = links.some((link) => { + const tgtName = + typeof link.target === "string" + ? link.target + : nodes[link.target as number]?.name; + return tgtName === node.name; + }); + values.set(node.name, hasIncomingLink ? inc : out); + } + + return values; +} + +/** + * Para cada orientação, liga cada ano ao próximo ano em que o grupo tem cadeiras (> 0), + * ignorando anos intermediários ausentes ou com valor zero. + */ +export function buildOrientationFlowLinks( + nodes: { name: string }[], + nodeValues: Map, +): FlowLink[] { + const byOrientation = new Map< + number, + { year: number; name: string; value: number }[] + >(); + + for (const node of nodes) { + const parsed = parseSankeyNodeName(node.name); + if (!parsed) continue; + const value = nodeValues.get(node.name) ?? 0; + if (value <= 0) continue; + + let list = byOrientation.get(parsed.orientacao); + if (!list) { + list = []; + byOrientation.set(parsed.orientacao, list); + } + list.push({ year: parsed.year, name: node.name, value }); + } + + const links: FlowLink[] = []; + for (const entries of byOrientation.values()) { + entries.sort((a, b) => a.year - b.year); + for (let i = 0; i < entries.length - 1; i++) { + const src = entries[i]!; + const tgt = entries[i + 1]!; + links.push({ + source: src.name, + target: tgt.name, + value: Math.min(src.value, tgt.value) || src.value || tgt.value, + }); + } + } + return links; +} + +/** + * Faixa preenchida entre a borda inferior direita da origem e a borda inferior esquerda + * do destino, com espessura igual à altura de cada nó (cadeiras). + */ +export function sankeyFlowPath( + source: SankeyFlowNode, + target: SankeyFlowNode, + curvature = 0.5, +): string { + const xSource = source.x1 ?? 0; + const xTarget = target.x0 ?? 0; + const ySourceBottom = source.y1 ?? 0; + const yTargetBottom = target.y1 ?? 0; + const hSource = Math.max(1, (source.y1 ?? 0) - (source.y0 ?? 0)); + const hTarget = Math.max(1, (target.y1 ?? 0) - (target.y0 ?? 0)); + + const ySourceTop = ySourceBottom - hSource; + const yTargetTop = yTargetBottom - hTarget; + + const gap = xTarget - xSource; + const c1 = xSource + gap * curvature; + const c2 = xTarget - gap * curvature; + + return [ + `M${xSource},${ySourceTop}`, + `C${c1},${ySourceTop} ${c2},${yTargetTop} ${xTarget},${yTargetTop}`, + `L${xTarget},${yTargetBottom}`, + `C${c2},${yTargetBottom} ${c1},${ySourceBottom} ${xSource},${ySourceBottom}`, + "Z", + ].join(""); +} From 9f295b209537e745a241c8e751a9cf72707930f6 Mon Sep 17 00:00:00 2001 From: Glauco Azevedo Date: Thu, 18 Jun 2026 16:44:40 -0300 Subject: [PATCH 2/3] =?UTF-8?q?test(sankey):=20cobrir=20gera=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20paths=20e=20anos=20saltados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adiciona testes para sankeyFlowPath, buildOrientationFlowLinks e renderização do CouncilSankey; happy-dom para ambiente DOM nos testes. Co-authored-by: Cursor --- package-lock.json | 675 +++++++++++++++++++++++++++++------ package.json | 2 + tests/councilSankey.test.ts | 8 +- tests/sankeyGraph.test.ts | 40 ++- tests/sankeyLinkPath.test.ts | 84 +++++ 5 files changed, 691 insertions(+), 118 deletions(-) create mode 100644 tests/sankeyLinkPath.test.ts diff --git a/package-lock.json b/package-lock.json index f00dad7..7541a90 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@types/node": "^22.10.0", "@types/topojson-client": "^3.1.5", "@types/topojson-specification": "^1.0.5", + "esbuild": "^0.24.0", "happy-dom": "^15.11.7", "sirv": "^3.0.0", "typescript": "^5.7.2", @@ -27,9 +28,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "cpu": [ "ppc64" ], @@ -44,9 +45,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", "cpu": [ "arm" ], @@ -61,9 +62,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", "cpu": [ "arm64" ], @@ -78,9 +79,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", "cpu": [ "x64" ], @@ -95,9 +96,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", "cpu": [ "arm64" ], @@ -112,9 +113,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", "cpu": [ "x64" ], @@ -129,9 +130,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", "cpu": [ "arm64" ], @@ -146,9 +147,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", "cpu": [ "x64" ], @@ -163,9 +164,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", "cpu": [ "arm" ], @@ -180,9 +181,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", "cpu": [ "arm64" ], @@ -197,9 +198,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", "cpu": [ "ia32" ], @@ -214,9 +215,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", "cpu": [ "loong64" ], @@ -231,9 +232,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", "cpu": [ "mips64el" ], @@ -248,9 +249,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", "cpu": [ "ppc64" ], @@ -265,9 +266,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", "cpu": [ "riscv64" ], @@ -282,9 +283,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", "cpu": [ "s390x" ], @@ -299,9 +300,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", "cpu": [ "x64" ], @@ -316,9 +317,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", "cpu": [ "arm64" ], @@ -333,9 +334,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", "cpu": [ "x64" ], @@ -350,9 +351,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", "cpu": [ "arm64" ], @@ -367,9 +368,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", "cpu": [ "x64" ], @@ -401,9 +402,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", "cpu": [ "x64" ], @@ -418,9 +419,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", "cpu": [ "arm64" ], @@ -435,9 +436,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", "cpu": [ "ia32" ], @@ -452,9 +453,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", "cpu": [ "x64" ], @@ -1831,9 +1832,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1844,32 +1845,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" } }, "node_modules/estree-walker": { @@ -2898,6 +2898,473 @@ } } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/vitest": { "version": "2.1.9", "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", diff --git a/package.json b/package.json index ca05e0f..3b7c3d9 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "@types/topojson-specification": "^1.0.5", "happy-dom": "^15.11.7", "sirv": "^3.0.0", + "esbuild": "^0.24.0", + "happy-dom": "^15.11.7", "typescript": "^5.7.2", "vite": "^6.0.3", "vitest": "^2.1.8" diff --git a/tests/councilSankey.test.ts b/tests/councilSankey.test.ts index 1d543b4..83a46e5 100644 --- a/tests/councilSankey.test.ts +++ b/tests/councilSankey.test.ts @@ -11,7 +11,7 @@ function miniGraph(): SankeyGraph { } describe("CouncilSankey", () => { - it("renderiza nós e links no SVG", () => { + it("renderiza nós e links preenchidos no SVG", () => { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.id = "sankeyChart"; const message = document.createElement("p"); @@ -23,7 +23,11 @@ describe("CouncilSankey", () => { expect(svg.classList.contains("is-visible")).toBe(true); expect(message.hidden).toBe(true); expect(svg.querySelectorAll(".node rect").length).toBeGreaterThan(0); - expect(svg.querySelectorAll(".sankey-links path").length).toBeGreaterThan(0); + const paths = svg.querySelectorAll(".sankey-links path"); + expect(paths.length).toBeGreaterThan(0); + const firstPath = paths[0] as SVGPathElement; + expect(firstPath.getAttribute("d")).toMatch(/Z$/); + expect(firstPath.style.fillOpacity).toBe("0.45"); }); it("mostra estado vazio quando grafo não tem links", () => { diff --git a/tests/sankeyGraph.test.ts b/tests/sankeyGraph.test.ts index a5828dc..dcda784 100644 --- a/tests/sankeyGraph.test.ts +++ b/tests/sankeyGraph.test.ts @@ -36,24 +36,40 @@ describe("graphFromVereadores", () => { }); }); +function loadCamaraRjRows() { + return readFileSync(join(process.cwd(), "data/camara-rj.csv"), "utf-8") + .trim() + .split("\n") + .slice(1) + .map((line) => { + const [source, target, value] = line.split(","); + return { source, target, value }; + }); +} + describe("graphFromCsv Rio", () => { it("produz links com value > 0 e índices válidos", () => { - const rows = readFileSync( - join(process.cwd(), "data/camara-rj.csv"), - "utf-8", - ) - .trim() - .split("\n") - .slice(1) - .map((line) => { - const [source, target, value] = line.split(","); - return { source, target, value }; - }); - const graph = graphFromCsv(rows, listarAnos()); + const graph = graphFromCsv(loadCamaraRjRows(), listarAnos()); expect(graph.links.some((l) => l.value > 0)).toBe(true); for (const link of graph.links) { expect(link.source).toBeGreaterThanOrEqual(0); expect(link.target).toBeLessThan(graph.nodes.length); } }); + + it("cria ligação direta quando ano intermediário tem zero cadeiras", () => { + const rows = [ + { source: "2004-03", target: "2008-03", value: "5" }, + { source: "2008-03", target: "2012-03", value: "0" }, + { source: "2012-03", target: "2016-03", value: "3" }, + ]; + const graph = graphFromCsv(rows, listarAnos()); + const names = graph.nodes.map((n) => n.name); + const idx = (name: string) => names.indexOf(name); + const skipLink = graph.links.find( + (l) => l.source === idx("2008-03") && l.target === idx("2016-03"), + ); + expect(skipLink).toBeDefined(); + expect(skipLink!.value).toBe(3); + }); }); diff --git a/tests/sankeyLinkPath.test.ts b/tests/sankeyLinkPath.test.ts new file mode 100644 index 0000000..4a1dde1 --- /dev/null +++ b/tests/sankeyLinkPath.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vitest"; +import { + buildOrientationFlowLinks, + computeNodeValues, + orientationFromNode, + sankeyFlowPath, + yearFromNode, +} from "../src/sankey/sankeyLinkPath"; + +describe("orientationFromNode / yearFromNode", () => { + it("extrai ano e orientação do nome do nó", () => { + expect(orientationFromNode("2008-03")).toBe(3); + expect(yearFromNode("2008-03")).toBe(2008); + expect(orientationFromNode("invalid")).toBeNull(); + }); +}); + +describe("sankeyFlowPath", () => { + it("gera path fechado com curvas Bezier", () => { + const d = sankeyFlowPath( + { x0: 10, x1: 60, y0: 20, y1: 70 }, + { x0: 200, x1: 250, y0: 40, y1: 90 }, + ); + expect(d).toMatch(/^M/); + expect(d).toContain("C"); + expect(d).toMatch(/Z$/); + }); + + it("ancora nas bordas inferiores com espessura da altura do nó", () => { + const d = sankeyFlowPath( + { x0: 0, x1: 50, y0: 10, y1: 60 }, + { x0: 150, x1: 200, y0: 30, y1: 80 }, + ); + expect(d).toContain("M50,10"); + expect(d).toContain("L150,80"); + expect(d).toContain("50,60"); + }); +}); + +describe("buildOrientationFlowLinks", () => { + const nodes = [ + { name: "2004-02" }, + { name: "2008-02" }, + { name: "2012-02" }, + { name: "2016-02" }, + { name: "2004-03" }, + { name: "2008-03" }, + { name: "2012-03" }, + { name: "2016-03" }, + ]; + + it("liga anos consecutivos com cadeiras", () => { + const csvLinks = [ + { source: "2004-02", target: "2008-02", value: 1 }, + { source: "2008-02", target: "2012-02", value: 1 }, + { source: "2012-02", target: "2016-02", value: 1 }, + ]; + const values = computeNodeValues(nodes, csvLinks); + + const links = buildOrientationFlowLinks(nodes, values); + const o2 = links.filter((l) => l.source.endsWith("-02")); + expect(o2).toHaveLength(3); + expect(o2[0]).toEqual({ source: "2004-02", target: "2008-02", value: 1 }); + expect(o2[1]).toEqual({ source: "2008-02", target: "2012-02", value: 1 }); + expect(o2[2]).toEqual({ source: "2012-02", target: "2016-02", value: 1 }); + }); + + it("pula anos intermediários sem cadeiras", () => { + const csvLinks = [ + { source: "2004-03", target: "2008-03", value: 5 }, + { source: "2008-03", target: "2012-03", value: 0 }, + { source: "2012-03", target: "2016-03", value: 3 }, + ]; + const values = computeNodeValues(nodes, csvLinks); + expect(values.get("2012-03")).toBe(0); + + const links = buildOrientationFlowLinks(nodes, values); + const skip = links.find( + (l) => l.source === "2008-03" && l.target === "2016-03", + ); + expect(skip).toBeDefined(); + expect(skip!.value).toBe(3); + }); +}); From f44d3544c1a111d82b5bc17ce72918cf6371010d Mon Sep 17 00:00:00 2001 From: Glauco Azevedo Date: Thu, 18 Jun 2026 16:45:36 -0300 Subject: [PATCH 3/3] fix: remover happy-dom duplicado no package.json Co-authored-by: Cursor --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 3b7c3d9..ee3b8c1 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "happy-dom": "^15.11.7", "sirv": "^3.0.0", "esbuild": "^0.24.0", - "happy-dom": "^15.11.7", "typescript": "^5.7.2", "vite": "^6.0.3", "vitest": "^2.1.8"