{"spec_id":"lollipop-grouped","library":"echarts","language":"javascript","code":"// anyplot.ai\n// lollipop-grouped: Grouped Lollipop Chart\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Revenue by product line across regions, sorted by total revenue descending.\nconst seriesNames = [\"Hardware\", \"Software\", \"Services\"];\nconst regions = [\n  { name: \"North America\", values: [42, 68, 35] },\n  { name: \"Asia Pacific\", values: [38, 61, 22] },\n  { name: \"Europe\", values: [31, 54, 29] },\n  { name: \"Latin America\", values: [18, 24, 12] },\n  { name: \"Middle East & Africa\", values: [14, 19, 9] },\n];\nconst categories = regions.map((r) => r.name);\n\n// --- Custom lollipop renderer -----------------------------------------------\n// ECharts has no built-in lollipop series; a \"custom\" series drawing a thin\n// stem + circular head per data point is the native way to build one. Each\n// series gets a small horizontal offset within its category band so the\n// lollipops for a category sit side by side, mirroring grouped-bar layout.\nconst GROUP_RATIO = 0.62;\nconst DOT_RADIUS = 15;\nconst STEM_WIDTH = 4;\n\nfunction makeRenderItem(seriesIndex) {\n  return function (params, api) {\n    const categoryIndex = api.value(0);\n    const value = api.value(1);\n    const start = api.coord([0, categoryIndex]);\n    const end = api.coord([value, categoryIndex]);\n    const bandHeight = api.size([0, 1])[1];\n    const step = (bandHeight * GROUP_RATIO) / seriesNames.length;\n    const offset = (seriesIndex - (seriesNames.length - 1) / 2) * step;\n    start[1] += offset;\n    end[1] += offset;\n    const color = t.palette[seriesIndex];\n    return {\n      type: \"group\",\n      children: [\n        {\n          type: \"line\",\n          shape: { x1: start[0], y1: start[1], x2: end[0], y2: end[1] },\n          style: { stroke: color, lineWidth: STEM_WIDTH },\n        },\n        {\n          type: \"circle\",\n          shape: { cx: end[0], cy: end[1], r: DOT_RADIUS },\n          style: { fill: color, stroke: t.pageBg, lineWidth: 2 },\n        },\n      ],\n    };\n  };\n}\n\nconst series = seriesNames.map((name, seriesIndex) => ({\n  name,\n  type: \"custom\",\n  renderItem: makeRenderItem(seriesIndex),\n  encode: { x: 1, y: 0 },\n  data: categories.map((_, categoryIndex) => [\n    categoryIndex,\n    regions[categoryIndex].values[seriesIndex],\n  ]),\n  z: 2,\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: \"lollipop-grouped · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22 },\n  },\n  legend: {\n    data: seriesNames,\n    top: 74,\n    left: \"center\",\n    itemWidth: 18,\n    itemHeight: 12,\n    textStyle: { color: t.ink, fontSize: 16 },\n  },\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink },\n    formatter: (p) =>\n      `${p.seriesName} — ${categories[p.value[0]]}<br/>$${p.value[1]}M`,\n  },\n  grid: { left: 280, right: 90, top: 150, bottom: 80 },\n  xAxis: {\n    type: \"value\",\n    name: \"Revenue ($M)\",\n    nameLocation: \"center\",\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: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"category\",\n    data: categories,\n    inverse: true,\n    axisLabel: { color: t.inkSoft, fontSize: 16 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { show: false },\n    splitLine: { show: false },\n  },\n  series,\n});\n"}