{"spec_id":"histogram-cumulative","library":"echarts","language":"javascript","code":"// anyplot.ai\n// histogram-cumulative: Cumulative Histogram\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: warehouse order processing times (minutes), deterministic LCG ----\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (state * 1664525 + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = makeLcg(42);\n\nfunction randNormal(mean, std) {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\n// Two processing streams: automated picking (fast) and manual picking (slower)\nconst processingTimes = [];\nfor (let i = 0; i < 420; i++) processingTimes.push(randNormal(18, 5));\nfor (let i = 0; i < 180; i++) processingTimes.push(randNormal(38, 9));\n\n// --- Histogram + cumulative percentage --------------------------------------\nconst binCount = 20;\nconst minVal = Math.max(0, Math.min(...processingTimes));\nconst maxVal = Math.max(...processingTimes);\nconst binWidth = (maxVal - minVal) / binCount;\n\nconst counts = new Array(binCount).fill(0);\nprocessingTimes.forEach((v) => {\n  let idx = Math.floor((v - minVal) / binWidth);\n  if (idx >= binCount) idx = binCount - 1;\n  if (idx < 0) idx = 0;\n  counts[idx] += 1;\n});\n\nconst binEdges = [];\nfor (let i = 0; i <= binCount; i++) binEdges.push(minVal + i * binWidth);\nconst labels = binEdges.slice(1).map((edge) => edge.toFixed(0));\n\nlet running = 0;\nconst cumulativePct = counts.map((c) => {\n  running += c;\n  return Number(((running / processingTimes.length) * 100).toFixed(1));\n});\n\n// Evenly spaced round-number tick labels (every 10 minutes) instead of the\n// raw, irregular bin-edge values.\nconst maxRounded = Math.ceil(maxVal / 10) * 10;\nconst tickLabelByIndex = new Map();\nfor (let v = 10; v <= maxRounded; v += 10) {\n  const idx = Math.max(\n    0,\n    Math.min(binCount - 1, Math.round((v - minVal) / binWidth) - 1)\n  );\n  tickLabelByIndex.set(idx, String(v));\n}\n\n// Interpolate the median processing time (the 50th-percentile crossing) so\n// it can be called out directly on the curve.\nconst half = processingTimes.length / 2;\nlet cumBefore = 0;\nlet medianBinIndex = binCount - 1;\nlet medianTime = maxVal;\nfor (let i = 0; i < binCount; i++) {\n  const cumAfter = cumBefore + counts[i];\n  if (cumAfter >= half) {\n    medianBinIndex = i;\n    const fraction = counts[i] === 0 ? 0 : (half - cumBefore) / counts[i];\n    medianTime = minVal + (i + fraction) * binWidth;\n    break;\n  }\n  cumBefore = cumAfter;\n}\n\n// --- Init ---------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"histogram-cumulative · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 500 },\n  },\n  grid: { left: 110, right: 60, top: 100, bottom: 110 },\n  tooltip: {\n    trigger: \"axis\",\n    valueFormatter: (value) => `${value}%`,\n  },\n  xAxis: {\n    type: \"category\",\n    data: labels,\n    name: \"Processing Time (minutes)\",\n    nameLocation: \"middle\",\n    nameGap: 50,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 13,\n      interval: (index) => tickLabelByIndex.has(index),\n      formatter: (value, index) => tickLabelByIndex.get(index) ?? \"\",\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    min: 0,\n    max: 100,\n    name: \"Cumulative Orders (%)\",\n    nameLocation: \"middle\",\n    nameGap: 70,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 13, formatter: \"{value}%\" },\n    axisLine: { show: false },\n    axisTick: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      type: \"bar\",\n      name: \"Cumulative Orders\",\n      data: cumulativePct,\n      barWidth: \"88%\",\n      itemStyle: { color: t.palette[0], borderRadius: [3, 3, 0, 0] },\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5 },\n        label: {\n          color: t.inkSoft,\n          fontSize: 12,\n          formatter: \"50th percentile\",\n          position: \"insideStartTop\",\n        },\n        data: [{ yAxis: 50 }],\n      },\n      markPoint: {\n        symbol: \"circle\",\n        symbolSize: 14,\n        itemStyle: { color: t.ink, borderColor: t.pageBg, borderWidth: 2 },\n        label: {\n          show: true,\n          color: t.ink,\n          fontSize: 13,\n          fontWeight: 600,\n          position: \"top\",\n          distance: 12,\n          formatter: `Median ≈ ${medianTime.toFixed(0)} min`,\n        },\n        data: [{ coord: [medianBinIndex, cumulativePct[medianBinIndex]] }],\n      },\n    },\n  ],\n});\n"}