{"spec_id":"subplot-grid","library":"echarts","language":"javascript","code":"// anyplot.ai\n// subplot-grid: Subplot Grid Layout\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 82/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Mulberry32 PRNG — the browser has no seeded Math.random(), so a tiny\n// fixed-seed generator stands in for np.random.seed(42) / set.seed(42).\nfunction mulberry32(seed) {\n  return () => {\n    seed = (seed + 0x6d2b79f5) | 0;\n    let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);\n    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;\n    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;\n  };\n}\nconst rand = mulberry32(42);\n\nconst DAYS = 60;\nconst startDate = new Date(Date.UTC(2024, 0, 2));\nconst dates = [];\nconst closePrices = [];\nconst dailyVolumes = [];\nconst dailyReturns = [];\n\nlet price = 150;\nfor (let i = 0; i < DAYS; i++) {\n  const d = new Date(startDate);\n  d.setUTCDate(d.getUTCDate() + i);\n  dates.push(`${d.getUTCMonth() + 1}/${d.getUTCDate()}`);\n\n  const changePct = (rand() - 0.5) * 4; // daily move in [-2%, +2%]\n  price *= 1 + changePct / 100;\n  closePrices.push(Math.round(price * 100) / 100);\n  dailyReturns.push(Math.round(changePct * 100) / 100);\n\n  const baseVolume = 500000 + rand() * 1500000;\n  const spike = Math.abs(changePct) * 400000; // big moves trade heavier volume\n  dailyVolumes.push(Math.round(baseVolume + spike));\n}\n\n// Histogram of daily returns — 10 equal-width bins.\nconst BIN_COUNT = 10;\nconst minReturn = Math.min(...dailyReturns);\nconst maxReturn = Math.max(...dailyReturns);\nconst binWidth = (maxReturn - minReturn) / BIN_COUNT;\nconst binLabels = [];\nconst binCounts = new Array(BIN_COUNT).fill(0);\nfor (let i = 0; i < BIN_COUNT; i++) {\n  binLabels.push((minReturn + i * binWidth).toFixed(1));\n}\ndailyReturns.forEach((r) => {\n  let idx = Math.floor((r - minReturn) / binWidth);\n  if (idx >= BIN_COUNT) idx = BIN_COUNT - 1;\n  if (idx < 0) idx = 0;\n  binCounts[idx]++;\n});\nconst binData = binCounts.map((count, i) => ({\n  value: count,\n  // Semantic exception: up/gain -> green, down/loss -> red (finance convention).\n  itemStyle: { color: Number(binLabels[i]) >= 0 ? t.palette[0] : t.palette[4] },\n}));\n\n// Volume-vs-return scatter (bottom-right cell) — volume in millions.\nconst returnVsVolume = dailyReturns.map((r, i) => [dailyVolumes[i] / 1e6, r]);\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\n// Layout: 2x2 grid — each cell a distinct plot type against the same trading\n// window. Top row shares the date axis geometry (price vs. volume, same\n// x-domain and width) for direct visual comparison; bottom row uses\n// independent axes since the histogram (return bins) and scatter (volume)\n// plot unrelated domains.\nconst COLUMN = { left: \"6%\", width: \"41%\" };\nconst COLUMN_RIGHT = { left: \"55%\", width: \"41%\" };\nconst ROW_TOP = { top: \"16%\", height: \"30%\" };\nconst ROW_BOTTOM = { top: \"63%\", height: \"30%\" };\n\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  textStyle: { color: t.inkSoft },\n  tooltip: { trigger: \"axis\" },\n  title: [\n    {\n      text: \"subplot-grid · javascript · echarts · anyplot.ai\",\n      left: \"center\",\n      top: \"2%\",\n      textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n    },\n    { text: \"Closing Price ($)\", left: COLUMN.left, top: \"10%\", textStyle: { color: t.ink, fontSize: 17, fontWeight: \"normal\" } },\n    { text: \"Daily Volume\", left: COLUMN_RIGHT.left, top: \"10%\", textStyle: { color: t.ink, fontSize: 17, fontWeight: \"normal\" } },\n    { text: \"Daily Returns Distribution\", left: COLUMN.left, top: \"56%\", textStyle: { color: t.ink, fontSize: 17, fontWeight: \"normal\" } },\n    { text: \"Volume vs. Return\", left: COLUMN_RIGHT.left, top: \"56%\", textStyle: { color: t.ink, fontSize: 17, fontWeight: \"normal\" } },\n  ],\n  grid: [\n    { ...COLUMN, ...ROW_TOP, containLabel: true }, // 0: price line\n    { ...COLUMN_RIGHT, ...ROW_TOP, containLabel: true }, // 1: volume bar\n    { ...COLUMN, ...ROW_BOTTOM, containLabel: true }, // 2: returns histogram\n    { ...COLUMN_RIGHT, ...ROW_BOTTOM, containLabel: true }, // 3: volume-vs-return scatter\n  ],\n  xAxis: [\n    {\n      gridIndex: 0,\n      type: \"category\",\n      data: dates,\n      boundaryGap: false,\n      axisLabel: { color: t.inkSoft, fontSize: 13, interval: 9 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { show: false },\n    },\n    {\n      gridIndex: 1,\n      type: \"category\",\n      data: dates,\n      axisLabel: { color: t.inkSoft, fontSize: 13, interval: 9 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { show: false },\n    },\n    {\n      gridIndex: 2,\n      type: \"category\",\n      data: binLabels,\n      name: \"Daily return (%)\",\n      nameLocation: \"middle\",\n      nameGap: 32,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13, rotate: 45 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { show: false },\n    },\n    {\n      gridIndex: 3,\n      type: \"value\",\n      name: \"Volume (M)\",\n      nameLocation: \"middle\",\n      nameGap: 32,\n      nameTextStyle: { color: t.inkSoft, fontSize: 14 },\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n  ],\n  yAxis: [\n    {\n      // Extra headroom (not plain scale:true) so the \"Period high\" markPoint\n      // pin has clearance above the line and never collides with the subtitle.\n      gridIndex: 0,\n      type: \"value\",\n      min: (value) => Math.floor(value.min - (value.max - value.min) * 0.08),\n      max: (value) => Math.ceil(value.max + (value.max - value.min) * 0.18),\n      axisLabel: { color: t.inkSoft, fontSize: 13, formatter: \"${value}\" },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 1,\n      type: \"value\",\n      axisLabel: { color: t.inkSoft, fontSize: 13, formatter: (v) => `${(v / 1e6).toFixed(1)}M` },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 2,\n      type: \"value\",\n      axisLabel: { color: t.inkSoft, fontSize: 13 },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n    {\n      gridIndex: 3,\n      type: \"value\",\n      axisLabel: { color: t.inkSoft, fontSize: 13, formatter: \"{value}%\" },\n      axisLine: { lineStyle: { color: t.inkSoft } },\n      splitLine: { lineStyle: { color: t.grid } },\n    },\n  ],\n  series: [\n    {\n      name: \"Close price\",\n      type: \"line\",\n      xAxisIndex: 0,\n      yAxisIndex: 0,\n      data: closePrices,\n      showSymbol: false,\n      lineStyle: { width: 3, color: t.palette[0] },\n      areaStyle: { color: t.palette[0], opacity: 0.08 },\n      markPoint: {\n        symbolSize: 48,\n        symbolOffset: [22, \"-45%\"],\n        itemStyle: { color: t.palette[0] },\n        label: { color: \"#fff\", fontSize: 11, fontWeight: 600, formatter: (p) => `$${p.value.toFixed(2)}` },\n        data: [{ type: \"max\", name: \"Period high\" }],\n      },\n    },\n    {\n      name: \"Volume\",\n      type: \"bar\",\n      xAxisIndex: 1,\n      yAxisIndex: 1,\n      data: dailyVolumes,\n      itemStyle: { color: t.palette[2] },\n      barMaxWidth: 10,\n      markPoint: {\n        symbolSize: 52,\n        itemStyle: { color: t.palette[2] },\n        label: { color: \"#fff\", fontSize: 10, fontWeight: 600, formatter: (p) => `${(p.value / 1e6).toFixed(1)}M` },\n        data: [{ type: \"max\", name: \"Peak volume\" }],\n      },\n    },\n    {\n      name: \"Return frequency\",\n      type: \"bar\",\n      xAxisIndex: 2,\n      yAxisIndex: 2,\n      data: binData,\n      barMaxWidth: 34,\n    },\n    {\n      name: \"Volume vs. return\",\n      type: \"scatter\",\n      xAxisIndex: 3,\n      yAxisIndex: 3,\n      data: returnVsVolume,\n      symbolSize: 12,\n      itemStyle: { color: t.palette[5], opacity: 0.8 },\n    },\n  ],\n});\n"}