{"spec_id":"mosaic-categorical","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// mosaic-categorical: Mosaic Plot for Categorical Association Analysis\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Contingency table: performance rating counts by department.\nconst departments = [\"Engineering\", \"Sales\", \"Marketing\", \"Support\"];\nconst ratings = [\"Exceeds\", \"Meets\", \"Below\"];\nconst counts = [\n  [42, 58, 10], // Engineering\n  [35, 70, 25], // Sales\n  [20, 45, 15], // Marketing\n  [15, 38, 12], // Support\n];\n\nconst colTotals = counts.map((row) => row.reduce((a, b) => a + b, 0));\nconst grandTotal = colTotals.reduce((a, b) => a + b, 0);\n\n// Column boundaries in raw-count data units — these back the real xAxis scale\n// (0..grandTotal) so tick centers and column widths both derive from it.\nlet cum = 0;\nconst colStart = [];\nconst colEnd = [];\ncolTotals.forEach((ct) => {\n  colStart.push(cum);\n  cum += ct;\n  colEnd.push(cum);\n});\nconst colCenters = colStart.map((s, i) => (s + colEnd[i]) / 2);\n\n// Precomputed luminance-based text color per rating (a lookup, not a per-cell helper call).\nfunction luminance(hex) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n}\nconst textColors = ratings.map((_, j) => (luminance(t.palette[j]) > 0.6 ? \"#1A1A17\" : \"#FFFFFF\"));\n\n// Standout cell for storytelling emphasis: department with the highest \"Below\" share.\nconst belowIdx = ratings.length - 1;\nlet standoutIdx = 0;\nlet standoutRatio = -1;\ncounts.forEach((row, i) => {\n  const ratio = row[belowIdx] / colTotals[i];\n  if (ratio > standoutRatio) {\n    standoutRatio = ratio;\n    standoutIdx = i;\n  }\n});\n\nconst colGap = 6;\nconst rowGap = 3;\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      load() {\n        const r = this.renderer;\n\n        // Draw on top of the real xAxis/yAxis coordinate system: Highcharts has\n        // already reserved space for the title, subtitle, axis titles/labels and\n        // legend, so this box is the actual plot area, not a hand-picked margin.\n        const plotLeftPx = this.plotLeft;\n        const plotTopPx = this.plotTop;\n        const plotWidthPx = this.plotWidth;\n        const plotHeightPx = this.plotHeight;\n\n        // Column x-boundaries: width proportional to department headcount share.\n        const availableWidth = plotWidthPx - (departments.length - 1) * colGap;\n        let cursorX = plotLeftPx;\n        const colX = [];\n        const colWidth = [];\n        departments.forEach((_, i) => {\n          const w = (colTotals[i] / grandTotal) * availableWidth;\n          colX.push(cursorX);\n          colWidth.push(w);\n          cursorX += w + colGap;\n        });\n\n        // Mosaic rectangles: column width ∝ department share, row height ∝ rating share.\n        departments.forEach((dept, i) => {\n          const availableHeight = plotHeightPx - (ratings.length - 1) * rowGap;\n          let cursorY = plotTopPx;\n          ratings.forEach((rating, j) => {\n            const cellHeight = (counts[i][j] / colTotals[i]) * availableHeight;\n            const isStandout = i === standoutIdx && j === belowIdx;\n            r.rect(colX[i], cursorY, colWidth[i], cellHeight, 2)\n              .attr({\n                fill: t.palette[j],\n                stroke: isStandout ? t.amber : t.pageBg,\n                \"stroke-width\": isStandout ? 3 : 2,\n              })\n              .add();\n\n            if (colWidth[i] > 46 && cellHeight > 28) {\n              r.text(String(counts[i][j]), colX[i] + colWidth[i] / 2, cursorY + cellHeight / 2 + 5)\n                .attr({ align: \"center\" })\n                .css({ color: textColors[j], fontSize: \"14px\", fontWeight: \"600\" })\n                .add();\n            }\n            cursorY += cellHeight + rowGap;\n          });\n        });\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"mosaic-categorical · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: `${departments[standoutIdx]} has the highest 'Below' share, at ${Math.round(standoutRatio * 100)}%`,\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    type: \"linear\",\n    min: 0,\n    max: grandTotal,\n    tickPositions: colCenters,\n    lineWidth: 0,\n    tickLength: 0,\n    gridLineWidth: 0,\n    labels: {\n      formatter() {\n        return departments[colCenters.indexOf(this.value)] ?? \"\";\n      },\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n    },\n    title: {\n      text: \"Department  ·  column width ∝ headcount\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n  },\n  yAxis: {\n    type: \"linear\",\n    min: 0,\n    max: 100,\n    tickPositions: [0, 25, 50, 75, 100],\n    lineWidth: 0,\n    gridLineColor: t.grid,\n    labels: {\n      formatter() {\n        return `${this.value}%`;\n      },\n      style: { color: t.inkSoft, fontSize: \"14px\" },\n    },\n    title: {\n      text: \"Rating share within department\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n  },\n  legend: {\n    enabled: true,\n    align: \"right\",\n    verticalAlign: \"middle\",\n    layout: \"vertical\",\n    title: { text: \"Performance rating\", style: { color: t.inkSoft, fontSize: \"14px\", fontWeight: \"600\" } },\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n    symbolRadius: 2,\n  },\n  tooltip: { enabled: false },\n  plotOptions: { series: { animation: false, enableMouseTracking: false } },\n  series: ratings.map((rating, j) => ({\n    type: \"column\",\n    name: rating,\n    data: [],\n    color: t.palette[j],\n  })),\n});\n"}