From 5195c2f666ad14b8205b6d0f5591279d7366272f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Oct 2026 11:20:30 +0000 Subject: [PATCH 1/5] feat(muix): implement network-weighted Adopted from 5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69 (5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69), written by gemini-4-argon outside the pipeline. --- .../implementations/javascript/muix.tsx | 652 +++++++++++------- 1 file changed, 404 insertions(+), 248 deletions(-) diff --git a/plots/network-weighted/implementations/javascript/muix.tsx b/plots/network-weighted/implementations/javascript/muix.tsx index 04f72337286..ad8acfe2bfc 100644 --- a/plots/network-weighted/implementations/javascript/muix.tsx +++ b/plots/network-weighted/implementations/javascript/muix.tsx @@ -3,225 +3,286 @@ // Library: muix 7.29.1 | JavaScript 22.23.2 // Quality: 90/100 | Created: 2026-09-02 //# anyplot-orientation: square + import { useState } from "react"; -import { ChartContainer } from "@mui/x-charts/ChartContainer"; +import { ChartsContainer } from "@mui/x-charts/ChartsContainer"; import { useXScale, useYScale, useDrawingArea } from "@mui/x-charts/hooks"; const t = window.ANYPLOT_TOKENS; + +// Constants const TITLE = "network-weighted · javascript · muix · anyplot.ai"; +const TITLE_H = 58; +const LEGEND_H = 112; +const AREA = 4.5; +const ITERATIONS = 500; +const INITIAL_TEMP = 0.12; +const COOLING_FACTOR = 0.99; +const PAD_MULT = 1.25; +const NODE_MIN_R = 15; +const NODE_MAX_R = 40; +const EDGE_MIN_W = 2; +const EDGE_MAX_W = 13; -// --- Data: research-lab co-authorship network (in-memory, deterministic) ---- -// Four research domains, colored by the Imprint categorical palette in -// canonical order (abstract groups, no semantic-color exception applies). -const DOMAINS = ["Life Sciences", "Physical Sciences", "Computer Science", "Engineering"]; +// Columns: set these to your data's column names +const NODE_ID = "Lab code"; // role: node +const NODE_LABEL = "Research institute"; // role: label +const DOMAIN = "Research domain"; // role: group +const SOURCE = "Source lab"; // role: source +const TARGET = "Partner lab"; // role: target +const WEIGHT = "Co-authored papers"; // role: weight -const nodes = [ - { id: "GEN", label: "Genomics Lab", domain: 0 }, - { id: "IMM", label: "Immunology Institute", domain: 0 }, - { id: "NEU", label: "Neuroscience Institute", domain: 0 }, - { id: "MAR", label: "Marine Biology Station", domain: 0 }, - { id: "QPL", label: "Quantum Physics Lab", domain: 1 }, - { id: "MAT", label: "Materials Science Institute", domain: 1 }, - { id: "AST", label: "Astrophysics Observatory", domain: 1 }, - { id: "CLI", label: "Climate Science Center", domain: 1 }, - { id: "AIR", label: "AI Research Center", domain: 2 }, - { id: "DSI", label: "Data Science Institute", domain: 2 }, - { id: "ROB", label: "Robotics Lab", domain: 2 }, - { id: "CHE", label: "Chemical Engineering Lab", domain: 3 }, - { id: "BIO", label: "Bioengineering Institute", domain: 3 }, - { id: "ENV", label: "Environmental Engineering Lab", domain: 3 }, +// Data: replace this block with your own records +const nodes: Array> = [ + { [NODE_ID]: "GEN", [NODE_LABEL]: "Genomics Lab", [DOMAIN]: "Life Sciences" }, + { [NODE_ID]: "IMM", [NODE_LABEL]: "Immunology Institute", [DOMAIN]: "Life Sciences" }, + { [NODE_ID]: "NEU", [NODE_LABEL]: "Neuroscience Institute", [DOMAIN]: "Life Sciences" }, + { [NODE_ID]: "MAR", [NODE_LABEL]: "Marine Biology Station", [DOMAIN]: "Life Sciences" }, + { [NODE_ID]: "QPL", [NODE_LABEL]: "Quantum Physics Lab", [DOMAIN]: "Physical Sciences" }, + { [NODE_ID]: "MAT", [NODE_LABEL]: "Materials Science Institute", [DOMAIN]: "Physical Sciences" }, + { [NODE_ID]: "AST", [NODE_LABEL]: "Astrophysics Observatory", [DOMAIN]: "Physical Sciences" }, + { [NODE_ID]: "CLI", [NODE_LABEL]: "Climate Science Center", [DOMAIN]: "Physical Sciences" }, + { [NODE_ID]: "AIR", [NODE_LABEL]: "AI Research Center", [DOMAIN]: "Computer Science" }, + { [NODE_ID]: "DSI", [NODE_LABEL]: "Data Science Institute", [DOMAIN]: "Computer Science" }, + { [NODE_ID]: "ROB", [NODE_LABEL]: "Robotics Lab", [DOMAIN]: "Computer Science" }, + { [NODE_ID]: "CHE", [NODE_LABEL]: "Chemical Engineering Lab", [DOMAIN]: "Engineering" }, + { [NODE_ID]: "BIO", [NODE_LABEL]: "Bioengineering Institute", [DOMAIN]: "Engineering" }, + { [NODE_ID]: "ENV", [NODE_LABEL]: "Environmental Engineering Lab", [DOMAIN]: "Engineering" }, ]; -// Weight = co-authored papers, 2020-2024, between the two labs. -const edges = [ - { source: "GEN", target: "IMM", weight: 42 }, - { source: "GEN", target: "NEU", weight: 16 }, - { source: "GEN", target: "BIO", weight: 22 }, - { source: "IMM", target: "BIO", weight: 12 }, - { source: "IMM", target: "NEU", weight: 6 }, - { source: "NEU", target: "AIR", weight: 19 }, - { source: "MAR", target: "ENV", weight: 27 }, - { source: "MAR", target: "CLI", weight: 31 }, - { source: "QPL", target: "MAT", weight: 36 }, - { source: "QPL", target: "AST", weight: 21 }, - { source: "MAT", target: "CHE", weight: 33 }, - { source: "MAT", target: "ENV", weight: 9 }, - { source: "AST", target: "CLI", weight: 8 }, - { source: "CLI", target: "ENV", weight: 24 }, - { source: "AIR", target: "DSI", weight: 45 }, - { source: "AIR", target: "ROB", weight: 29 }, - { source: "DSI", target: "ROB", weight: 18 }, - { source: "DSI", target: "GEN", weight: 15 }, - { source: "DSI", target: "CLI", weight: 12 }, - { source: "DSI", target: "MAT", weight: 7 }, - { source: "DSI", target: "BIO", weight: 10 }, - { source: "ROB", target: "BIO", weight: 14 }, - { source: "CHE", target: "BIO", weight: 20 }, - { source: "CHE", target: "ENV", weight: 17 }, +const data: Array> = [ + { [SOURCE]: "GEN", [TARGET]: "IMM", [WEIGHT]: 42 }, + { [SOURCE]: "GEN", [TARGET]: "NEU", [WEIGHT]: 16 }, + { [SOURCE]: "GEN", [TARGET]: "BIO", [WEIGHT]: 22 }, + { [SOURCE]: "IMM", [TARGET]: "BIO", [WEIGHT]: 12 }, + { [SOURCE]: "IMM", [TARGET]: "NEU", [WEIGHT]: 6 }, + { [SOURCE]: "NEU", [TARGET]: "AIR", [WEIGHT]: 19 }, + { [SOURCE]: "MAR", [TARGET]: "ENV", [WEIGHT]: 27 }, + { [SOURCE]: "MAR", [TARGET]: "CLI", [WEIGHT]: 31 }, + { [SOURCE]: "QPL", [TARGET]: "MAT", [WEIGHT]: 36 }, + { [SOURCE]: "QPL", [TARGET]: "AST", [WEIGHT]: 21 }, + { [SOURCE]: "MAT", [TARGET]: "CHE", [WEIGHT]: 33 }, + { [SOURCE]: "MAT", [TARGET]: "ENV", [WEIGHT]: 9 }, + { [SOURCE]: "AST", [TARGET]: "CLI", [WEIGHT]: 8 }, + { [SOURCE]: "CLI", [TARGET]: "ENV", [WEIGHT]: 24 }, + { [SOURCE]: "AIR", [TARGET]: "DSI", [WEIGHT]: 45 }, + { [SOURCE]: "AIR", [TARGET]: "ROB", [WEIGHT]: 29 }, + { [SOURCE]: "DSI", [TARGET]: "ROB", [WEIGHT]: 18 }, + { [SOURCE]: "DSI", [TARGET]: "GEN", [WEIGHT]: 15 }, + { [SOURCE]: "DSI", [TARGET]: "CLI", [WEIGHT]: 12 }, + { [SOURCE]: "DSI", [TARGET]: "MAT", [WEIGHT]: 7 }, + { [SOURCE]: "DSI", [TARGET]: "BIO", [WEIGHT]: 10 }, + { [SOURCE]: "ROB", [TARGET]: "BIO", [WEIGHT]: 14 }, + { [SOURCE]: "CHE", [TARGET]: "BIO", [WEIGHT]: 20 }, + { [SOURCE]: "CHE", [TARGET]: "ENV", [WEIGHT]: 17 }, ]; -const nodeIndex = {}; -nodes.forEach((n, i) => { - nodeIndex[n.id] = i; -}); +// Plot: reads only data and the constants above +interface LabNode { + id: string; + label: string; + domainName: string; + domainIndex: number; + weightedDegree: number; + radius: number; + x: number; + y: number; +} + +const domains: string[] = []; +const nodeIndexById = new Map(); +const layoutNodes: LabNode[] = []; + +for (const row of nodes) { + const id = String(row[NODE_ID]); + const dom = String(row[DOMAIN]); + if (!domains.includes(dom)) domains.push(dom); + nodeIndexById.set(id, layoutNodes.length); + layoutNodes.push({ + id, + label: String(row[NODE_LABEL]), + domainName: dom, + domainIndex: domains.indexOf(dom), + weightedDegree: 0, + radius: NODE_MIN_R, + x: 0, + y: 0, + }); +} + +interface LabEdge { + sourceIdx: number; + targetIdx: number; + weight: number; + strokeWidth: number; + ratio: number; +} + +const edges: LabEdge[] = []; +for (const row of data) { + const sIdx = nodeIndexById.get(String(row[SOURCE]))!; + const tIdx = nodeIndexById.get(String(row[TARGET]))!; + const weight = Number(row[WEIGHT]); + layoutNodes[sIdx].weightedDegree += weight; + layoutNodes[tIdx].weightedDegree += weight; + edges.push({ sourceIdx: sIdx, targetIdx: tIdx, weight, strokeWidth: EDGE_MIN_W, ratio: 0 }); +} const edgeWeights = edges.map((e) => e.weight); -const MIN_WEIGHT = Math.min(...edgeWeights); -const MAX_WEIGHT = Math.max(...edgeWeights); +const minWeight = Math.min(...edgeWeights); +const maxWeight = Math.max(...edgeWeights); +const weightSpan = maxWeight - minWeight; + +for (const edge of edges) { + edge.ratio = (edge.weight - minWeight) / weightSpan; + edge.strokeWidth = EDGE_MIN_W + edge.ratio * (EDGE_MAX_W - EDGE_MIN_W); +} -// Weighted degree = sum of incident edge weights, drives node radius. -const weightedDegree = nodes.map(() => 0); -edges.forEach((e) => { - weightedDegree[nodeIndex[e.source]] += e.weight; - weightedDegree[nodeIndex[e.target]] += e.weight; -}); -const MIN_DEGREE = Math.min(...weightedDegree); -const MAX_DEGREE = Math.max(...weightedDegree); +const degrees = layoutNodes.map((n) => n.weightedDegree); +const minDegree = Math.min(...degrees); +const maxDegree = Math.max(...degrees); +const degreeSpan = maxDegree - minDegree; -// --- Force-directed layout (Fruchterman-Reingold, weighted attraction) ------ -// A tiny fixed-seed LCG stands in for a seeded RNG (the browser has none); -// only the initial scatter is randomized, the physics is fully deterministic. -let lcgState = 42; -function rand() { - lcgState = (lcgState * 1664525 + 1013904223) % 4294967296; - return lcgState / 4294967296; +for (const node of layoutNodes) { + const ratio = (node.weightedDegree - minDegree) / degreeSpan; + node.radius = NODE_MIN_R + ratio * (NODE_MAX_R - NODE_MIN_R); } -const N = nodes.length; -const AREA = 4.5; -const K = Math.sqrt(AREA / N); -const ITERATIONS = 500; +let lcgState = 42; +const nCount = layoutNodes.length; +const kForce = Math.sqrt(AREA / nCount); -const positions = nodes.map(() => ({ x: rand() * 2 - 1, y: rand() * 2 - 1 })); -let temperature = 0.12; +for (let i = 0; i < nCount; i += 1) { + lcgState = (lcgState * 1664525 + 1013904223) % 4294967296; + const rx = (lcgState / 4294967296) * 2 - 1; + lcgState = (lcgState * 1664525 + 1013904223) % 4294967296; + const ry = (lcgState / 4294967296) * 2 - 1; + layoutNodes[i].x = rx; + layoutNodes[i].y = ry; +} -for (let iter = 0; iter < ITERATIONS; iter++) { - const disp = positions.map(() => ({ x: 0, y: 0 })); +let temperature = INITIAL_TEMP; +for (let iter = 0; iter < ITERATIONS; iter += 1) { + const dispX = new Array(nCount).fill(0); + const dispY = new Array(nCount).fill(0); - // Repulsion between every node pair keeps the layout from collapsing. - for (let i = 0; i < N; i++) { - for (let j = i + 1; j < N; j++) { - const dx = positions[i].x - positions[j].x; - const dy = positions[i].y - positions[j].y; + for (let i = 0; i < nCount; i += 1) { + for (let j = i + 1; j < nCount; j += 1) { + const dx = layoutNodes[i].x - layoutNodes[j].x; + const dy = layoutNodes[i].y - layoutNodes[j].y; const dist = Math.sqrt(dx * dx + dy * dy) || 0.01; - const force = (K * K) / dist; + const force = (kForce * kForce) / dist; const ux = dx / dist; const uy = dy / dist; - disp[i].x += ux * force; - disp[i].y += uy * force; - disp[j].x -= ux * force; - disp[j].y -= uy * force; + dispX[i] += ux * force; + dispY[i] += uy * force; + dispX[j] -= ux * force; + dispY[j] -= uy * force; } } - // Attraction along edges — heavier weight pulls the pair closer together, - // so the layout itself, not just line width, communicates connection strength. - edges.forEach((e) => { - const i = nodeIndex[e.source]; - const j = nodeIndex[e.target]; - const dx = positions[i].x - positions[j].x; - const dy = positions[i].y - positions[j].y; + for (const edge of edges) { + const i = edge.sourceIdx; + const j = edge.targetIdx; + const dx = layoutNodes[i].x - layoutNodes[j].x; + const dy = layoutNodes[i].y - layoutNodes[j].y; const dist = Math.sqrt(dx * dx + dy * dy) || 0.01; - const wRatio = (e.weight - MIN_WEIGHT) / (MAX_WEIGHT - MIN_WEIGHT || 1); - const idealDist = K * (1.5 - 1.0 * wRatio); + const idealDist = kForce * (1.5 - 1.0 * edge.ratio); const force = (dist * dist) / idealDist; const ux = dx / dist; const uy = dy / dist; - disp[i].x -= ux * force; - disp[i].y -= uy * force; - disp[j].x += ux * force; - disp[j].y += uy * force; - }); + dispX[i] -= ux * force; + dispY[i] -= uy * force; + dispX[j] += ux * force; + dispY[j] += uy * force; + } - for (let i = 0; i < N; i++) { - const dx = disp[i].x; - const dy = disp[i].y; + for (let i = 0; i < nCount; i += 1) { + const dx = dispX[i]; + const dy = dispY[i]; const dist = Math.sqrt(dx * dx + dy * dy) || 0.01; const limited = Math.min(dist, temperature); - positions[i].x += (dx / dist) * limited; - positions[i].y += (dy / dist) * limited; + layoutNodes[i].x += (dx / dist) * limited; + layoutNodes[i].y += (dy / dist) * limited; } - temperature *= 0.99; + temperature *= COOLING_FACTOR; } -// Center the layout, then fit its actual (generally non-circular) bounding -// box to the drawing area independently per axis — a chain-shaped network -// like this one would otherwise sit inside a huge, mostly-empty circle. -const centroidX = positions.reduce((s, p) => s + p.x, 0) / N; -const centroidY = positions.reduce((s, p) => s + p.y, 0) / N; -positions.forEach((p) => { - p.x -= centroidX; - p.y -= centroidY; -}); - -const MARGIN = { top: 100, right: 60, bottom: 170, left: 60 }; -const { width: CANVAS_W, height: CANVAS_H } = window.ANYPLOT_SIZE; -const PAD = 1.25; // headroom for node radius + label above the outermost nodes -const rangeX = Math.max(...positions.map((p) => Math.abs(p.x))) || 1; -const rangeY = Math.max(...positions.map((p) => Math.abs(p.y))) || 1; -const X_HALF = rangeX * PAD; -const Y_HALF = rangeY * PAD; - -const NODE_MIN_R = 15; -const NODE_MAX_R = 40; -const EDGE_MIN_W = 2; -const EDGE_MAX_W = 13; - -function nodeRadius(i) { - const ratio = (weightedDegree[i] - MIN_DEGREE) / (MAX_DEGREE - MIN_DEGREE || 1); - return NODE_MIN_R + ratio * (NODE_MAX_R - NODE_MIN_R); +const centroidX = layoutNodes.reduce((s, n) => s + n.x, 0) / nCount; +const centroidY = layoutNodes.reduce((s, n) => s + n.y, 0) / nCount; +for (const node of layoutNodes) { + node.x -= centroidX; + node.y -= centroidY; } -function edgeWidth(weight) { - const ratio = (weight - MIN_WEIGHT) / (MAX_WEIGHT - MIN_WEIGHT || 1); - return EDGE_MIN_W + ratio * (EDGE_MAX_W - EDGE_MIN_W); -} +const rangeX = Math.max(...layoutNodes.map((n) => Math.abs(n.x))); +const rangeY = Math.max(...layoutNodes.map((n) => Math.abs(n.y))); +const xHalf = rangeX * PAD_MULT; +const yHalf = rangeY * PAD_MULT; -// Draw heaviest edges last so the strongest collaborations stay legible on top. const sortedEdges = [...edges].sort((a, b) => a.weight - b.weight); +const midWeight = Math.round((minWeight + maxWeight) / 2); +const weightSamples = [ + { weight: minWeight, strokeWidth: EDGE_MIN_W }, + { + weight: midWeight, + strokeWidth: EDGE_MIN_W + ((midWeight - minWeight) / weightSpan) * (EDGE_MAX_W - EDGE_MIN_W), + }, + { weight: maxWeight, strokeWidth: EDGE_MAX_W }, +]; -// --- Overlay: title drawn in the reserved top margin ------------------------- -function GraphTitle() { - return ( - - {TITLE} - - ); +interface HoverInfo { + label: string; + detail: string; + x: number; + y: number; } -// --- Overlay: edges + nodes, both hoverable for the interactive HTML export - -function NetworkOverlay({ onHoverChange }) { +function WeightedNetworkOverlay({ + hover, + onHoverChange, +}: { + hover: HoverInfo | null; + onHoverChange: (info: HoverInfo | null) => void; +}) { const xScale = useXScale(); const yScale = useYScale(); + const drawingArea = useDrawingArea(); return ( {sortedEdges.map((edge, i) => { - const s = positions[nodeIndex[edge.source]]; - const d = positions[nodeIndex[edge.target]]; - const x1 = xScale(s.x); - const y1 = yScale(s.y); - const x2 = xScale(d.x); - const y2 = yScale(d.y); - const width = edgeWidth(edge.weight); - const ratio = (edge.weight - MIN_WEIGHT) / (MAX_WEIGHT - MIN_WEIGHT || 1); - const tooltip = { - label: `${nodes[nodeIndex[edge.source]].label} ↔ ${nodes[nodeIndex[edge.target]].label}`, - detail: `${edge.weight} co-authored papers`, + const s = layoutNodes[edge.sourceIdx]; + const d = layoutNodes[edge.targetIdx]; + const x1 = Number(xScale(s.x)); + const y1 = Number(yScale(s.y)); + const x2 = Number(xScale(d.x)); + const y2 = Number(yScale(d.y)); + const tooltip: HoverInfo = { + label: `${s.label} ↔ ${d.label}`, + detail: `${edge.weight} (${WEIGHT})`, x: (x1 + x2) / 2, y: (y1 + y2) / 2, }; return ( - - - {/* Wider transparent hit path: the visible stroke is often too thin to hover reliably. */} + + onHoverChange(tooltip)} onMouseLeave={() => onHoverChange(null)} @@ -229,14 +290,13 @@ function NetworkOverlay({ onHoverChange }) { ); })} - {nodes.map((node, i) => { - const p = positions[i]; - const cx = xScale(p.x); - const cy = yScale(p.y); - const radius = nodeRadius(i); - const tooltip = { + {layoutNodes.map((node) => { + const cx = Number(xScale(node.x)); + const cy = Number(yScale(node.y)); + const radius = node.radius; + const tooltip: HoverInfo = { label: node.label, - detail: `${DOMAINS[node.domain]} · weighted degree ${weightedDegree[i]}`, + detail: `${node.domainName} · weighted degree ${node.weightedDegree}`, x: cx, y: cy - radius - 10, }; @@ -246,105 +306,201 @@ function NetworkOverlay({ onHoverChange }) { cx={cx} cy={cy} r={radius} - fill={t.palette[node.domain]} + fill={t.palette[node.domainIndex]} stroke={t.pageBg} strokeWidth={3} style={{ cursor: "pointer" }} onMouseEnter={() => onHoverChange(tooltip)} onMouseLeave={() => onHoverChange(null)} /> - + + {node.id} ); })} - - ); -} - -// --- Overlay: hover tooltip for edges and nodes ------------------------------ -function HoverTooltip({ hover }) { - if (!hover) return null; - const charWidth = 7.4; - const width = Math.max(hover.label.length, hover.detail.length) * charWidth + 24; - const height = 46; - const x = Math.min(Math.max(hover.x - width / 2, 8), CANVAS_W - width - 8); - const y = Math.max(hover.y - height - 12, 8); - return ( - - - - {hover.label} - - - {hover.detail} - - - ); -} - -// --- Overlay: domain-color legend + edge-weight scale, in the bottom margin - -function Legend() { - const drawingArea = useDrawingArea(); - const rowY = drawingArea.top + drawingArea.height + 55; - const swatchR = 9; - const groupGap = 225; - - const weightSamples = [MIN_WEIGHT, Math.round((MIN_WEIGHT + MAX_WEIGHT) / 2), MAX_WEIGHT]; - const weightRowY = rowY + 55; - const weightStartX = drawingArea.left; - return ( - - {DOMAINS.map((name, i) => { - const x = drawingArea.left + i * groupGap; - return ( - - - - {name} - - - ); - })} - - Edge width = co-authored papers · node size = weighted degree - - {weightSamples.map((w, i) => { - const x = weightStartX + i * 140; - const lineY = weightRowY + 12; - return ( - - - - {w} - - - ); - })} + {hover && ( + + + + {hover.label} + + + {hover.detail} + + + )} ); } -// --- Chart (default-exported component — the harness mounts it) ------------- export default function Chart() { - const [hover, setHover] = useState(null); + const { width, height } = window.ANYPLOT_SIZE; + const [hover, setHover] = useState(null); + const chartHeight = height - TITLE_H - LEGEND_H; + return ( - - - - - - +
+ {TITLE} +
+ + + +
+
+ {domains.map((name, i) => ( + + + {name} + + ))} +
+
+ {`Edge width = ${WEIGHT} · node size = weighted degree:`} + {weightSamples.map((sample) => ( + + + + + {sample.weight} + + ))} +
+
+ ); } From 92bdf04b1293cfbdf0adb8981a13e06dc8b8fd71 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Oct 2026 11:20:32 +0000 Subject: [PATCH 2/5] chore(muix): add metadata for network-weighted --- .../implementations/javascript/muix.tsx | 2 +- .../metadata/javascript/muix.yaml | 251 +----------------- 2 files changed, 13 insertions(+), 240 deletions(-) diff --git a/plots/network-weighted/implementations/javascript/muix.tsx b/plots/network-weighted/implementations/javascript/muix.tsx index ad8acfe2bfc..a17bab778b4 100644 --- a/plots/network-weighted/implementations/javascript/muix.tsx +++ b/plots/network-weighted/implementations/javascript/muix.tsx @@ -1,7 +1,7 @@ // anyplot.ai // network-weighted: Weighted Network Graph with Edge Thickness // Library: muix 7.29.1 | JavaScript 22.23.2 -// Quality: 90/100 | Created: 2026-09-02 +// Quality: pending | Created: 2026-09-02 //# anyplot-orientation: square import { useState } from "react"; diff --git a/plots/network-weighted/metadata/javascript/muix.yaml b/plots/network-weighted/metadata/javascript/muix.yaml index 92ca2ac6e92..4d8c7013481 100644 --- a/plots/network-weighted/metadata/javascript/muix.yaml +++ b/plots/network-weighted/metadata/javascript/muix.yaml @@ -1,249 +1,22 @@ +# Per-library metadata for muix implementation of network-weighted +# Auto-generated by impl-generate.yml + library: muix language: javascript specification_id: network-weighted created: '2026-09-02T04:31:39Z' -updated: '2026-09-02T04:45:02Z' -generated_by: claude-sonnet -workflow_run: 33590653437 +updated: '2026-10-24T11:20:31Z' +generated_by: gemini-4-argon +adopted_from: 5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69 (5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69) +workflow_run: 40089447860 issue: 3290 -language_version: 22.23.2 -library_version: 7.29.1 +language_version: 22.23.3 +library_version: 9.16.0 preview_url_light: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-light.png preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-dark.png preview_html_light: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-light.html preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-dark.html -quality_score: 90 +quality_score: null review: - strengths: - - Real force-directed (Fruchterman-Reingold) layout with weighted attraction — heavier - collaborations pull nodes closer together, so the layout itself, not just edge - thickness, communicates connection strength. - - 'Correct Imprint palette assignment: first series (Life Sciences) is #009E73, - remaining three domains follow canonical order 2-4; chrome (background, text) - is theme-correct in both renders.' - - Weighted-degree node sizing surfaces hub institutions (DSI, MAT, ENV) at a glance - — a genuine visual-hierarchy/storytelling win, not just a technical requirement. - - Heaviest edges are drawn last (sorted z-order) so the strongest collaborations - stay legible on top of overlapping lines. - - Deterministic reproducibility via a fixed-seed LCG for the initial layout scatter; - all data generated in-memory, no fetch/CDN. - - Idiomatic community MUI X composition — reuses `ChartContainer` plus `useXScale`/`useYScale`/`useDrawingArea` - hooks to build a custom network overlay, which is exactly the recommended community-surface - pattern for chart types MUI X has no native component for, and adds working hover - tooltips for the interactive HTML export. - - Title format matches the spec exactly; legend clearly explains both encodings - (domain color and weight-to-edge-width/node-size mapping) with sample values. - weaknesses: - - Lightest edges (e.g. IMM-NEU, weight=6) render at strokeOpacity as low as 0.22 - combined with strokeWidth=2, making the weakest connections fairly faint in both - themes — consider raising the opacity floor slightly (e.g. 0.3) so even the thinnest - edges stay comfortably legible at a glance. - - Title occupies roughly 45-48% of canvas width, on the low end of the guideline - range for a shorter custom title — a modest fontSize bump would give it slightly - more visual weight relative to the network below it. - - The overlay is split across several small components (GraphTitle, NetworkOverlay, - HoverTooltip, Legend) purely for organization — justified by the interactivity/physics - requirements, but a one-line comment naming the overall layout algorithm at the - top of the file would help future readers orient faster. - image_description: |- - Light render (plot-light.png): - Background: Warm off-white, matches #FAF8F1 — not pure white. - Chrome: Title "network-weighted · javascript · muix · anyplot.ai" centered top in dark ink, clearly legible. 14 node-ID labels (GEN, IMM, NEU, MAR, QPL, MAT, AST, CLI, ENV, AIR, DSI, ROB, CHE, BIO) placed above each node in bold dark text, all readable with no overlap. Bottom legend row explains 4 domain colors (Life Sciences, Physical Sciences, Computer Science, Engineering) plus a caption "Edge width = co-authored papers · node size = weighted degree" and a 3-point weight scale (6 / 26 / 45) with matching line thicknesses. - Data: Nodes colored by domain using the Imprint categorical palette — Life Sciences green #009E73 (first series, correct), Physical Sciences lavender, Computer Science blue, Engineering ochre, in canonical palette order. Edges are dark, semi-transparent lines whose width and opacity scale with co-authorship weight; node radius scales with weighted degree (DSI, MAT, ENV visibly largest = hubs). Layout is a force-directed graph where heavier edges pull node pairs closer. - Legibility verdict: PASS — all title, node-label, and legend text is clearly readable against the light background; no light-on-light issues. - - Dark render (plot-dark.png): - Background: Warm near-black, matches #1A1A17 — not pure black. - Chrome: Same title, node labels, and legend layout as the light render, all rendered in light ink/off-white text, clearly legible against the dark background. No dark-on-dark text anywhere — title, node IDs, domain legend, and weight-scale labels are all bright and readable. - Data: Node and edge colors are visually identical to the light render (same Imprint palette hues, same green-first domain, same edge width/opacity encoding) — confirming only chrome flipped between themes, not data colors. Node outline stroke uses the page background color in both themes, giving crisp separation from crossing edges. - Legibility verdict: PASS — no dark-on-dark failures; all text and data elements read clearly on the dark surface, brand green remains vivid. - criteria_checklist: - visual_quality: - score: 28 - max: 30 - items: - - id: VQ-01 - name: Text Legibility - score: 7 - max: 8 - passed: true - comment: All font sizes explicitly set (title 28px, node labels 16px, legend - 14-16px), readable in both themes; title proportion slightly on the low - end but not a violation - - id: VQ-02 - name: No Overlap - score: 6 - max: 6 - passed: true - comment: No text-on-text or text-on-data collisions; edge crossings are expected - for a force-directed graph, not a defect - - id: VQ-03 - name: Element Visibility - score: 5 - max: 6 - passed: true - comment: Edge width (2-13px) and node radius (15-40px) well adapted to a 14-node/24-edge - graph; lightest edges (opacity 0.22) are slightly faint - - id: VQ-04 - name: Color Accessibility - score: 2 - max: 2 - passed: true - comment: Domain colors distinguishable without relying on red-green; page-background - node stroke adds separation from crossing edges - - id: VQ-05 - name: Layout & Canvas - score: 4 - max: 4 - passed: true - comment: Network fills a large majority of the canvas with balanced margins; - legend sits directly below the plot, not isolated; nothing cut off - - id: VQ-06 - name: Axis Labels & Title - score: 2 - max: 2 - passed: true - comment: N/A for a network graph (no coordinate axes) — full credit; title - and legend captions are descriptive - - id: VQ-07 - name: Palette Compliance - score: 2 - max: 2 - passed: true - comment: 'First domain (Life Sciences) is #009E73, remaining 3 domains follow - canonical Imprint order; backgrounds and chrome theme-correct in both renders' - design_excellence: - score: 16 - max: 20 - items: - - id: DE-01 - name: Aesthetic Sophistication - score: 6 - max: 8 - comment: Custom force-directed layout with weighted attraction, intentional - size/opacity hierarchy — clearly above a configured default, though not - quite publication-tier polish - - id: DE-02 - name: Visual Refinement - score: 5 - max: 6 - comment: No competing chrome (no grid/spines/axis lines), generous whitespace, - restrained tooltip and legend styling - - id: DE-03 - name: Data Storytelling - score: 5 - max: 6 - comment: Node size = weighted degree and edge width/opacity = collaboration - strength let the viewer immediately spot hub institutions and strong ties; - heaviest edges drawn on top reinforces the hierarchy - spec_compliance: - score: 15 - max: 15 - items: - - id: SC-01 - name: Plot Type - score: 5 - max: 5 - comment: Correct weighted network graph with force-directed positioning - - id: SC-02 - name: Required Features - score: 4 - max: 4 - comment: Edge thickness by weight, weight legend, weighted-degree node sizing, - spring layout with weight-influenced attraction all present - - id: SC-03 - name: Data Mapping - score: 3 - max: 3 - comment: All 14 nodes and 24 edges rendered and visible - - id: SC-04 - name: Title & Legend - score: 3 - max: 3 - comment: Title exactly matches 'network-weighted · javascript · muix · anyplot.ai'; - legend labels match domain data - data_quality: - score: 14 - max: 15 - items: - - id: DQ-01 - name: Feature Coverage - score: 5 - max: 6 - comment: Wide weight range (6-45) with visibly distinct thin/thick edges, - hub vs. peripheral nodes, cross-domain and intra-domain edges - - id: DQ-02 - name: Realistic Context - score: 5 - max: 5 - comment: Real, neutral, comprehensible scenario — academic co-authorship network - across research domains - - id: DQ-03 - name: Appropriate Scale - score: 4 - max: 4 - comment: Paper-count weights and weighted-degree sums are internally consistent - and plausible for the domain - code_quality: - score: 9 - max: 10 - items: - - id: CQ-01 - name: KISS Structure - score: 2 - max: 3 - comment: Component decomposition (GraphTitle, NetworkOverlay, HoverTooltip, - Legend) is justified by the interactivity/physics needs, though it's more - structure than a minimal chart - - id: CQ-02 - name: Reproducibility - score: 2 - max: 2 - comment: Fixed-seed LCG (state=42) for deterministic layout scatter - - id: CQ-03 - name: Clean Imports - score: 2 - max: 2 - comment: Only used hooks/components imported - - id: CQ-04 - name: Code Elegance - score: 2 - max: 2 - comment: Complexity is warranted by the physics simulation MUI X doesn't provide - natively; no fake UI - - id: CQ-05 - name: Output & API - score: 1 - max: 1 - comment: Correct default-exported Chart component, skipAnimation set, harness - owns file output - library_mastery: - score: 8 - max: 10 - items: - - id: LM-01 - name: Idiomatic Usage - score: 4 - max: 5 - comment: Uses ChartContainer + useXScale/useYScale/useDrawingArea, the recommended - community pattern for custom chart types MUI X has no built-in component - for - - id: LM-02 - name: Distinctive Features - score: 4 - max: 5 - comment: The hooks-based custom-overlay composition is a signature MUI X capability, - not trivially replicated in another library with minor syntax changes - verdict: APPROVED -impl_tags: - dependencies: [] - techniques: - - force-directed-layout - - custom-legend - - hover-tooltips - patterns: - - data-generation - dataprep: [] - styling: - - alpha-blending - - edge-highlighting + strengths: [] + weaknesses: [] From c1e2f0645d604d140f651b1e0a59831054338f15 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Oct 2026 18:17:55 +0000 Subject: [PATCH 3/5] chore(muix): store the review for network-weighted --- .../implementations/javascript/muix.tsx | 4 +- .../metadata/javascript/muix.yaml | 267 +++++++++++++++++- 2 files changed, 262 insertions(+), 9 deletions(-) diff --git a/plots/network-weighted/implementations/javascript/muix.tsx b/plots/network-weighted/implementations/javascript/muix.tsx index a17bab778b4..457bfbfbc27 100644 --- a/plots/network-weighted/implementations/javascript/muix.tsx +++ b/plots/network-weighted/implementations/javascript/muix.tsx @@ -1,7 +1,7 @@ // anyplot.ai // network-weighted: Weighted Network Graph with Edge Thickness -// Library: muix 7.29.1 | JavaScript 22.23.2 -// Quality: pending | Created: 2026-09-02 +// Library: muix 9.16.0 | JavaScript 22.23.3 +// Quality: 85/100 | Updated: 2026-10-24 //# anyplot-orientation: square import { useState } from "react"; diff --git a/plots/network-weighted/metadata/javascript/muix.yaml b/plots/network-weighted/metadata/javascript/muix.yaml index 4d8c7013481..61aaf3134fc 100644 --- a/plots/network-weighted/metadata/javascript/muix.yaml +++ b/plots/network-weighted/metadata/javascript/muix.yaml @@ -1,11 +1,8 @@ -# Per-library metadata for muix implementation of network-weighted -# Auto-generated by impl-generate.yml - library: muix language: javascript specification_id: network-weighted created: '2026-09-02T04:31:39Z' -updated: '2026-10-24T11:20:31Z' +updated: '2026-10-24T18:17:55Z' generated_by: gemini-4-argon adopted_from: 5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69 (5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69) workflow_run: 40089447860 @@ -16,7 +13,263 @@ preview_url_light: https://storage.googleapis.com/anyplot-images/plots/network-w preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-dark.png preview_html_light: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-light.html preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-dark.html -quality_score: null +quality_score: 85 review: - strengths: [] - weaknesses: [] + strengths: + - 'Edge width scales linearly from 2 px (weight 6) to 13 px (weight 45): the thinnest + edge stays visible in both themes and the thickest never buries a node' + - Spring layout shortens the ideal length of heavy edges, so strongly collaborating + labs sit closer together and the four research domains form readable clusters + - Node radius encodes weighted degree and node color the research domain, both named + in the legend band; the edge-width legend shows labeled samples for 6, 26 and + 45 papers + - 'Theme-correct in both renders: Imprint domain colors identical, edges and labels + from ink tokens, page-colored node outlines and label chips separate nodes and + text from edges' + - 'MUI X composition idiom: a custom SVG overlay inside ChartsContainer positioned + with useXScale/useYScale, plus a React-state hover card for edges and nodes in + the HTML output' + weaknesses: + - 'VQ-02 (both): the AIR and IMM label chips (16 px text on a 42x20 page-colored + rect above each node) sit on the short ROB-AIR (weight 29) and GEN-IMM (weight + 42) edges and hide most of their visible length, so those thicknesses cannot be + read along the edge → keep label chips off edge segments, e.g. place each label + on the side of its node away from its heaviest edge, or shrink the chip to the + text width. Likely cause: every label is fixed at y = cy - radius - 11 regardless + of where the node''s edges leave.' + - 'VQ-03 (both): the edge-width legend samples are drawn at a fixed strokeOpacity + 0.55, while the edges use 0.22 + ratio*0.4 (0.22 for weight 6, 0.62 for weight + 45), so the ''6'' sample is darker and the ''45'' sample lighter than the edges + they explain → draw each sample with the same opacity formula as the edges. Likely + cause: strokeOpacity={0.55} on the legend instead of 0.22 + ratio*0.4.' + - 'Suggestion: show the full institute name (already in NODE_LABEL) as a small caption + list or in the legend band so the three-letter codes can be decoded in the static + PNG.' + image_description: |- + Light render (plot-light.png): + Background: warm off-white #FAF8F1 across the full 2400x2400 square canvas, no axes or grid. + Chrome: bold dark title "network-weighted · javascript · muix · anyplot.ai" centered at top (~50% width); bold dark three-letter node codes (MAR, AST, CLI, ENV, QPL, MAT, CHE, DSI, ROB, AIR, BIO, GEN, NEU, IMM) on page-colored chips above each node; bottom legend band with a domain row (Life Sciences, Physical Sciences, Computer Science, Engineering) and an edge-width row "Edge width = Co-authored papers · node size = weighted degree:" with sample lines labeled 6, 26, 45 - all readable. + Data: 15 nodes in four Imprint colors (Life Sciences #009E73 green first, purple, blue, ochre) with page-colored outlines, radius 15-40 px by weighted degree (DSI, GEN, AIR, MAT largest); 24 gray edges 2-13 px wide with opacity rising with weight; AIR-DSI the heaviest dark edge, QPL-MAT and GEN-IMM next. The AIR and IMM label chips cover most of the short ROB-AIR and GEN-IMM edges. + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: warm near-black #1A1A17, no axes or grid. + Chrome: title, node codes and legend text all flip to light ink and are clearly readable; no dark-on-dark text; label chips use the dark page color. + Data: node colors identical to the light render (green, purple, blue, ochre); edges flip to light gray with the same width and opacity ramp, the thinnest NEU-IMM edge still visible; layout identical. + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 28 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 8 + max: 8 + passed: true + comment: All sizes set explicitly (title 22 px, node labels 16 px bold, legend + 14-15 px CSS); every element readable in both themes; title ~50% of width + - id: VQ-02 + name: No Overlap + score: 5 + max: 6 + passed: true + comment: The AIR and IMM label chips sit on the short ROB-AIR (29) and GEN-IMM + (42) edges and hide most of their length (defect line) + - id: VQ-03 + name: Element Visibility + score: 5 + max: 6 + passed: true + comment: Thinnest edge (6, 2 px) visible in both themes; the edge-width legend + samples use a fixed 0.55 opacity while the edges run 0.22-0.62, so the samples + do not match the marks they explain (defect line) + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Four Imprint hues (green, purple, blue, ochre) plus node labels; + no red-green sole signal + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Square 2400x2400, network centered, legend band clear of the graph, + nothing clipped + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: N/A - network has no axes; the legend caption names the weight column + in reader text ('Co-authored papers') + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First domain (Life Sciences) = #009E73, domains in canonical Imprint + order via t.palette[i]; backgrounds #FAF8F1 / #1A1A17; edges and chrome + from ink tokens; data colors identical in both renders' + design_excellence: + score: 11 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: 'Beyond baseline: page-colored node outlines separating nodes from + edges, bold monospace-like node codes on semi-opaque page-colored label + chips, clear title > labels > legend hierarchy' + - id: DE-02 + name: Visual Refinement + score: 3 + max: 6 + passed: true + comment: Edges drawn light-to-heavy so heavy links sit on top, rounded line + caps, a two-row legend band with gap-tuned spacing; label chips still sit + on some short edges + - id: DE-03 + name: Data Storytelling + score: 3 + max: 6 + passed: true + comment: Edge opacity rises with weight alongside width, so the heaviest links + (AIR-DSI, QPL-MAT, GEN-IMM) read first and the weak cross-domain bridges + recede + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Weighted network graph with weight-scaled edges + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Proportional edge width 2-13 px; spring layout with weight-shortened + ideal lengths; weight legend with labeled sample widths (6/26/45); node + size = weighted degree and color = domain, both named; nodes and labels + drawn over edges + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Layout-positioned type (exempt); all 15 nodes and 24 edges drawn, + widths match weights + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title 'network-weighted · javascript · muix · anyplot.ai'; domain + legend and edge-width legend in reader text + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Hubs (DSI, MAT, GEN), heavy intra-domain clusters, light cross-domain + bridges, peripheral nodes (AST, NEU), full 6-45 weight range + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Research-institute co-authorship network (a spec application), neutral + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: 6-45 co-authored papers per lab pair is plausible; legend values + 6/26/45 checked against min/mid/max of the data (6, round(25.5)=26, 45) + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: Columns -> Data -> Plot zones; the one overlay component is required + by MUI X's hook-based composition + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Hard-coded data, fixed-seed LCG for initial positions + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: useState, ChartsContainer, useXScale/useYScale/useDrawingArea all + used + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Written-out Fruchterman-Reingold layout is allowed (no layout helper + in @mui/x-charts); hover tooltip is real interactivity, not fake UI + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: 'Default export, sized from ANYPLOT_SIZE, skipAnimation, v9 ChartsContainer + with position: ''none'' axes' + library_mastery: + score: 6 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Custom SVG overlay inside ChartsContainer placed with useXScale/useYScale + - the MUI X composition idiom for marks it has no series type for + - id: LM-02 + name: Distinctive Features + score: 2 + max: 5 + passed: true + comment: useDrawingArea keeps the React-state hover card inside the plot area; + legends are hand-built HTML rather than ChartsLegend + verdict: REJECTED + model: claude-opus-5-5 + criteria_version: qc-899b86ffde.aqr-869a70087c.sg-c73b4641dd.lib-f7283eb626 + rendered_at: '2026-10-24T11:20:52Z' +impl_tags: + dependencies: [] + techniques: + - custom-legend + - hover-tooltips + - html-export + - layer-composition + patterns: + - data-generation + dataprep: + - normalization + styling: + - minimal-chrome + - alpha-blending + - edge-highlighting From b9c9e4893f9a15a7fc167d457a8abd06adb4a89a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Oct 2026 18:20:14 +0000 Subject: [PATCH 4/5] fix(muix): address review feedback for network-weighted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attempt 1/4 - fixes based on AI review - VQ-02: node label chips sit on the bisector of the widest angular gap between each node's edges and are sized to the text, so no chip covers an edge segment - VQ-03: edge-width legend samples use the same opacity formula as the edges Declined: - "Suggestion: show the full institute names as a caption list" — suggestion, not taken --- .../implementations/javascript/muix.tsx | 72 ++++++++++++++----- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/plots/network-weighted/implementations/javascript/muix.tsx b/plots/network-weighted/implementations/javascript/muix.tsx index 457bfbfbc27..e73bf867153 100644 --- a/plots/network-weighted/implementations/javascript/muix.tsx +++ b/plots/network-weighted/implementations/javascript/muix.tsx @@ -220,16 +220,38 @@ const rangeY = Math.max(...layoutNodes.map((n) => Math.abs(n.y))); const xHalf = rangeX * PAD_MULT; const yHalf = rangeY * PAD_MULT; +const edgeOpacity = (ratio: number) => 0.22 + ratio * 0.4; + const sortedEdges = [...edges].sort((a, b) => a.weight - b.weight); const midWeight = Math.round((minWeight + maxWeight) / 2); -const weightSamples = [ - { weight: minWeight, strokeWidth: EDGE_MIN_W }, - { - weight: midWeight, - strokeWidth: EDGE_MIN_W + ((midWeight - minWeight) / weightSpan) * (EDGE_MAX_W - EDGE_MIN_W), - }, - { weight: maxWeight, strokeWidth: EDGE_MAX_W }, -]; +const weightSamples = [minWeight, midWeight, maxWeight].map((weight) => { + const ratio = (weight - minWeight) / weightSpan; + return { weight, ratio, strokeWidth: EDGE_MIN_W + ratio * (EDGE_MAX_W - EDGE_MIN_W) }; +}); + +// Label direction per node: bisector of the widest angular gap between its edges +function labelAngle(nodeIdx: number, px: (n: LabNode) => [number, number]): number { + const [cx, cy] = px(layoutNodes[nodeIdx]); + const angles = edges + .filter((e) => e.sourceIdx === nodeIdx || e.targetIdx === nodeIdx) + .map((e) => { + const other = layoutNodes[e.sourceIdx === nodeIdx ? e.targetIdx : e.sourceIdx]; + const [ox, oy] = px(other); + return Math.atan2(oy - cy, ox - cx); + }) + .sort((a, b) => a - b); + if (angles.length === 0) return -Math.PI / 2; + let bestGap = -1; + let bestAngle = -Math.PI / 2; + angles.forEach((a, i) => { + const next = i === angles.length - 1 ? angles[0] + 2 * Math.PI : angles[i + 1]; + if (next - a > bestGap) { + bestGap = next - a; + bestAngle = (a + next) / 2; + } + }); + return bestAngle; +} interface HoverInfo { label: string; @@ -248,6 +270,7 @@ function WeightedNetworkOverlay({ const xScale = useXScale(); const yScale = useYScale(); const drawingArea = useDrawingArea(); + const toPx = (n: LabNode): [number, number] => [Number(xScale(n.x)), Number(yScale(n.y))]; return ( @@ -272,7 +295,7 @@ function WeightedNetworkOverlay({ x2={x2} y2={y2} stroke={t.ink} - strokeOpacity={0.22 + edge.ratio * 0.4} + strokeOpacity={edgeOpacity(edge.ratio)} strokeWidth={edge.strokeWidth} strokeLinecap="round" /> @@ -290,10 +313,20 @@ function WeightedNetworkOverlay({ ); })} - {layoutNodes.map((node) => { - const cx = Number(xScale(node.x)); - const cy = Number(yScale(node.y)); + {layoutNodes.map((node, idx) => { + const [cx, cy] = toPx(node); const radius = node.radius; + const chipW = node.id.length * 10 + 8; + const chipH = 20; + const angle = labelAngle(idx, toPx); + const cosA = Math.cos(angle); + const sinA = Math.sin(angle); + const toChipEdge = Math.min( + chipW / 2 / Math.max(Math.abs(cosA), 1e-6), + chipH / 2 / Math.max(Math.abs(sinA), 1e-6), + ); + const labelX = cx + cosA * (radius + 6 + toChipEdge); + const labelY = cy + sinA * (radius + 6 + toChipEdge); const tooltip: HoverInfo = { label: node.label, detail: `${node.domainName} · weighted degree ${node.weightedDegree}`, @@ -314,19 +347,20 @@ function WeightedNetworkOverlay({ onMouseLeave={() => onHoverChange(null)} /> From 85e4f8735fabd7b977347028563905a3b556ef8d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 24 Oct 2026 18:23:38 +0000 Subject: [PATCH 5/5] chore(muix): store the review for network-weighted --- .../implementations/javascript/muix.tsx | 2 +- .../metadata/javascript/muix.yaml | 186 ++++++++---------- 2 files changed, 79 insertions(+), 109 deletions(-) diff --git a/plots/network-weighted/implementations/javascript/muix.tsx b/plots/network-weighted/implementations/javascript/muix.tsx index e73bf867153..08f048b407c 100644 --- a/plots/network-weighted/implementations/javascript/muix.tsx +++ b/plots/network-weighted/implementations/javascript/muix.tsx @@ -1,7 +1,7 @@ // anyplot.ai // network-weighted: Weighted Network Graph with Edge Thickness // Library: muix 9.16.0 | JavaScript 22.23.3 -// Quality: 85/100 | Updated: 2026-10-24 +// Quality: 86/100 | Updated: 2026-10-24 //# anyplot-orientation: square import { useState } from "react"; diff --git a/plots/network-weighted/metadata/javascript/muix.yaml b/plots/network-weighted/metadata/javascript/muix.yaml index 61aaf3134fc..fd162256de6 100644 --- a/plots/network-weighted/metadata/javascript/muix.yaml +++ b/plots/network-weighted/metadata/javascript/muix.yaml @@ -2,7 +2,7 @@ library: muix language: javascript specification_id: network-weighted created: '2026-09-02T04:31:39Z' -updated: '2026-10-24T18:17:55Z' +updated: '2026-10-24T18:23:38Z' generated_by: gemini-4-argon adopted_from: 5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69 (5b2bcb3e5e4dc5b3685a669c07cf3ab1b9918f69) workflow_run: 40089447860 @@ -13,53 +13,41 @@ preview_url_light: https://storage.googleapis.com/anyplot-images/plots/network-w preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-dark.png preview_html_light: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-light.html preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/network-weighted/javascript/muix/plot-dark.html -quality_score: 85 +quality_score: 86 review: strengths: - - 'Edge width scales linearly from 2 px (weight 6) to 13 px (weight 45): the thinnest - edge stays visible in both themes and the thickest never buries a node' - - Spring layout shortens the ideal length of heavy edges, so strongly collaborating - labs sit closer together and the four research domains form readable clusters - - Node radius encodes weighted degree and node color the research domain, both named - in the legend band; the edge-width legend shows labeled samples for 6, 26 and - 45 papers - - 'Theme-correct in both renders: Imprint domain colors identical, edges and labels - from ink tokens, page-colored node outlines and label chips separate nodes and - text from edges' - - 'MUI X composition idiom: a custom SVG overlay inside ChartsContainer positioned - with useXScale/useYScale, plus a React-state hover card for edges and nodes in - the HTML output' + - Edge width and opacity both scale with co-authored papers, and the legend's 6 + / 26 / 45 strokes use the same mapping, so the scale reads directly in both themes + - Force layout with weight-dependent spring length keeps strong collaborators (AIR-DSI, + GEN-IMM, QPL-MAT) close together + - Lab codes are placed in the widest free angle around each node on a translucent + page-colored chip, so labels stay readable over edges + - Uses MUI X v9 composition (ChartsContainer + scale hooks) with hidden axes as + the coordinate frame, and React-state hover tooltips in the HTML output weaknesses: - - 'VQ-02 (both): the AIR and IMM label chips (16 px text on a 42x20 page-colored - rect above each node) sit on the short ROB-AIR (weight 29) and GEN-IMM (weight - 42) edges and hide most of their visible length, so those thicknesses cannot be - read along the edge → keep label chips off edge segments, e.g. place each label - on the side of its node away from its heaviest edge, or shrink the chip to the - text width. Likely cause: every label is fixed at y = cy - radius - 11 regardless - of where the node''s edges leave.' - - 'VQ-03 (both): the edge-width legend samples are drawn at a fixed strokeOpacity - 0.55, while the edges use 0.22 + ratio*0.4 (0.22 for weight 6, 0.62 for weight - 45), so the ''6'' sample is darker and the ''45'' sample lighter than the edges - they explain → draw each sample with the same opacity formula as the edges. Likely - cause: strokeOpacity={0.55} on the legend instead of 0.22 + ratio*0.4.' - - 'Suggestion: show the full institute name (already in NODE_LABEL) as a small caption - list or in the legend band so the three-letter codes can be decoded in the static - PNG.' + - 'CQ-01 (code): a standalone helper function `labelAngle` (lines 233–254) and the + `edgeOpacity` arrow function sit beside the plot code → inline the angle computation + in the node map and the opacity expression at its two uses. Likely cause: helpers + factored out during the label-placement pass.' + - 'Suggestion: drawing the nodes as a MUI X scatter series would let the native + `ChartsTooltip` replace the hand-built hover box.' + - 'Suggestion: the tooltip width expression is repeated three times in lines 378–389; + a single constant would shorten it.' image_description: |- Light render (plot-light.png): - Background: warm off-white #FAF8F1 across the full 2400x2400 square canvas, no axes or grid. - Chrome: bold dark title "network-weighted · javascript · muix · anyplot.ai" centered at top (~50% width); bold dark three-letter node codes (MAR, AST, CLI, ENV, QPL, MAT, CHE, DSI, ROB, AIR, BIO, GEN, NEU, IMM) on page-colored chips above each node; bottom legend band with a domain row (Life Sciences, Physical Sciences, Computer Science, Engineering) and an edge-width row "Edge width = Co-authored papers · node size = weighted degree:" with sample lines labeled 6, 26, 45 - all readable. - Data: 15 nodes in four Imprint colors (Life Sciences #009E73 green first, purple, blue, ochre) with page-colored outlines, radius 15-40 px by weighted degree (DSI, GEN, AIR, MAT largest); 24 gray edges 2-13 px wide with opacity rising with weight; AIR-DSI the heaviest dark edge, QPL-MAT and GEN-IMM next. The AIR and IMM label chips cover most of the short ROB-AIR and GEN-IMM edges. + Background: warm off-white #FAF8F1, no axes or grid + Chrome: bold near-black title centered (~50% width); bold lab codes on translucent chips; legend text in inkSoft; all readable + Data: 15 nodes colored by domain in Imprint 1-4 (Life Sciences #009E73 first, then #C475FD, #4467A3, #BD8233), sized by weighted degree; ink edges 2-13 px wide with opacity growing with weight; legend samples 6/26/45 Legibility verdict: PASS Dark render (plot-dark.png): - Background: warm near-black #1A1A17, no axes or grid. - Chrome: title, node codes and legend text all flip to light ink and are clearly readable; no dark-on-dark text; label chips use the dark page color. - Data: node colors identical to the light render (green, purple, blue, ochre); edges flip to light gray with the same width and opacity ramp, the thinnest NEU-IMM edge still visible; layout identical. + Background: warm near-black #1A1A17 + Chrome: title, lab codes and legend in light ink; no dark-on-dark text + Data: node colors identical to light render; edges flip to light ink, thin edges dim but visible; legend samples match Legibility verdict: PASS criteria_checklist: visual_quality: - score: 28 + score: 30 max: 30 items: - id: VQ-01 @@ -67,80 +55,70 @@ review: score: 8 max: 8 passed: true - comment: All sizes set explicitly (title 22 px, node labels 16 px bold, legend - 14-15 px CSS); every element readable in both themes; title ~50% of width + comment: Explicit sizes (22 px title, 16 px bold codes, 14-15 px legend); + readable in both themes - id: VQ-02 name: No Overlap - score: 5 + score: 6 max: 6 passed: true - comment: The AIR and IMM label chips sit on the short ROB-AIR (29) and GEN-IMM - (42) edges and hide most of their length (defect line) + comment: No text collisions; labels drawn on top of edges as the spec asks - id: VQ-03 name: Element Visibility - score: 5 + score: 6 max: 6 passed: true - comment: Thinnest edge (6, 2 px) visible in both themes; the edge-width legend - samples use a fixed 0.55 opacity while the edges run 0.22-0.62, so the samples - do not match the marks they explain (defect line) + comment: Thinnest edge visible in both themes; legend strokes match the edges - id: VQ-04 name: Color Accessibility score: 2 max: 2 passed: true - comment: Four Imprint hues (green, purple, blue, ochre) plus node labels; - no red-green sole signal + comment: Four CVD-distinct Imprint hues; weight also carried by width - id: VQ-05 name: Layout & Canvas score: 4 max: 4 passed: true - comment: Square 2400x2400, network centered, legend band clear of the graph, - nothing clipped + comment: Square canvas, network centered, nothing clipped - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: N/A - network has no axes; the legend caption names the weight column - in reader text ('Co-authored papers') + comment: 'N/A: no axes by design; legend caption names the weight variable' - id: VQ-07 name: Palette Compliance score: 2 max: 2 passed: true - comment: 'First domain (Life Sciences) = #009E73, domains in canonical Imprint - order via t.palette[i]; backgrounds #FAF8F1 / #1A1A17; edges and chrome - from ink tokens; data colors identical in both renders' + comment: 'Imprint positions 1-4 in canonical order; #FAF8F1 / #1A1A17 pages; + chrome from theme tokens' design_excellence: - score: 11 + score: 12 max: 20 items: - id: DE-01 name: Aesthetic Sophistication score: 5 max: 8 - passed: true - comment: 'Beyond baseline: page-colored node outlines separating nodes from - edges, bold monospace-like node codes on semi-opaque page-colored label - chips, clear title > labels > legend hierarchy' + passed: false + comment: Edge opacity coupled to weight, page-colored node outlines, translucent + label chips - id: DE-02 name: Visual Refinement - score: 3 + score: 4 max: 6 - passed: true - comment: Edges drawn light-to-heavy so heavy links sit on top, rounded line - caps, a two-row legend band with gap-tuned spacing; label chips still sit - on some short edges + passed: false + comment: Each label placed in the widest angular gap between its node's edges; + integrated two-row legend - id: DE-03 name: Data Storytelling score: 3 max: 6 - passed: true - comment: Edge opacity rises with weight alongside width, so the heaviest links - (AIR-DSI, QPL-MAT, GEN-IMM) read first and the weak cross-domain bridges - recede + passed: false + comment: Weight-dependent spring length pulls heavy collaborators together; + heavy edges darker as focal strokes spec_compliance: score: 15 max: 15 @@ -150,30 +128,26 @@ review: score: 5 max: 5 passed: true - comment: Weighted network graph with weight-scaled edges + comment: Weighted network graph - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: Proportional edge width 2-13 px; spring layout with weight-shortened - ideal lengths; weight legend with labeled sample widths (6/26/45); node - size = weighted degree and color = domain, both named; nodes and labels - drawn over edges + comment: Thickness by weight, force layout, labeled weight scale, node size + = weighted degree, color = domain with legend, labels on top of edges - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: Layout-positioned type (exempt); all 15 nodes and 24 edges drawn, - widths match weights + comment: Layout-positioned type; widths and radii follow the data - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title 'network-weighted · javascript · muix · anyplot.ai'; domain - legend and edge-width legend in reader text + comment: Mandated title; legend entries readable data_quality: score: 15 max: 15 @@ -183,93 +157,89 @@ review: score: 6 max: 6 passed: true - comment: Hubs (DSI, MAT, GEN), heavy intra-domain clusters, light cross-domain - bridges, peripheral nodes (AST, NEU), full 6-45 weight range + comment: Hubs (DSI, GEN), heavy vs light links, peripheral node (AST) - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Research-institute co-authorship network (a spec application), neutral + comment: Research-institute co-authorship network, neutral - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: 6-45 co-authored papers per lab pair is plausible; legend values - 6/26/45 checked against min/mid/max of the data (6, round(25.5)=26, 45) + comment: 'Checked: weight samples 6/26/45 = min/rounded midpoint/max; DSI + largest weighted degree (107), AST smallest (29), matching node sizes' code_quality: - score: 10 + score: 9 max: 10 items: - id: CQ-01 name: KISS Structure - score: 3 + score: 2 max: 3 - passed: true - comment: Columns -> Data -> Plot zones; the one overlay component is required - by MUI X's hook-based composition + passed: false + comment: Helper function labelAngle and edgeOpacity arrow beside the plot + code (defect line) - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: Hard-coded data, fixed-seed LCG for initial positions + comment: Fixed-seed LCG, hard-coded data - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: useState, ChartsContainer, useXScale/useYScale/useDrawingArea all - used + comment: All imports used - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Written-out Fruchterman-Reingold layout is allowed (no layout helper - in @mui/x-charts); hover tooltip is real interactivity, not fake UI + comment: 'Written-out force layout allowed: nothing in MUI X computes it' - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: 'Default export, sized from ANYPLOT_SIZE, skipAnimation, v9 ChartsContainer - with position: ''none'' axes' + comment: Default export, skipAnimation, MUI X v9 ChartsContainer library_mastery: - score: 6 + score: 5 max: 10 items: - id: LM-01 name: Idiomatic Usage - score: 4 + score: 3 max: 5 - passed: true - comment: Custom SVG overlay inside ChartsContainer placed with useXScale/useYScale - - the MUI X composition idiom for marks it has no series type for + passed: false + comment: ChartsContainer composition with hidden linear axes as coordinate + system - id: LM-02 name: Distinctive Features score: 2 max: 5 - passed: true - comment: useDrawingArea keeps the React-state hover card inside the plot area; - legends are hand-built HTML rather than ChartsLegend - verdict: REJECTED + passed: false + comment: useXScale/useYScale/useDrawingArea composition hooks drive a custom + SVG overlay + verdict: APPROVED model: claude-opus-5-5 criteria_version: qc-899b86ffde.aqr-869a70087c.sg-c73b4641dd.lib-f7283eb626 - rendered_at: '2026-10-24T11:20:52Z' + rendered_at: '2026-10-24T18:20:31Z' impl_tags: dependencies: [] techniques: - custom-legend - hover-tooltips - html-export - - layer-composition + - annotations patterns: - data-generation + - iteration-over-groups dataprep: - - normalization + - force-directed-layout styling: - - minimal-chrome - alpha-blending - edge-highlighting