{"spec_id":"hexbin-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// hexbin-basic: Basic Hexbin Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-25\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Tiny LCG so the browser (no seeded Math.random) still reproduces the same\n// sensor readings on every render.\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function lcg() {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\n\nfunction randomNormal(rng, mean, std) {\n  const u1 = Math.max(rng(), 1e-9);\n  const u2 = rng();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\nconst DATA_X_MIN = 0;\nconst DATA_X_MAX = 100;\nconst DATA_Y_MIN = 0;\nconst DATA_Y_MAX = 60;\n\nconst rng = makeLcg(42);\n\n// Three industrial-IoT operating regimes plus background noise: temperature\n// (°C) vs. vibration amplitude (mm/s) readings from a fleet of sensors.\nconst regimes = [\n  { cx: 30, cy: 14, sx: 9, sy: 4, n: 1800 }, // normal operation\n  { cx: 55, cy: 27, sx: 10, sy: 6, n: 1400 }, // elevated load\n  { cx: 76, cy: 41, sx: 7, sy: 5, n: 800 }, // thermal-stress zone\n];\n\nconst readings = [];\nregimes.forEach((regime) => {\n  for (let i = 0; i < regime.n; i++) {\n    const temperature = randomNormal(rng, regime.cx, regime.sx);\n    const vibration = randomNormal(rng, regime.cy, regime.sy);\n    readings.push([\n      Math.min(Math.max(temperature, DATA_X_MIN + 1), DATA_X_MAX - 1),\n      Math.min(Math.max(vibration, DATA_Y_MIN + 1), DATA_Y_MAX - 1),\n    ]);\n  }\n});\nfor (let i = 0; i < 500; i++) {\n  readings.push([5 + rng() * 90, 2 + rng() * 55]);\n}\n\n// --- Hexagonal binning --------------------------------------------------------\n// ECharts has no built-in hexbin series, so bins are aggregated by hand and\n// drawn with a \"custom\" series (a native ECharts capability, not a workaround).\n// Binning runs in CSS-mount pixel space (matching the grid rect + axis domain\n// below) rather than data space, so the hexagons stay perfectly regular even\n// though the x/y axes cover different physical units and ranges.\nconst GRID_LEFT = 130;\nconst GRID_RIGHT = 250;\nconst GRID_TOP = 130;\nconst GRID_BOTTOM = 110;\nconst plotWidth = size.width - GRID_LEFT - GRID_RIGHT;\nconst plotHeight = size.height - GRID_TOP - GRID_BOTTOM;\n\nconst HEX_COLUMNS = 24;\nconst hexPxRadius = plotWidth / (HEX_COLUMNS * Math.sqrt(3));\nconst dx = hexPxRadius * Math.sqrt(3);\nconst dy = hexPxRadius * 1.5;\n\n// Pad the axis domain by one hex radius on every side so bins centered near\n// the true data extent stay fully inside the plot frame instead of spilling\n// past the axis line and covering tick labels.\nconst xPad = (hexPxRadius / plotWidth) * (DATA_X_MAX - DATA_X_MIN);\nconst yPad = (hexPxRadius / plotHeight) * (DATA_Y_MAX - DATA_Y_MIN);\nconst X_MIN = DATA_X_MIN - xPad;\nconst X_MAX = DATA_X_MAX + xPad;\nconst Y_MIN = DATA_Y_MIN - yPad;\nconst Y_MAX = DATA_Y_MAX + yPad;\n\nconst xToPx = (x) => GRID_LEFT + ((x - X_MIN) / (X_MAX - X_MIN)) * plotWidth;\nconst yToPx = (y) => GRID_TOP + (1 - (y - Y_MIN) / (Y_MAX - Y_MIN)) * plotHeight;\nconst pxToX = (px) => X_MIN + ((px - GRID_LEFT) / plotWidth) * (X_MAX - X_MIN);\nconst pxToY = (py) => Y_MIN + (1 - (py - GRID_TOP) / plotHeight) * (Y_MAX - Y_MIN);\n\nfunction binReadings(points) {\n  const bins = new Map();\n  points.forEach(([x, y]) => {\n    const px = xToPx(x);\n    const py = yToPx(y);\n    const rowF = py / dy;\n    let pj = Math.round(rowF);\n    let colF = px / dx - (pj & 1 ? 0.5 : 0);\n    let pi = Math.round(colF);\n    const dRow = rowF - pj;\n\n    if (Math.abs(dRow) * 3 > 1) {\n      const dCol = colF - pi;\n      const pi2 = pi + (colF < pi ? -1 : 1) * 0.5;\n      const pj2 = pj + (rowF < pj ? -1 : 1);\n      const dCol2 = colF - pi2;\n      const dRow2 = rowF - pj2;\n      if (dCol * dCol + dRow * dRow > dCol2 * dCol2 + dRow2 * dRow2) {\n        pi = pi2 + (pj & 1 ? 1 : -1) * 0.5;\n        pj = pj2;\n      }\n    }\n\n    const key = `${pi}|${pj}`;\n    let bin = bins.get(key);\n    if (!bin) {\n      bin = { cxPx: (pi + (pj & 1 ? 0.5 : 0)) * dx, cyPx: pj * dy, count: 0 };\n      bins.set(key, bin);\n    }\n    bin.count += 1;\n  });\n  return [...bins.values()];\n}\n\nconst hexBins = binReadings(readings);\nconst maxCount = Math.max(...hexBins.map((b) => b.count));\nconst hexSeriesData = hexBins.map((b) => [pxToX(b.cxPx), pxToY(b.cyPx), b.count]);\n\n// --- Custom renderer: regular hexagon per bin --------------------------------\n// A fixed pixel radius (matching the pixel-space binning above) keeps every\n// hexagon a true regular hexagon and tiling gap-free, independent of the\n// axes' data-to-pixel scale.\nfunction renderHex(params, api) {\n  const center = api.coord([api.value(0), api.value(1)]);\n  const r = hexPxRadius * 0.94;\n  const points = [];\n  for (let k = 0; k < 6; k++) {\n    const angle = (Math.PI / 180) * (60 * k - 30);\n    points.push([center[0] + r * Math.cos(angle), center[1] + r * Math.sin(angle)]);\n  }\n  return {\n    type: \"polygon\",\n    shape: { points },\n    style: api.style({ fill: api.visual(\"color\"), stroke: t.pageBg, lineWidth: 1 }),\n  };\n}\n\n// --- Init ---------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"hexbin-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    formatter: (p) =>\n      `Temperature: ${p.value[0].toFixed(1)} °C<br/>Vibration: ${p.value[1].toFixed(1)} mm/s<br/>Readings: ${p.value[2]}`,\n  },\n  grid: { left: GRID_LEFT, right: GRID_RIGHT, top: GRID_TOP, bottom: GRID_BOTTOM },\n  xAxis: {\n    type: \"value\",\n    min: X_MIN,\n    max: X_MAX,\n    name: \"Operating Temperature (°C)\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, showMinLabel: false, showMaxLabel: false },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    min: Y_MIN,\n    max: Y_MAX,\n    name: \"Vibration Amplitude (mm/s)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, showMinLabel: false, showMaxLabel: false },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  visualMap: {\n    type: \"continuous\",\n    dimension: 2,\n    seriesIndex: 0,\n    min: 0,\n    max: maxCount,\n    orient: \"vertical\",\n    right: 40,\n    top: \"middle\",\n    itemHeight: 420,\n    text: [\"High density\", \"Low density\"],\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    inRange: { color: t.seq },\n    calculable: true,\n  },\n  series: [\n    {\n      type: \"custom\",\n      name: \"Sensor Reading Density\",\n      coordinateSystem: \"cartesian2d\",\n      renderItem: renderHex,\n      data: hexSeriesData,\n      encode: { x: 0, y: 1, tooltip: [0, 1, 2] },\n    },\n  ],\n});\n"}