{"spec_id":"scatter-pitch-events","library":"echarts","language":"javascript","code":"// anyplot.ai\n// scatter-pitch-events: Soccer Pitch Event Map\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-21\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME;\n\n// Seeded LCG for reproducible data\nlet seed = 42;\nfunction rng() {\n  seed = (seed * 1664525 + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\n\n// Pitch constants (FIFA standard, metres)\nconst PW = 105, PH = 68;\nconst PITCH_GREEN = THEME === 'dark' ? '#2C5A27' : '#3D7A35';\nconst PITCH_LINE  = 'rgba(255,255,255,0.88)';\n\n// Geometry helpers\nfunction circlePts(cx, cy, r, n = 64) {\n  const pts = [];\n  for (let i = 0; i <= n; i++) {\n    const a = (i / n) * 2 * Math.PI;\n    pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);\n  }\n  return pts;\n}\n\nfunction arcPts(cx, cy, r, a0, a1, n = 32) {\n  const pts = [];\n  for (let i = 0; i <= n; i++) {\n    const a = a0 + (i / n) * (a1 - a0);\n    pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]);\n  }\n  return pts;\n}\n\nfunction mkLine(data) {\n  return {\n    type: 'line',\n    data,\n    symbol: 'none',\n    lineStyle: { color: PITCH_LINE, width: 2.5 },\n    z: 2,\n    silent: true,\n    legendHoverLink: false,\n    emphasis: { disabled: true },\n  };\n}\n\n// Pitch marking bounds (FIFA)\nconst PA_Y1   = (PH - 40.32) / 2;         // 13.84 — penalty area bottom\nconst PA_Y2   = PH - PA_Y1;               // 54.16 — penalty area top\nconst GA_Y1   = (PH - 18.32) / 2;         // 24.84 — goal area bottom\nconst GA_Y2   = PH - GA_Y1;               // 43.16 — goal area top\nconst PARC    = Math.acos(5.5 / 9.15);    // penalty arc half-angle (~0.926 rad)\n\nconst pitchSeries = [\n  mkLine([[0,0],[PW,0],[PW,PH],[0,PH],[0,0]]),                  // outer boundary\n  mkLine([[PW/2,0],[PW/2,PH]]),                                  // halfway line\n  mkLine(circlePts(PW/2, PH/2, 9.15)),                          // center circle\n  mkLine(circlePts(PW/2, PH/2, 0.5, 20)),                       // center spot\n  mkLine([[0,PA_Y1],[16.5,PA_Y1],[16.5,PA_Y2],[0,PA_Y2]]),      // left penalty area\n  mkLine([[0,GA_Y1],[5.5,GA_Y1],[5.5,GA_Y2],[0,GA_Y2]]),        // left goal area\n  mkLine(circlePts(11, PH/2, 0.5, 20)),                         // left penalty spot\n  mkLine(arcPts(11, PH/2, 9.15, -PARC, PARC)),                  // left penalty arc\n  mkLine([[PW,PA_Y1],[88.5,PA_Y1],[88.5,PA_Y2],[PW,PA_Y2]]),    // right penalty area\n  mkLine([[PW,GA_Y1],[99.5,GA_Y1],[99.5,GA_Y2],[PW,GA_Y2]]),    // right goal area\n  mkLine(circlePts(94, PH/2, 0.5, 20)),                         // right penalty spot\n  mkLine(arcPts(94, PH/2, 9.15, Math.PI-PARC, Math.PI+PARC)),  // right penalty arc\n  mkLine(arcPts(0,  0,  1, 0,           Math.PI/2)),            // corner arcs\n  mkLine(arcPts(PW, 0,  1, Math.PI/2,   Math.PI)),\n  mkLine(arcPts(PW, PH, 1, Math.PI,     3*Math.PI/2)),\n  mkLine(arcPts(0,  PH, 1, 3*Math.PI/2, 2*Math.PI)),\n  mkLine([[-2.44,30.34],[0,30.34]]),                             // left goal\n  mkLine([[-2.44,37.66],[0,37.66]]),\n  mkLine([[-2.44,30.34],[-2.44,37.66]]),\n  mkLine([[PW,30.34],[PW+2.44,30.34]]),                          // right goal\n  mkLine([[PW,37.66],[PW+2.44,37.66]]),\n  mkLine([[PW+2.44,30.34],[PW+2.44,37.66]]),\n];\n\n// Generate event data\nconst passes = [], shots = [], tackles = [], intercepts = [];\n\nfor (let i = 0; i < 80; i++) {\n  const x1 = rng() * PW, y1 = rng() * PH;\n  const x2 = Math.max(1, Math.min(PW-1, x1 + (rng()-0.25)*20));\n  const y2 = Math.max(1, Math.min(PH-1, y1 + (rng()-0.5)*14));\n  passes.push({ x1, y1, x2, y2, ok: rng() > 0.28 });\n}\nfor (let i = 0; i < 22; i++) {\n  shots.push({ x: 62 + rng()*40, y: 14 + rng()*40, ok: rng() > 0.68 });\n}\nfor (let i = 0; i < 35; i++) {\n  tackles.push({ x: rng()*PW, y: rng()*PH, ok: rng() > 0.38 });\n}\nfor (let i = 0; i < 25; i++) {\n  intercepts.push({ x: rng()*PW*0.65, y: rng()*PH });\n}\n\n// Arrow data for lines series (pass trajectories, shot vectors to goal)\nconst passArrows = passes.map(p => ({\n  coords: [[p.x1,p.y1],[p.x2,p.y2]],\n  lineStyle: { opacity: p.ok ? 0.55 : 0.18 },\n}));\nconst shotArrows = shots.map(s => ({\n  coords: [[s.x,s.y],[PW+0.5, PH/2]],\n  lineStyle: { opacity: s.ok ? 0.55 : 0.18 },\n}));\n\nconst eventSeries = [\n  // Pass arrows\n  {\n    type: 'lines', coordinateSystem: 'cartesian2d',\n    data: passArrows,\n    symbol: ['none','arrow'], symbolSize: 6,\n    effect: { show: false },\n    lineStyle: { color: t.palette[0], width: 1.6 },\n    z: 4, silent: true, legendHoverLink: false,\n  },\n  // Shot trajectories (dashed arrow to goal)\n  {\n    type: 'lines', coordinateSystem: 'cartesian2d',\n    data: shotArrows,\n    symbol: ['none','arrow'], symbolSize: 7,\n    effect: { show: false },\n    lineStyle: { color: t.palette[1], width: 1.6, type: 'dashed' },\n    z: 4, silent: true, legendHoverLink: false,\n  },\n  // Pass markers — circles, palette[0] = #009E73 (first series)\n  {\n    type: 'scatter', name: 'Pass',\n    data: passes.map(p => ({ value: [p.x1,p.y1], itemStyle: { opacity: p.ok ? 0.88 : 0.38 } })),\n    symbol: 'circle', symbolSize: 10,\n    itemStyle: { color: t.palette[0], borderColor: 'rgba(255,255,255,0.5)', borderWidth: 1 },\n    z: 6,\n  },\n  // Shot markers — diamonds\n  {\n    type: 'scatter', name: 'Shot',\n    data: shots.map(s => ({ value: [s.x,s.y], itemStyle: { opacity: s.ok ? 1.0 : 0.4 } })),\n    symbol: 'diamond', symbolSize: 16,\n    itemStyle: { color: t.palette[1], borderColor: 'rgba(255,255,255,0.6)', borderWidth: 1.5 },\n    z: 7,\n  },\n  // Tackle markers — triangles\n  {\n    type: 'scatter', name: 'Tackle',\n    data: tackles.map(s => ({ value: [s.x,s.y], itemStyle: { opacity: s.ok ? 0.88 : 0.38 } })),\n    symbol: 'triangle', symbolSize: 14,\n    itemStyle: { color: t.palette[2], borderColor: 'rgba(255,255,255,0.5)', borderWidth: 1 },\n    z: 6,\n  },\n  // Interception markers — squares\n  {\n    type: 'scatter', name: 'Interception',\n    data: intercepts.map(s => ({ value: [s.x,s.y], itemStyle: { opacity: 0.88 } })),\n    symbol: 'rect', symbolSize: 12,\n    itemStyle: { color: t.palette[3], borderColor: 'rgba(255,255,255,0.5)', borderWidth: 1 },\n    z: 6,\n  },\n];\n\n// Pitch surface background via markArea on empty scatter\nconst bgSeries = {\n  type: 'scatter',\n  data: [],\n  z: 0, silent: true, legendHoverLink: false,\n  markArea: {\n    silent: true, z: 0,\n    itemStyle: { color: PITCH_GREEN, opacity: 1, borderWidth: 0 },\n    emphasis: { disabled: true },\n    data: [[{ coord: [0,0] }, { coord: [PW,PH] }]],\n  },\n};\n\n// Aspect ratio: X range (-4 to 109) = 113 units, Y range (-1.5 to 69.5) = 71 units\n// CSS mount 1600×900; grid top/bottom = 65 each → grid H = 770 px\n// Grid W = 770 × (113/71) = 1226 → left/right margin = (1600-1226)/2 = 187\nconst chart = echarts.init(document.getElementById('container'));\n\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: t.pageBg,\n\n  title: {\n    text: 'scatter-pitch-events · javascript · echarts · anyplot.ai',\n    subtext: 'Opacity = outcome (bright: successful · faded: unsuccessful)  ·  Arrows show event direction',\n    left: 'center',\n    top: 14,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 'bold' },\n    subtextStyle: { color: t.inkSoft, fontSize: 13 },\n  },\n\n  legend: {\n    data: ['Pass', 'Shot', 'Tackle', 'Interception'],\n    bottom: 20,\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    itemWidth: 18, itemHeight: 14,\n  },\n\n  tooltip: {\n    trigger: 'item',\n    formatter: p => {\n      if (!['Pass','Shot','Tackle','Interception'].includes(p.seriesName)) return '';\n      return `${p.seriesName}<br/>x: ${p.value[0].toFixed(1)}m, y: ${p.value[1].toFixed(1)}m`;\n    },\n  },\n\n  grid: { left: 187, right: 187, top: 65, bottom: 65 },\n\n  xAxis: {\n    type: 'value', min: -4, max: 109,\n    show: false, splitLine: { show: false },\n  },\n  yAxis: {\n    type: 'value', min: -1.5, max: 69.5,\n    show: false, splitLine: { show: false },\n  },\n\n  series: [bgSeries, ...pitchSeries, ...eventSeries],\n});\n"}