diff --git a/plots/network-weighted/implementations/javascript/muix.tsx b/plots/network-weighted/implementations/javascript/muix.tsx index 04f7233728..08f048b407 100644 --- a/plots/network-weighted/implementations/javascript/muix.tsx +++ b/plots/network-weighted/implementations/javascript/muix.tsx @@ -1,227 +1,311 @@ // 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 +// Library: muix 9.16.0 | JavaScript 22.23.3 +// Quality: 86/100 | Updated: 2026-10-24 //# 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; + +// 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 -// --- 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"]; - -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); - -// 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 minWeight = Math.min(...edgeWeights); +const maxWeight = Math.max(...edgeWeights); +const weightSpan = maxWeight - minWeight; -// --- 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 edge of edges) { + edge.ratio = (edge.weight - minWeight) / weightSpan; + edge.strokeWidth = EDGE_MIN_W + edge.ratio * (EDGE_MAX_W - EDGE_MIN_W); } -const N = nodes.length; -const AREA = 4.5; -const K = Math.sqrt(AREA / N); -const ITERATIONS = 500; +const degrees = layoutNodes.map((n) => n.weightedDegree); +const minDegree = Math.min(...degrees); +const maxDegree = Math.max(...degrees); +const degreeSpan = maxDegree - minDegree; + +for (const node of layoutNodes) { + const ratio = (node.weightedDegree - minDegree) / degreeSpan; + node.radius = NODE_MIN_R + ratio * (NODE_MAX_R - NODE_MIN_R); +} -const positions = nodes.map(() => ({ x: rand() * 2 - 1, y: rand() * 2 - 1 })); -let temperature = 0.12; +let lcgState = 42; +const nCount = layoutNodes.length; +const kForce = Math.sqrt(AREA / nCount); + +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 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; +} -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 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; -const NODE_MIN_R = 15; -const NODE_MAX_R = 40; -const EDGE_MIN_W = 2; -const EDGE_MAX_W = 13; +const edgeOpacity = (ratio: number) => 0.22 + ratio * 0.4; -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 sortedEdges = [...edges].sort((a, b) => a.weight - b.weight); +const midWeight = Math.round((minWeight + maxWeight) / 2); +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) }; +}); -function edgeWidth(weight) { - const ratio = (weight - MIN_WEIGHT) / (MAX_WEIGHT - MIN_WEIGHT || 1); - return 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; } -// Draw heaviest edges last so the strongest collaborations stay legible on top. -const sortedEdges = [...edges].sort((a, b) => a.weight - b.weight); - -// --- 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(); + const toPx = (n: LabNode): [number, number] => [Number(xScale(n.x)), Number(yScale(n.y))]; 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 +313,23 @@ 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, 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: `${DOMAINS[node.domain]} · weighted degree ${weightedDegree[i]}`, + detail: `${node.domainName} · weighted degree ${node.weightedDegree}`, x: cx, y: cy - radius - 10, }; @@ -246,105 +339,202 @@ 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} + + ))} +
+
+ ); } diff --git a/plots/network-weighted/metadata/javascript/muix.yaml b/plots/network-weighted/metadata/javascript/muix.yaml index 92ca2ac6e9..fd162256de 100644 --- a/plots/network-weighted/metadata/javascript/muix.yaml +++ b/plots/network-weighted/metadata/javascript/muix.yaml @@ -2,140 +2,123 @@ 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-24T18:23:38Z' +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: 86 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. + - 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: - - 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. + - '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, 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. + 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, 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. + 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 name: Text Legibility - score: 7 + score: 8 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 + 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: 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 + 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: 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 + 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: Domain colors distinguishable without relying on red-green; page-background - node stroke adds separation from crossing edges + comment: Four CVD-distinct Imprint hues; weight also carried by width - 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 + comment: Square canvas, network centered, nothing clipped - 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 + 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) is #009E73, remaining 3 domains follow - canonical Imprint order; backgrounds and chrome theme-correct in both renders' + comment: 'Imprint positions 1-4 in canonical order; #FAF8F1 / #1A1A17 pages; + chrome from theme tokens' design_excellence: - score: 16 + score: 12 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 6 + score: 5 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 + passed: false + comment: Edge opacity coupled to weight, page-colored node outlines, translucent + label chips - id: DE-02 name: Visual Refinement - score: 5 + score: 4 max: 6 - comment: No competing chrome (no grid/spines/axis lines), generous whitespace, - restrained tooltip and legend styling + 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: 5 + score: 3 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 + passed: false + comment: Weight-dependent spring length pulls heavy collaborators together; + heavy edges darker as focal strokes spec_compliance: score: 15 max: 15 @@ -144,46 +127,50 @@ review: name: Plot Type score: 5 max: 5 - comment: Correct weighted network graph with force-directed positioning + passed: true + comment: Weighted network graph - 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 + passed: true + 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 - comment: All 14 nodes and 24 edges rendered and visible + passed: true + comment: Layout-positioned type; widths and radii follow the data - 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 + passed: true + comment: Mandated title; legend entries readable data_quality: - score: 14 + score: 15 max: 15 items: - id: DQ-01 name: Feature Coverage - score: 5 + score: 6 max: 6 - comment: Wide weight range (6-45) with visibly distinct thin/thick edges, - hub vs. peripheral nodes, cross-domain and intra-domain edges + passed: true + comment: Hubs (DSI, GEN), heavy vs light links, peripheral node (AST) - id: DQ-02 name: Realistic Context score: 5 max: 5 - comment: Real, neutral, comprehensible scenario — academic co-authorship network - across research domains + passed: true + comment: Research-institute co-authorship network, neutral - 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 + passed: true + 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: 9 max: 10 @@ -192,58 +179,67 @@ review: 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 + passed: false + comment: Helper function labelAngle and edgeOpacity arrow beside the plot + code (defect line) - id: CQ-02 name: Reproducibility score: 2 max: 2 - comment: Fixed-seed LCG (state=42) for deterministic layout scatter + passed: true + comment: Fixed-seed LCG, hard-coded data - id: CQ-03 name: Clean Imports score: 2 max: 2 - comment: Only used hooks/components imported + passed: true + comment: All imports used - 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 + passed: true + comment: 'Written-out force layout allowed: nothing in MUI X computes it' - id: CQ-05 name: Output & API score: 1 max: 1 - comment: Correct default-exported Chart component, skipAnimation set, harness - owns file output + passed: true + comment: Default export, skipAnimation, MUI X v9 ChartsContainer library_mastery: - score: 8 + score: 5 max: 10 items: - id: LM-01 name: Idiomatic Usage - score: 4 + score: 3 max: 5 - comment: Uses ChartContainer + useXScale/useYScale/useDrawingArea, the recommended - community pattern for custom chart types MUI X has no built-in component - for + passed: false + comment: ChartsContainer composition with hidden linear axes as coordinate + system - id: LM-02 name: Distinctive Features - score: 4 + score: 2 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 + 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-24T18:20:31Z' impl_tags: dependencies: [] techniques: - - force-directed-layout - custom-legend - hover-tooltips + - html-export + - annotations patterns: - data-generation - dataprep: [] + - iteration-over-groups + dataprep: + - force-directed-layout styling: - alpha-blending - edge-highlighting