{"spec_id":"parallel-categories-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// parallel-categories-basic: Basic Parallel Categories Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Customer journey across four categorical dimensions: acquisition channel ->\n// product category -> device type -> purchase outcome. ECharts has no\n// dedicated \"parallel categories\" chart (that's a numeric parallel-coordinates\n// variant); a Sankey diagram is its native equivalent for width-proportional\n// ribbons flowing between categorical stages.\nconst CHANNELS = [\"Organic Search\", \"Paid Ads\", \"Social Media\", \"Email Campaign\"];\nconst CATEGORIES = [\"Electronics\", \"Apparel\", \"Home & Garden\"];\nconst DEVICES = [\"Desktop\", \"Mobile\"];\nconst OUTCOMES = [\"Purchased\", \"Abandoned\"];\n\n// channel -> category visit counts\nconst channelToCategory = [\n  [\"Organic Search\", \"Electronics\", 420],\n  [\"Organic Search\", \"Apparel\", 280],\n  [\"Organic Search\", \"Home & Garden\", 150],\n  [\"Paid Ads\", \"Electronics\", 380],\n  [\"Paid Ads\", \"Apparel\", 210],\n  [\"Paid Ads\", \"Home & Garden\", 90],\n  [\"Social Media\", \"Electronics\", 160],\n  [\"Social Media\", \"Apparel\", 340],\n  [\"Social Media\", \"Home & Garden\", 120],\n  [\"Email Campaign\", \"Electronics\", 90],\n  [\"Email Campaign\", \"Apparel\", 130],\n  [\"Email Campaign\", \"Home & Garden\", 60],\n];\n\n// category -> device counts (each category's flow conserved into Desktop/Mobile)\nconst categoryToDevice = [\n  [\"Electronics\", \"Desktop\", 630],\n  [\"Electronics\", \"Mobile\", 420],\n  [\"Apparel\", \"Desktop\", 528],\n  [\"Apparel\", \"Mobile\", 432],\n  [\"Home & Garden\", \"Desktop\", 210],\n  [\"Home & Garden\", \"Mobile\", 210],\n];\n\n// device -> outcome counts (Desktop converts noticeably better than Mobile)\nconst deviceToOutcome = [\n  [\"Desktop\", \"Purchased\", 1000],\n  [\"Desktop\", \"Abandoned\", 368],\n  [\"Mobile\", \"Purchased\", 470],\n  [\"Mobile\", \"Abandoned\", 592],\n];\n\n// Color by first dimension (source channel) and last dimension (target\n// outcome). Category nodes now carry their own semantic hue (fits the\n// domain: Electronics -> cyan/tech, Home & Garden -> lime/growth, Apparel ->\n// rose) so the middle of the flow keeps visual hierarchy instead of going\n// fully neutral; only the small Desktop/Mobile waypoint stays neutral.\nconst channelColor = {\n  \"Organic Search\": t.palette[0],\n  \"Paid Ads\": t.palette[1],\n  \"Social Media\": t.palette[2],\n  \"Email Campaign\": t.palette[3],\n};\nconst categoryColor = {\n  Electronics: t.palette[5],\n  \"Home & Garden\": t.palette[7],\n  Apparel: t.palette[6],\n};\nconst outcomeColor = { Purchased: t.palette[0], Abandoned: t.palette[4] };\n\nconst nodes = [\n  ...CHANNELS.map((name) => ({\n    name,\n    itemStyle: { color: channelColor[name] },\n  })),\n  ...CATEGORIES.map((name) => ({\n    name,\n    itemStyle: { color: categoryColor[name] },\n  })),\n  ...DEVICES.map((name) => ({\n    name,\n    itemStyle: {\n      color: t.elevatedBg,\n      borderColor: t.inkSoft,\n      borderWidth: 1.5,\n    },\n  })),\n  ...OUTCOMES.map((name) => ({\n    name,\n    itemStyle: { color: outcomeColor[name] },\n  })),\n];\n\nconst links = [\n  ...channelToCategory.map(([source, target, value]) => ({\n    source,\n    target,\n    value,\n    lineStyle: { color: channelColor[source], opacity: 0.5, curveness: 0.5 },\n  })),\n  ...categoryToDevice.map(([source, target, value]) => ({\n    source,\n    target,\n    value,\n    lineStyle: { color: categoryColor[source], opacity: 0.5, curveness: 0.5 },\n  })),\n  ...deviceToOutcome.map(([source, target, value]) => ({\n    source,\n    target,\n    value,\n    lineStyle: { color: outcomeColor[target], opacity: 0.5, curveness: 0.5 },\n  })),\n];\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"parallel-categories-basic · javascript · echarts · anyplot.ai\",\n    subtext:\n      \"Customer journey: acquisition channel → product category → device → purchase outcome\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 600 },\n    subtextStyle: { color: t.inkSoft, fontSize: 16 },\n  },\n  tooltip: { trigger: \"item\" },\n  series: [\n    {\n      type: \"sankey\",\n      left: 40,\n      right: 160,\n      top: 130,\n      bottom: 60,\n      nodeWidth: 22,\n      nodeGap: 22,\n      nodeAlign: \"justify\",\n      // Raised from the default 32: more Gauss-Seidel relaxation passes let\n      // ECharts' own node-ordering algorithm converge further toward\n      // fewer ribbon crossings.\n      layoutIterations: 100,\n      orient: \"horizontal\",\n      draggable: false,\n      emphasis: { focus: \"adjacency\" },\n      label: {\n        color: t.ink,\n        fontSize: 14,\n        fontWeight: 500,\n        backgroundColor: t.pageBg,\n        padding: [3, 6],\n        borderRadius: 3,\n      },\n      data: nodes,\n      links,\n    },\n  ],\n});\n"}