{"spec_id":"alluvial-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// alluvial-basic: Basic Alluvial Diagram\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Student performance tracks followed across four semesters. Each time point\n// is a strict column; every link only connects a category to its immediate\n// successor column, which keeps the ECharts sankey layout aligned into a\n// classic alluvial column structure (no cross-generation links).\nconst timePoints = [\"Semester 1\", \"Semester 2\", \"Semester 3\", \"Semester 4\"];\nconst categories = [\"Advanced\", \"Proficient\", \"Developing\", \"Needs Support\"];\nconst categoryColors = [t.palette[0], t.palette[1], t.palette[2], t.palette[3]];\n\n// Transition matrices: transitions[k][from][to] = students moving from\n// categories[from] at timePoints[k] to categories[to] at timePoints[k + 1].\nconst transitions = [\n  [\n    [45, 15, 0, 0],\n    [20, 50, 20, 0],\n    [0, 25, 45, 20],\n    [0, 0, 25, 35],\n  ],\n  [\n    [50, 15, 0, 0],\n    [25, 45, 20, 0],\n    [0, 30, 40, 20],\n    [0, 0, 20, 35],\n  ],\n  [\n    [60, 15, 0, 0],\n    [30, 45, 15, 0],\n    [0, 25, 40, 15],\n    [0, 0, 15, 40],\n  ],\n];\n\nconst nodeName = (timeIdx, catIdx) => `${timeIdx}|${catIdx}`;\n\nconst nodes = timePoints.flatMap((_, timeIdx) =>\n  categories.map((_, catIdx) => ({\n    name: nodeName(timeIdx, catIdx),\n    itemStyle: { color: categoryColors[catIdx] },\n  }))\n);\n\n// Category index 0 (Advanced) is the best outcome and 3 (Needs Support) the\n// worst, so a transition to a lower index is an improvement. Improving flows\n// get a visibility boost and declining flows recede, so the \"students\n// improving over time\" story reads immediately instead of requiring the\n// viewer to trace individual bands through the crossing pattern.\nconst linkOpacity = (fromIdx, toIdx) => {\n  if (toIdx < fromIdx) return 0.72; // improving\n  if (toIdx > fromIdx) return 0.2; // declining\n  return 0.45; // stable\n};\n\nconst links = transitions.flatMap((matrix, k) =>\n  matrix.flatMap((row, fromIdx) =>\n    row\n      .map((value, toIdx) => ({ value, fromIdx, toIdx }))\n      .filter((link) => link.value > 0)\n      .map((link) => ({\n        source: nodeName(k, link.fromIdx),\n        target: nodeName(k + 1, link.toIdx),\n        value: link.value,\n        lineStyle: {\n          color: \"source\",\n          opacity: linkOpacity(link.fromIdx, link.toIdx),\n          curveness: 0.45,\n        },\n      }))\n  )\n);\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Column headers (time points) ----------------------------------------\n// Centers approximate the sankey's four justified column x-positions for the\n// 1600px landscape mount (left: 60, right: 60, nodeWidth: 24).\nconst columnCenters = [72, 557, 1043, 1528];\nconst headerGraphics = timePoints.map((label, i) => ({\n  type: \"text\",\n  position: [columnCenters[i], 108],\n  style: {\n    text: label,\n    textAlign: \"center\",\n    fill: t.ink,\n    fontSize: 16,\n    fontWeight: \"bold\",\n  },\n}));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"alluvial-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 30,\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  graphic: { elements: headerGraphics },\n  series: [\n    {\n      type: \"sankey\",\n      left: 60,\n      right: 60,\n      top: 160,\n      bottom: 70,\n      nodeWidth: 24,\n      nodeGap: 18,\n      nodeAlign: \"justify\",\n      layoutIterations: 64,\n      emphasis: { focus: \"adjacency\" },\n      label: {\n        position: \"top\",\n        color: t.ink,\n        fontSize: 16,\n        formatter: (params) => categories[Number(params.name.split(\"|\")[1])],\n      },\n      lineStyle: { color: \"source\", opacity: 0.45, curveness: 0.45 },\n      data: nodes,\n      links,\n    },\n  ],\n});\n"}