{"spec_id":"heatmap-cohort-retention","library":"echarts","language":"javascript","code":"// anyplot.ai\n// heatmap-cohort-retention: Cohort Retention Heatmap\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-20\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (deterministic, SaaS product analytics — weekly cohort retention) ---\nconst cohortMonths = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\"];\nconst cohortYear = \"2024\";\nconst cohortSizes = [2100, 1850, 2340, 1920, 2580, 2210, 1760, 2450, 2030, 1640];\n\n// Base weekly retention curve (typical SaaS decay pattern)\nconst baseRetention = [100, 82, 68, 57, 49, 43, 38, 34, 31, 28];\n\n// Cohort quality multipliers — product improvements drove higher retention over the year\nconst cohortMult = [0.88, 0.91, 0.89, 0.93, 0.95, 0.92, 0.96, 0.94, 0.97, 0.99];\n\nconst numCohorts = cohortMonths.length;\nconst numPeriods = 10;\n\n// Build triangular heatmap data: [x_period, y_cohort, value]\n// Older cohorts (Jan) have all periods; newer cohorts (Oct) have fewer\nconst heatmapData = [];\nfor (let ci = 0; ci < numCohorts; ci++) {\n  const maxPeriod = numPeriods - ci;\n  for (let p = 0; p < maxPeriod; p++) {\n    const rate = p === 0 ? 100 : Math.round(baseRetention[p] * cohortMult[ci]);\n    heatmapData.push([p, ci, rate]);\n  }\n}\n\n// X-axis: weekly periods\nconst weekLabels = Array.from({ length: numPeriods }, (_, i) => `Week ${i}`);\n\n// Y-axis: cohort labels with cohort sizes\nconst formatSize = (n) => {\n  const k = n / 1000;\n  return k % 1 === 0 ? `${k}k` : `${k.toFixed(1)}k`;\n};\nconst cohortLabels = cohortMonths.map(\n  (m, i) => `${m} ${cohortYear}  (n=${formatSize(cohortSizes[i])})`\n);\n\n// --- Init ---\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---\nconst titleText = \"heatmap-cohort-retention · javascript · echarts · anyplot.ai\";\n\nchart.setOption({\n  animation: false,\n  backgroundColor: t.pageBg,\n  color: t.palette,\n\n  title: {\n    text: titleText,\n    subtext: \"Monthly SaaS cohorts — later cohorts show improved Week-1 retention\",\n    left: \"center\",\n    top: 22,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"600\" },\n    subtextStyle: { color: t.inkSoft, fontSize: 14 }\n  },\n\n  tooltip: {\n    trigger: \"item\",\n    formatter: (params) => {\n      const [period, cohortIdx, val] = params.value;\n      const month = cohortMonths[cohortIdx];\n      return `${month} ${cohortYear} cohort · Week ${period}<br/>Retention: <b>${val}%</b>`;\n    }\n  },\n\n  grid: {\n    left: 210,\n    right: 110,\n    top: 130,\n    bottom: 80\n  },\n\n  xAxis: {\n    type: \"category\",\n    data: weekLabels,\n    position: \"top\",\n    axisLabel: { color: t.inkSoft, fontSize: 14, margin: 10 },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { show: false }\n  },\n\n  yAxis: {\n    type: \"category\",\n    data: cohortLabels,\n    inverse: true,\n    axisLabel: { color: t.inkSoft, fontSize: 13, margin: 12 },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { show: false }\n  },\n\n  visualMap: {\n    min: 0,\n    max: 100,\n    calculable: false,\n    show: true,\n    orient: \"vertical\",\n    right: 15,\n    top: \"middle\",\n    itemWidth: 18,\n    itemHeight: 200,\n    text: [\"100%\", \"0%\"],\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    inRange: { color: t.seq }\n  },\n\n  series: [{\n    name: \"Retention %\",\n    type: \"heatmap\",\n    data: heatmapData,\n    label: {\n      show: true,\n      formatter: (params) => `${params.value[2]}%`,\n      color: \"#FFFFFF\",\n      textBorderColor: \"rgba(0,0,0,0.25)\",\n      textBorderWidth: 2,\n      fontSize: 14,\n      fontWeight: \"500\"\n    },\n    itemStyle: {\n      borderWidth: 2,\n      borderColor: t.pageBg\n    }\n  }]\n});\n"}