{"spec_id":"errorbar-asymmetric","library":"echarts","language":"javascript","code":"// anyplot.ai\n// errorbar-asymmetric: Asymmetric Error Bars Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Quarterly revenue-growth forecast. Upside potential grows every quarter\n// (planned product launches expand the ceiling), while downside risk spikes\n// in Q3 (a flagged supply-chain risk) before easing back — so the total\n// interval widens unevenly toward the end of the horizon, including one\n// quarter (Q3) whose low bound dips below 0%.\nconst quarters = [\"Q1\", \"Q2\", \"Q3\", \"Q4\", \"Q5\", \"Q6\"];\nconst forecast = [3.2, 4.1, 2.8, 5.5, 4.7, 6.0];\nconst errorLower = [1.5, 2.0, 3.2, 1.8, 2.5, 3.0];\nconst errorUpper = [0.8, 1.0, 1.2, 2.5, 3.5, 4.0];\n\nconst lowerBound = forecast.map((y, i) => y - errorLower[i]);\nconst upperBound = forecast.map((y, i) => y + errorUpper[i]);\nconst rangeData = quarters.map((_, i) => [i, lowerBound[i], upperBound[i]]);\n\n// Total interval width per quarter drives the error bar's stroke weight —\n// a redundant visual encoding so the widening-uncertainty trend reads at a\n// glance, not just from careful axis reading.\nconst totalWidth = quarters.map((_, i) => errorLower[i] + errorUpper[i]);\nconst minWidth = Math.min(...totalWidth);\nconst maxWidth = Math.max(...totalWidth);\nconst lineWidthFor = (i) =>\n  2 + ((totalWidth[i] - minWidth) / (maxWidth - minWidth)) * 2.5;\n\nconst brand = t.palette[0];\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Render item for the asymmetric whisker (vertical line + end caps) -----\nfunction renderErrorBar(params, api) {\n  const xValue = api.value(0);\n  const lowPoint = api.coord([xValue, api.value(1)]);\n  const highPoint = api.coord([xValue, api.value(2)]);\n  const halfWidth = api.size([1, 0])[0] * 0.15;\n  const style = {\n    stroke: brand,\n    lineWidth: lineWidthFor(params.dataIndex),\n    lineCap: \"round\",\n  };\n\n  return {\n    type: \"group\",\n    children: [\n      {\n        type: \"line\",\n        shape: { x1: lowPoint[0], y1: lowPoint[1], x2: highPoint[0], y2: highPoint[1] },\n        style,\n      },\n      {\n        type: \"line\",\n        shape: {\n          x1: lowPoint[0] - halfWidth,\n          y1: lowPoint[1],\n          x2: lowPoint[0] + halfWidth,\n          y2: lowPoint[1],\n        },\n        style,\n      },\n      {\n        type: \"line\",\n        shape: {\n          x1: highPoint[0] - halfWidth,\n          y1: highPoint[1],\n          x2: highPoint[0] + halfWidth,\n          y2: highPoint[1],\n        },\n        style,\n      },\n    ],\n  };\n}\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"errorbar-asymmetric · javascript · echarts · anyplot.ai\",\n    subtext: \"Bars mark the 10th-90th percentile forecast range per quarter\",\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: 27, fontWeight: \"bold\" },\n    subtextStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  legend: {\n    icon: \"roundRect\",\n    top: 112,\n    data: [\"10th-90th percentile range\", \"Point forecast\"],\n    textStyle: { color: t.inkSoft, fontSize: 16 },\n  },\n  grid: { left: 90, right: 60, top: 190, bottom: 80 },\n  xAxis: {\n    type: \"category\",\n    data: quarters,\n    name: \"Forecast quarter\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\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: \"Revenue growth (%)\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.inkSoft, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 14, formatter: \"{value}%\" },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"10th-90th percentile range\",\n      type: \"custom\",\n      renderItem: renderErrorBar,\n      itemStyle: { color: brand },\n      encode: { x: 0, y: [1, 2] },\n      data: rangeData,\n      z: 2,\n    },\n    {\n      name: \"Point forecast\",\n      type: \"scatter\",\n      data: forecast,\n      symbolSize: 20,\n      itemStyle: { color: brand, borderColor: t.pageBg, borderWidth: 3 },\n      z: 3,\n    },\n  ],\n});\n"}