{"spec_id":"contour-density","library":"echarts","language":"javascript","code":"// anyplot.ai\n// contour-density: Density Contour Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-04\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: coffee-shop visits — arrival time vs. dwell time (fixed-seed LCG) ----\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction gaussianPair() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const r = Math.sqrt(-2 * Math.log(u1));\n  return [r * Math.cos(2 * Math.PI * u2), r * Math.sin(2 * Math.PI * u2)];\n}\nfunction cluster(n, cx, cy, sx, sy) {\n  const pts = [];\n  for (let i = 0; i < n; i++) {\n    const [z0, z1] = gaussianPair();\n    pts.push([cx + z0 * sx, cy + z1 * sy]);\n  }\n  return pts;\n}\n\n// Two visit patterns: brief morning coffee runs, and long afternoon work sessions\nconst morningRush = cluster(380, 8.1, 14, 0.9, 5.5);\nconst afternoonWork = cluster(270, 14.6, 55, 1.6, 12);\nconst points = morningRush.concat(afternoonWork);\n\n// --- Bivariate Gaussian KDE on a regular grid --------------------------------\nconst xs = points.map((p) => p[0]);\nconst ys = points.map((p) => p[1]);\nconst xMin = Math.min(...xs);\nconst xMax = Math.max(...xs);\nconst yMin = Math.min(...ys);\nconst yMax = Math.max(...ys);\nconst padX = (xMax - xMin) * 0.12;\nconst padY = (yMax - yMin) * 0.12;\nconst gx0 = xMin - padX;\nconst gx1 = xMax + padX;\nconst gy0 = yMin - padY;\nconst gy1 = yMax + padY;\n\nconst GRID = 60;\nconst bwX = (gx1 - gx0) * 0.09;\nconst bwY = (gy1 - gy0) * 0.09;\nconst gridX = Array.from({ length: GRID }, (_, i) => gx0 + (i * (gx1 - gx0)) / (GRID - 1));\nconst gridY = Array.from({ length: GRID }, (_, j) => gy0 + (j * (gy1 - gy0)) / (GRID - 1));\n\nconst density = Array.from({ length: GRID }, () => new Array(GRID).fill(0));\nfor (const [px, py] of points) {\n  for (let i = 0; i < GRID; i++) {\n    const dx = (gridX[i] - px) / bwX;\n    const gaussX = Math.exp(-0.5 * dx * dx);\n    if (gaussX < 1e-6) continue;\n    for (let j = 0; j < GRID; j++) {\n      const dy = (gridY[j] - py) / bwY;\n      density[i][j] += gaussX * Math.exp(-0.5 * dy * dy);\n    }\n  }\n}\nlet maxDensity = 0;\nfor (let i = 0; i < GRID; i++) {\n  for (let j = 0; j < GRID; j++) maxDensity = Math.max(maxDensity, density[i][j]);\n}\n\n// --- Marching-triangles contour extraction -----------------------------------\n// Each grid cell is split into 2 triangles so saddle points resolve without\n// the extra case table a full marching-squares implementation would need.\nfunction edgeCrossing(pa, pb, level) {\n  const frac = (level - pa.z) / (pb.z - pa.z);\n  return [pa.x + frac * (pb.x - pa.x), pa.y + frac * (pb.y - pa.y)];\n}\nfunction triangleSegment(p1, p2, p3, level) {\n  const crossings = [];\n  for (const [pa, pb] of [\n    [p1, p2],\n    [p2, p3],\n    [p3, p1],\n  ]) {\n    if ((pa.z - level) * (pb.z - level) < 0) crossings.push(edgeCrossing(pa, pb, level));\n  }\n  return crossings.length === 2 ? crossings : null;\n}\n\nconst LEVEL_FRACTIONS = [0.12, 0.28, 0.44, 0.6, 0.76, 0.92];\nconst contourData = [];\nfor (const frac of LEVEL_FRACTIONS) {\n  const level = frac * maxDensity;\n  for (let i = 0; i < GRID - 1; i++) {\n    for (let j = 0; j < GRID - 1; j++) {\n      const p00 = { x: gridX[i], y: gridY[j], z: density[i][j] };\n      const p10 = { x: gridX[i + 1], y: gridY[j], z: density[i + 1][j] };\n      const p11 = { x: gridX[i + 1], y: gridY[j + 1], z: density[i + 1][j + 1] };\n      const p01 = { x: gridX[i], y: gridY[j + 1], z: density[i][j + 1] };\n      const seg1 = triangleSegment(p00, p10, p11, level);\n      if (seg1) contourData.push({ coords: seg1, value: frac, lineStyle: { width: 1.5 + frac * 2 } });\n      const seg2 = triangleSegment(p00, p11, p01, level);\n      if (seg2) contourData.push({ coords: seg2, value: frac, lineStyle: { width: 1.5 + frac * 2 } });\n    }\n  }\n}\n\n// --- Init ---------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  color: t.palette,\n  title: {\n    text: \"contour-density · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  grid: { left: 110, right: 230, top: 110, bottom: 100, containLabel: true },\n  xAxis: {\n    type: \"value\",\n    name: \"Arrival Time (hour of day)\",\n    nameLocation: \"middle\",\n    nameGap: 44,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    scale: true,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Dwell Time (minutes)\",\n    nameLocation: \"middle\",\n    nameGap: 56,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    scale: true,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  visualMap: {\n    type: \"continuous\",\n    seriesIndex: 1,\n    min: LEVEL_FRACTIONS[0],\n    max: LEVEL_FRACTIONS[LEVEL_FRACTIONS.length - 1],\n    inRange: { color: t.seq },\n    text: [\"High density\", \"Low density\"],\n    textStyle: { color: t.inkSoft, fontSize: 13 },\n    itemWidth: 16,\n    itemHeight: 200,\n    right: 30,\n    top: \"middle\",\n    orient: \"vertical\",\n    hoverLink: false,\n    calculable: false,\n  },\n  series: [\n    {\n      // Raw visit records — muted context underlay beneath the contours\n      type: \"scatter\",\n      data: points,\n      symbolSize: 5,\n      itemStyle: { color: t.muted, opacity: 0.35 },\n      z: 1,\n    },\n    {\n      // KDE contour lines, colored by density level via the visualMap above\n      type: \"lines\",\n      coordinateSystem: \"cartesian2d\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      polyline: false,\n      data: contourData,\n      z: 2,\n    },\n  ],\n});\n"}