{"spec_id":"mosaic-categorical","library":"echarts","language":"javascript","code":"// anyplot.ai\n// mosaic-categorical: Mosaic Plot for Categorical Association Analysis\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Retail orders cross-tabulated by purchase channel (columns) and product category (rows).\nconst channels = [\"Online\", \"Mobile App\", \"In-Store\"];\nconst categories = [\"Electronics\", \"Clothing\", \"Home Goods\", \"Books\"];\nconst counts = [\n  [420, 180, 90, 60], // Online\n  [150, 340, 70, 40], // Mobile App\n  [80, 120, 310, 90], // In-Store\n];\n\nconst channelTotals = counts.map((row) => row.reduce((a, b) => a + b, 0));\nconst grandTotal = channelTotals.reduce((a, b) => a + b, 0);\n\n// Cell rectangles in a 0-1 x 0-1 domain: width = share of grand total per\n// channel, height = conditional share of category within that channel.\nlet xCursor = 0;\nconst cells = [];\nchannels.forEach((channel, i) => {\n  const x0 = xCursor;\n  const x1 = xCursor + channelTotals[i] / grandTotal;\n  xCursor = x1;\n\n  let yCursor = 0;\n  categories.forEach((category, j) => {\n    const y0 = yCursor;\n    const y1 = yCursor + counts[i][j] / channelTotals[i];\n    yCursor = y1;\n    cells.push({\n      value: [x0, x1, y0, y1],\n      channel,\n      channelTotal: channelTotals[i],\n      colorIndex: j,\n      isFirstRow: j === 0,\n    });\n  });\n});\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  color: t.palette,\n  title: {\n    text: \"mosaic-categorical · javascript · echarts · anyplot.ai\",\n    subtext: \"Column width = share of orders per channel · color = product category\",\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 30, fontWeight: 500 },\n    subtextStyle: { color: t.inkSoft, fontSize: 17 },\n  },\n  legend: {\n    data: categories,\n    orient: \"vertical\",\n    right: 40,\n    top: \"middle\",\n    itemWidth: 20,\n    itemHeight: 20,\n    textStyle: { color: t.inkSoft, fontSize: 18 },\n  },\n  grid: {\n    left: 150,\n    right: 260,\n    top: 160,\n    bottom: 150,\n  },\n  xAxis: { type: \"value\", min: 0, max: 1, show: false },\n  yAxis: {\n    type: \"value\",\n    min: 0,\n    max: 1,\n    name: \"Share of product category within channel\",\n    nameLocation: \"middle\",\n    nameGap: 85,\n    nameRotate: 90,\n    nameTextStyle: { color: t.inkSoft, fontSize: 18 },\n    axisLabel: { formatter: (v) => `${Math.round(v * 100)}%`, color: t.inkSoft, fontSize: 17 },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  graphic: [\n    {\n      type: \"text\",\n      left: \"center\",\n      bottom: 24,\n      style: {\n        text: \"Purchase channel\",\n        fill: t.inkSoft,\n        fontSize: 18,\n        textAlign: \"center\",\n      },\n    },\n  ],\n  series: [\n    // Empty-data helper series: registers category names + colors in the legend.\n    ...categories.map((category, j) => ({\n      name: category,\n      type: \"bar\",\n      data: [],\n      itemStyle: { color: t.palette[j] },\n    })),\n    {\n      name: \"mosaic\",\n      type: \"custom\",\n      legendHoverLink: false,\n      coordinateSystem: \"cartesian2d\",\n      data: cells,\n      renderItem: (params, api) => {\n        const x0 = api.value(0);\n        const x1 = api.value(1);\n        const y0 = api.value(2);\n        const y1 = api.value(3);\n        const cell = cells[params.dataIndex];\n\n        const topLeft = api.coord([x0, y1]);\n        const bottomRight = api.coord([x1, y0]);\n        const rectWidth = bottomRight[0] - topLeft[0];\n        const rectHeight = bottomRight[1] - topLeft[1];\n        const gapPx = 3;\n\n        const children = [\n          {\n            type: \"rect\",\n            shape: {\n              x: topLeft[0] + gapPx / 2,\n              y: topLeft[1] + gapPx / 2,\n              width: Math.max(rectWidth - gapPx, 0),\n              height: Math.max(rectHeight - gapPx, 0),\n            },\n            style: { fill: t.palette[cell.colorIndex] },\n          },\n        ];\n\n        if (cell.isFirstRow) {\n          const bottomCenter = api.coord([(x0 + x1) / 2, 0]);\n          children.push({\n            type: \"text\",\n            style: {\n              text: `${cell.channel}\\nn=${cell.channelTotal}`,\n              x: bottomCenter[0],\n              y: bottomCenter[1] + 18,\n              textAlign: \"center\",\n              textVerticalAlign: \"top\",\n              fontSize: 18,\n              lineHeight: 23,\n              fill: t.inkSoft,\n            },\n          });\n        }\n\n        return { type: \"group\", children };\n      },\n    },\n  ],\n});\n"}