{"spec_id":"heatmap-correlation","library":"echarts","language":"javascript","code":"// anyplot.ai\n// heatmap-correlation: Correlation Matrix Heatmap\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-20\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Pairwise correlations among wellness-tracking metrics from a fitness cohort.\nconst variables = [\n  \"Sleep Hours\",\n  \"Resting HR\",\n  \"Daily Steps\",\n  \"VO2 Max\",\n  \"Body Fat %\",\n  \"Stress Score\",\n  \"Hydration\",\n];\n\nconst correlationMatrix = [\n  [1.0, -0.42, 0.18, 0.35, -0.28, -0.61, 0.22],\n  [-0.42, 1.0, -0.33, -0.71, 0.45, 0.39, -0.15],\n  [0.18, -0.33, 1.0, 0.58, -0.49, -0.24, 0.31],\n  [0.35, -0.71, 0.58, 1.0, -0.66, -0.3, 0.19],\n  [-0.28, 0.45, -0.49, -0.66, 1.0, 0.27, -0.2],\n  [-0.61, 0.39, -0.24, -0.3, 0.27, 1.0, -0.44],\n  [0.22, -0.15, 0.31, 0.19, -0.2, -0.44, 1.0],\n];\n\n// Per-cell label color: pick whichever ink anchor (dark vs light) wins more\n// WCAG contrast against the cell's interpolated imprint_div fill, since the\n// diverging colormap's red and blue endpoints have different luminance and a\n// single fixed text color can't stay legible across the whole range.\nconst relLuminance = (r, g, b) => {\n  const chan = (v) => {\n    const c = v / 255;\n    return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;\n  };\n  return 0.2126 * chan(r) + 0.7152 * chan(g) + 0.0722 * chan(b);\n};\n\nconst [redRgb, midRgb, blueRgb] = t.div.map((hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)));\nconst DARK_TEXT = \"#1A1A17\";\nconst LIGHT_TEXT = \"#F0EFE8\";\nconst darkLum = relLuminance(26, 26, 23);\nconst lightLum = relLuminance(240, 239, 232);\n\nconst labelColorFor = (value) => {\n  const [from, to] = value <= 0 ? [midRgb, redRgb] : [midRgb, blueRgb];\n  const cellLum = relLuminance(...from.map((v, i) => v + (to[i] - v) * Math.abs(value)));\n  const darkContrast = (Math.max(cellLum, darkLum) + 0.05) / (Math.min(cellLum, darkLum) + 0.05);\n  const lightContrast = (Math.max(cellLum, lightLum) + 0.05) / (Math.min(cellLum, lightLum) + 0.05);\n  return darkContrast >= lightContrast ? DARK_TEXT : LIGHT_TEXT;\n};\n\n// Mask the upper triangle to remove the redundant mirrored half — only the\n// diagonal and lower triangle carry a cell, matching the row/column order.\n// Diagonal (self-correlation, always 1.00) is muted and unlabeled so focus\n// stays on the informative off-diagonal correlations.\nconst heatmapData = [];\nfor (let row = 0; row < variables.length; row++) {\n  for (let col = 0; col <= row; col++) {\n    const value = correlationMatrix[row][col];\n    const isDiagonal = col === row;\n    heatmapData.push({\n      value: [col, row, value],\n      label: isDiagonal ? { show: false } : { color: labelColorFor(value) },\n      itemStyle: isDiagonal ? { opacity: 0.4 } : {},\n    });\n  }\n}\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Title (scaled to length per anyplot title-fontsize rule) ---------------\nconst title = \"heatmap-correlation · javascript · echarts · anyplot.ai\";\nconst titleFontSize = Math.round(22 * Math.min(1, 67 / title.length));\n\n// --- Option -------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: title,\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 500 },\n  },\n  grid: { left: 170, right: 60, top: 130, bottom: 170 },\n  xAxis: {\n    type: \"category\",\n    data: variables,\n    splitArea: { show: false },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    axisLabel: { color: t.inkSoft, fontSize: 15, rotate: 40 },\n  },\n  yAxis: {\n    type: \"category\",\n    data: variables,\n    inverse: true,\n    splitArea: { show: false },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    axisLabel: { color: t.inkSoft, fontSize: 15 },\n  },\n  // Placed inside the grid's own top-right column, which the triangle mask\n  // leaves blank for every row but the last — reuses that space instead of\n  // reserving a separate margin, so the matrix itself can fill more of the\n  // canvas.\n  visualMap: {\n    type: \"continuous\",\n    min: -1,\n    max: 1,\n    calculable: false,\n    orient: \"vertical\",\n    right: 100,\n    top: 150,\n    itemHeight: 650,\n    itemWidth: 24,\n    inRange: { color: t.div },\n    text: [\"1.0\", \"-1.0\"],\n    textStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  series: [\n    {\n      type: \"heatmap\",\n      data: heatmapData,\n      itemStyle: { borderColor: t.pageBg, borderWidth: 4 },\n      label: {\n        show: true,\n        fontSize: 15,\n        fontWeight: 600,\n        formatter: (params) => params.value[2].toFixed(2),\n      },\n    },\n  ],\n});\n"}