@johnmorrisdotca/kyogijo-stadiums 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +293 -0
  4. package/README.md +600 -2
  5. package/dist/annotate.d.ts +47 -0
  6. package/dist/annotate.js +349 -0
  7. package/dist/atlas-entry.d.ts +4 -0
  8. package/dist/atlas-entry.js +3 -0
  9. package/dist/atlas.d.ts +131 -0
  10. package/dist/atlas.js +149 -0
  11. package/dist/background.d.ts +56 -0
  12. package/dist/background.js +127 -0
  13. package/dist/ballpark.d.ts +48 -0
  14. package/dist/ballpark.js +129 -0
  15. package/dist/canvas-entry.d.ts +2 -0
  16. package/dist/canvas-entry.js +2 -0
  17. package/dist/canvas.d.ts +85 -0
  18. package/dist/canvas.js +279 -0
  19. package/dist/charts-entry.d.ts +10 -0
  20. package/dist/charts-entry.js +8 -0
  21. package/dist/charts.baseball.d.ts +30 -0
  22. package/dist/charts.baseball.js +152 -0
  23. package/dist/charts.curling.d.ts +52 -0
  24. package/dist/charts.curling.js +196 -0
  25. package/dist/charts.d.ts +172 -0
  26. package/dist/charts.js +508 -0
  27. package/dist/charts.network.d.ts +66 -0
  28. package/dist/charts.network.js +64 -0
  29. package/dist/charts.soccer.d.ts +43 -0
  30. package/dist/charts.soccer.js +191 -0
  31. package/dist/charts.types.d.ts +396 -0
  32. package/dist/charts.types.js +1 -0
  33. package/dist/colour.d.ts +29 -0
  34. package/dist/colour.js +97 -0
  35. package/dist/common.d.ts +14 -0
  36. package/dist/common.js +32 -0
  37. package/dist/court.d.ts +89 -0
  38. package/dist/court.js +248 -0
  39. package/dist/courts.style.d.ts +20 -0
  40. package/dist/courts.style.js +54 -0
  41. package/dist/cricket.board.d.ts +18 -0
  42. package/dist/cricket.board.js +67 -0
  43. package/dist/curling.board.d.ts +24 -0
  44. package/dist/curling.board.js +147 -0
  45. package/dist/d3-entry.d.ts +3 -0
  46. package/dist/d3-entry.js +2 -0
  47. package/dist/d3.d.ts +45 -0
  48. package/dist/d3.js +56 -0
  49. package/dist/draw.d.ts +52 -0
  50. package/dist/draw.js +705 -0
  51. package/dist/element-define.d.ts +2 -0
  52. package/dist/element-define.js +4 -0
  53. package/dist/element-entry.d.ts +1 -0
  54. package/dist/element-entry.js +2 -0
  55. package/dist/element.d.ts +39 -0
  56. package/dist/element.js +321 -0
  57. package/dist/eras-entry.d.ts +2 -0
  58. package/dist/eras-entry.js +2 -0
  59. package/dist/eras.d.ts +204 -0
  60. package/dist/eras.js +396 -0
  61. package/dist/events-entry.d.ts +3 -0
  62. package/dist/events-entry.js +3 -0
  63. package/dist/events.d.ts +54 -0
  64. package/dist/events.data.d.ts +59 -0
  65. package/dist/events.data.js +297 -0
  66. package/dist/events.js +127 -0
  67. package/dist/features.d.ts +31 -0
  68. package/dist/features.js +284 -0
  69. package/dist/field.score.d.ts +5 -0
  70. package/dist/field.score.js +33 -0
  71. package/dist/fields/afl.d.ts +11 -0
  72. package/dist/fields/afl.js +103 -0
  73. package/dist/fields/cricket.d.ts +9 -0
  74. package/dist/fields/cricket.js +81 -0
  75. package/dist/fields/fieldhockey.d.ts +6 -0
  76. package/dist/fields/fieldhockey.js +81 -0
  77. package/dist/fields/gaelic.d.ts +10 -0
  78. package/dist/fields/gaelic.js +86 -0
  79. package/dist/fields/handball.d.ts +6 -0
  80. package/dist/fields/handball.js +85 -0
  81. package/dist/fields/lacrosse.d.ts +17 -0
  82. package/dist/fields/lacrosse.js +169 -0
  83. package/dist/fields/rugby.d.ts +10 -0
  84. package/dist/fields/rugby.js +130 -0
  85. package/dist/fields.d.ts +12 -0
  86. package/dist/fields.js +44 -0
  87. package/dist/fields.kit.d.ts +63 -0
  88. package/dist/fields.kit.js +108 -0
  89. package/dist/fields.style.d.ts +4 -0
  90. package/dist/fields.style.js +19 -0
  91. package/dist/fields.types.d.ts +155 -0
  92. package/dist/fields.types.js +1 -0
  93. package/dist/format.d.ts +10 -0
  94. package/dist/format.js +28 -0
  95. package/dist/formations.d.ts +18 -0
  96. package/dist/formations.js +211 -0
  97. package/dist/frames-entry.d.ts +8 -0
  98. package/dist/frames-entry.js +7 -0
  99. package/dist/frames.d.ts +199 -0
  100. package/dist/frames.js +271 -0
  101. package/dist/frames.providers.d.ts +16 -0
  102. package/dist/frames.providers.js +302 -0
  103. package/dist/geojson-entry.d.ts +2 -0
  104. package/dist/geojson-entry.js +2 -0
  105. package/dist/geojson.d.ts +79 -0
  106. package/dist/geojson.js +147 -0
  107. package/dist/geometry-entry.d.ts +7 -0
  108. package/dist/geometry-entry.js +5 -0
  109. package/dist/geometry.d.ts +295 -0
  110. package/dist/geometry.js +361 -0
  111. package/dist/grounds-entry.d.ts +4 -0
  112. package/dist/grounds-entry.js +3 -0
  113. package/dist/grounds.d.ts +134 -0
  114. package/dist/grounds.data.d.ts +2 -0
  115. package/dist/grounds.data.js +1849 -0
  116. package/dist/grounds.js +74 -0
  117. package/dist/heat.colour.d.ts +37 -0
  118. package/dist/heat.colour.js +54 -0
  119. package/dist/heat.d.ts +43 -0
  120. package/dist/heat.js +614 -0
  121. package/dist/heat.stats.d.ts +123 -0
  122. package/dist/heat.stats.js +413 -0
  123. package/dist/heat.types.d.ts +145 -0
  124. package/dist/heat.types.js +1 -0
  125. package/dist/heat.zones.d.ts +54 -0
  126. package/dist/heat.zones.js +424 -0
  127. package/dist/homeside.d.ts +16 -0
  128. package/dist/homeside.js +43 -0
  129. package/dist/index.d.ts +47 -0
  130. package/dist/index.js +29 -0
  131. package/dist/layers-entry.d.ts +19 -0
  132. package/dist/layers-entry.js +11 -0
  133. package/dist/layers.d.ts +41 -0
  134. package/dist/layers.env.d.ts +72 -0
  135. package/dist/layers.env.js +79 -0
  136. package/dist/layers.js +533 -0
  137. package/dist/layers.marks.d.ts +45 -0
  138. package/dist/layers.marks.js +168 -0
  139. package/dist/layers.types.d.ts +226 -0
  140. package/dist/layers.types.js +1 -0
  141. package/dist/layout.d.ts +97 -0
  142. package/dist/layout.js +381 -0
  143. package/dist/layout.style.d.ts +6 -0
  144. package/dist/layout.style.js +17 -0
  145. package/dist/layouts-entry.d.ts +5 -0
  146. package/dist/layouts-entry.js +4 -0
  147. package/dist/leagues/aussie.d.ts +57 -0
  148. package/dist/leagues/aussie.js +36 -0
  149. package/dist/leagues/baseball.d.ts +58 -0
  150. package/dist/leagues/baseball.js +171 -0
  151. package/dist/leagues/basketball.d.ts +67 -0
  152. package/dist/leagues/basketball.js +128 -0
  153. package/dist/leagues/courts.d.ts +212 -0
  154. package/dist/leagues/courts.js +114 -0
  155. package/dist/leagues/cricket.d.ts +45 -0
  156. package/dist/leagues/cricket.js +34 -0
  157. package/dist/leagues/curling.d.ts +64 -0
  158. package/dist/leagues/curling.js +53 -0
  159. package/dist/leagues/fieldhockey.d.ts +48 -0
  160. package/dist/leagues/fieldhockey.js +29 -0
  161. package/dist/leagues/fields.d.ts +10 -0
  162. package/dist/leagues/fields.js +6 -0
  163. package/dist/leagues/football.d.ts +8 -0
  164. package/dist/leagues/football.js +82 -0
  165. package/dist/leagues/gaelic.d.ts +50 -0
  166. package/dist/leagues/gaelic.js +29 -0
  167. package/dist/leagues/handball.d.ts +45 -0
  168. package/dist/leagues/handball.js +32 -0
  169. package/dist/leagues/hockey.d.ts +63 -0
  170. package/dist/leagues/hockey.js +166 -0
  171. package/dist/leagues/lacrosse.d.ts +103 -0
  172. package/dist/leagues/lacrosse.js +175 -0
  173. package/dist/leagues/rugby.d.ts +59 -0
  174. package/dist/leagues/rugby.js +72 -0
  175. package/dist/leagues/sources.d.ts +18 -0
  176. package/dist/leagues/sources.js +95 -0
  177. package/dist/leagues/types.d.ts +43 -0
  178. package/dist/leagues/types.js +5 -0
  179. package/dist/leagues-entry.d.ts +23 -0
  180. package/dist/leagues-entry.js +15 -0
  181. package/dist/leagues.d.ts +172 -0
  182. package/dist/leagues.js +462 -0
  183. package/dist/look.d.ts +50 -0
  184. package/dist/look.js +186 -0
  185. package/dist/mark.d.ts +35 -0
  186. package/dist/mark.js +59 -0
  187. package/dist/mount-entry.d.ts +2 -0
  188. package/dist/mount-entry.js +2 -0
  189. package/dist/mount.d.ts +63 -0
  190. package/dist/mount.js +167 -0
  191. package/dist/mow.d.ts +36 -0
  192. package/dist/mow.js +111 -0
  193. package/dist/names.d.ts +77 -0
  194. package/dist/names.js +97 -0
  195. package/dist/outline.d.ts +25 -0
  196. package/dist/outline.js +112 -0
  197. package/dist/parks-entry.d.ts +4 -0
  198. package/dist/parks-entry.js +3 -0
  199. package/dist/parks.d.ts +200 -0
  200. package/dist/parks.data.d.ts +2 -0
  201. package/dist/parks.data.js +4489 -0
  202. package/dist/parks.js +92 -0
  203. package/dist/plot-entry.d.ts +2 -0
  204. package/dist/plot-entry.js +2 -0
  205. package/dist/plot.d.ts +74 -0
  206. package/dist/plot.js +68 -0
  207. package/dist/png-entry.d.ts +2 -0
  208. package/dist/png-entry.js +2 -0
  209. package/dist/png.d.ts +26 -0
  210. package/dist/png.js +46 -0
  211. package/dist/ranges.d.ts +33 -0
  212. package/dist/ranges.js +178 -0
  213. package/dist/ranges.names.d.ts +31 -0
  214. package/dist/ranges.names.js +65 -0
  215. package/dist/react-entry.d.ts +2 -0
  216. package/dist/react-entry.js +2 -0
  217. package/dist/react.d.ts +53 -0
  218. package/dist/react.js +58 -0
  219. package/dist/replay-entry.d.ts +5 -0
  220. package/dist/replay-entry.js +4 -0
  221. package/dist/replay.control.d.ts +47 -0
  222. package/dist/replay.control.js +184 -0
  223. package/dist/replay.d.ts +170 -0
  224. package/dist/replay.dom.d.ts +21 -0
  225. package/dist/replay.dom.js +388 -0
  226. package/dist/replay.js +579 -0
  227. package/dist/replay.types.d.ts +152 -0
  228. package/dist/replay.types.js +1 -0
  229. package/dist/restyle.d.ts +21 -0
  230. package/dist/restyle.js +70 -0
  231. package/dist/road.d.ts +26 -0
  232. package/dist/road.draw.d.ts +6 -0
  233. package/dist/road.draw.js +269 -0
  234. package/dist/road.js +59 -0
  235. package/dist/road.model.d.ts +63 -0
  236. package/dist/road.model.js +215 -0
  237. package/dist/road.style.d.ts +7 -0
  238. package/dist/road.style.js +26 -0
  239. package/dist/road.types.d.ts +122 -0
  240. package/dist/road.types.js +1 -0
  241. package/dist/scene-entry.d.ts +7 -0
  242. package/dist/scene-entry.js +5 -0
  243. package/dist/scene.css.d.ts +44 -0
  244. package/dist/scene.css.js +296 -0
  245. package/dist/scene.d.ts +27 -0
  246. package/dist/scene.fit.d.ts +12 -0
  247. package/dist/scene.fit.js +18 -0
  248. package/dist/scene.flat.d.ts +38 -0
  249. package/dist/scene.flat.js +167 -0
  250. package/dist/scene.js +420 -0
  251. package/dist/scene.path.d.ts +40 -0
  252. package/dist/scene.path.js +357 -0
  253. package/dist/scene.types.d.ts +172 -0
  254. package/dist/scene.types.js +1 -0
  255. package/dist/scene.xml.d.ts +31 -0
  256. package/dist/scene.xml.js +81 -0
  257. package/dist/score.d.ts +70 -0
  258. package/dist/score.js +212 -0
  259. package/dist/sets.board.d.ts +22 -0
  260. package/dist/sets.board.js +89 -0
  261. package/dist/sheet.d.ts +18 -0
  262. package/dist/sheet.js +38 -0
  263. package/dist/special.d.ts +44 -0
  264. package/dist/special.js +204 -0
  265. package/dist/special.style.d.ts +8 -0
  266. package/dist/special.style.js +36 -0
  267. package/dist/strings.d.ts +581 -0
  268. package/dist/strings.js +411 -0
  269. package/dist/style.d.ts +35 -0
  270. package/dist/style.js +177 -0
  271. package/dist/surfaces/baseball.d.ts +33 -0
  272. package/dist/surfaces/baseball.js +208 -0
  273. package/dist/surfaces/baseball.walls.d.ts +16 -0
  274. package/dist/surfaces/baseball.walls.js +150 -0
  275. package/dist/surfaces/basketball.d.ts +8 -0
  276. package/dist/surfaces/basketball.js +199 -0
  277. package/dist/surfaces/curling.d.ts +23 -0
  278. package/dist/surfaces/curling.js +128 -0
  279. package/dist/surfaces/field.d.ts +12 -0
  280. package/dist/surfaces/field.js +163 -0
  281. package/dist/surfaces/football.d.ts +12 -0
  282. package/dist/surfaces/football.js +208 -0
  283. package/dist/surfaces/hockey.d.ts +10 -0
  284. package/dist/surfaces/hockey.js +166 -0
  285. package/dist/surfaces/net.d.ts +7 -0
  286. package/dist/surfaces/net.js +151 -0
  287. package/dist/surfaces/soccer-providers.d.ts +124 -0
  288. package/dist/surfaces/soccer-providers.js +334 -0
  289. package/dist/surfaces/soccer-spec.d.ts +138 -0
  290. package/dist/surfaces/soccer-spec.js +175 -0
  291. package/dist/surfaces/soccer.d.ts +25 -0
  292. package/dist/surfaces/soccer.js +270 -0
  293. package/dist/surfaces/surface.d.ts +180 -0
  294. package/dist/surfaces/surface.js +49 -0
  295. package/dist/team.common.d.ts +75 -0
  296. package/dist/team.common.js +97 -0
  297. package/dist/team.formation.d.ts +24 -0
  298. package/dist/team.formation.js +121 -0
  299. package/dist/team.geometry.d.ts +66 -0
  300. package/dist/team.geometry.js +304 -0
  301. package/dist/team.layers.d.ts +16 -0
  302. package/dist/team.layers.js +32 -0
  303. package/dist/team.motion.d.ts +9 -0
  304. package/dist/team.motion.js +325 -0
  305. package/dist/team.network.d.ts +43 -0
  306. package/dist/team.network.js +215 -0
  307. package/dist/team.shape.d.ts +12 -0
  308. package/dist/team.shape.js +217 -0
  309. package/dist/team.types.d.ts +189 -0
  310. package/dist/team.types.js +1 -0
  311. package/dist/text.d.ts +31 -0
  312. package/dist/text.js +88 -0
  313. package/dist/themes.d.ts +14 -0
  314. package/dist/themes.js +24 -0
  315. package/dist/types.d.ts +565 -0
  316. package/dist/types.js +1 -0
  317. package/dist/units.d.ts +45 -0
  318. package/dist/units.js +90 -0
  319. package/dist/version.d.ts +2 -0
  320. package/dist/version.js +2 -0
  321. package/package.json +234 -4
package/dist/charts.js ADDED
@@ -0,0 +1,508 @@
1
+ // Ready-made charts on the data layers: a shot chart for the NBA, a shot map for the NHL, a pass chart for the NFL (here), a spray chart for
2
+ // baseball (charts.baseball.ts) and a shot map for soccer with a goal-mouth view (charts.soccer.ts). Each is a function of plain data that
3
+ // returns the layers and the drawing options to put them on, so that one call gives a good chart and the layers can still be changed.
4
+ // Written for this package. The charts match the kinds that mplsoccer (MIT, shot maps and pass maps), hockey_rink (shot maps) and sporty (MIT)
5
+ // draw; the code of GeomMLBStadiums (GPL) and of sportypy, sportyR and hockey_rink (GPL) was not read.
6
+ import { drawKyogijo } from "./draw.js";
7
+ import { fromSurface, frameByName, toSurface } from "./frames.js";
8
+ import { zoneCounts } from "./heat.js";
9
+ import { isPortrait } from "./common.js";
10
+ import { envFor, isNum, place, unitOfLayer } from "./layers.env.js";
11
+ import { layerTable } from "./layers.js";
12
+ import { outlineOf } from "./outline.js";
13
+ import { KYOGIJO_STRINGS, fill } from "./strings.js";
14
+ import { textWidth } from "./text.js";
15
+ // ----- what the charts share
16
+ /** The language of a chart's words. */
17
+ export const languageOf = (o) => (o.language === "ja" ? "ja" : "en");
18
+ /** The words of a chart. */
19
+ export const wordsOf = (language) => KYOGIJO_STRINGS[language];
20
+ export { isNum };
21
+ /** An object without the properties that are `undefined`, so that a layer is plain data and the same input gives the same JSON. */
22
+ export function clean(value) {
23
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
24
+ }
25
+ /** The ground round each surface that a chart draws, in feet: room above the surface for the key. */
26
+ export const MARGIN = Object.freeze({ nba: 9, nhl: 10, nfl: 14, cfl: 14, ifaf: 14, mlb: 40, soccer: 16 });
27
+ /** A frame by name, or the frame given. */
28
+ export const frameOf = (from, fallback) => (typeof from === "object" && from !== null ? from : frameByName(from ?? fallback));
29
+ /** What a layer's `from` is: the frame given, or the name of the one asked for (a name stays a name in the layer's JSON). */
30
+ export const fromOf = (given, frame) => (typeof given === "object" && given !== null ? given : frame.name);
31
+ /** The drawing's options for a chart: the sport, the language, the title and the ground round the surface, then whatever the caller laid over them. */
32
+ export function drawingOptions(sport, options, title, extra = {}) {
33
+ return {
34
+ sport,
35
+ language: languageOf(options),
36
+ ...(options.id === undefined ? {} : { id: options.id }),
37
+ title,
38
+ margin: MARGIN[sport],
39
+ ...extra,
40
+ ...(options.surface ?? {}),
41
+ };
42
+ }
43
+ /**
44
+ * The key of a chart: one row of marks and their names in the margin above the surface (drawn over the ground round it), as two layers:
45
+ * the marks, in the look the chart's own marks have, and their words. `extra` is more words with no mark (the size of a mark, a total).
46
+ */
47
+ export function keyLayers(o, entries, looks, all, extra = [], at = {}) {
48
+ const words = all.charts;
49
+ const sep = all.layers.listSep;
50
+ const { box } = outlineOf(o);
51
+ const margin = o.margin ?? 8;
52
+ const base = Math.max(box.w, box.h) * 0.013;
53
+ // The row is centred on the part of the surface that is shown: all of it, or the half of a pitch.
54
+ const view = { x: at.x ?? box.x, w: at.w ?? box.w };
55
+ const items = [...entries.map((e) => ({ text: e.label, mark: true })), ...extra.map((text) => ({ text, mark: false }))];
56
+ const widthAt = (size) => items.reduce((sum, item) => sum + (item.mark ? size * 0.9 + size * 0.45 : 0) + textWidth(item.text) * size + size * 1.4, 0) - size * 1.4;
57
+ let size = Math.min(base * 1.25, margin * 0.5);
58
+ const room = view.w * 0.96;
59
+ if (widthAt(size) > room)
60
+ size *= room / widthAt(size);
61
+ const y = at.y ?? box.y - margin * 0.5;
62
+ let x = view.x + (view.w - widthAt(size)) / 2;
63
+ const marks = [];
64
+ const text = [];
65
+ for (const [i, item] of items.entries()) {
66
+ if (item.mark) {
67
+ marks.push(clean({ x: x + size * 0.45, y, outcome: entries[i].outcome }));
68
+ x += size * 0.9 + size * 0.45;
69
+ }
70
+ text.push({ x, y, text: item.text });
71
+ x += textWidth(item.text) * size + size * 1.4;
72
+ }
73
+ const hollowWords = Object.fromEntries(Object.entries(looks).map(([name, look]) => [name, clean({ hollow: look.hollow, shape: look.shape, scale: look.scale })]));
74
+ const labels = entries.map((e) => e.label).join(sep);
75
+ const keyMarks = {
76
+ kind: "points",
77
+ id: "key-marks",
78
+ name: words.key,
79
+ title: words.key,
80
+ description: labels,
81
+ units: "ft",
82
+ clip: "none",
83
+ data: marks,
84
+ style: { size: size * 0.9, outcomes: hollowWords },
85
+ };
86
+ const keyText = {
87
+ kind: "text",
88
+ id: "key-text",
89
+ name: words.key,
90
+ title: words.key,
91
+ description: extra.join(sep),
92
+ units: "ft",
93
+ clip: "none",
94
+ data: text,
95
+ style: { size, anchor: "start" },
96
+ };
97
+ return [keyMarks, keyText];
98
+ }
99
+ /** The layers of the key are named `key-marks` and `key-text`. */
100
+ export const isKey = (layer) => layer.id === "key-marks" || layer.id === "key-text";
101
+ /** The key of a chart as data, from its entries and looks. */
102
+ export function keyItems(entries, looks) {
103
+ return entries.map((e) => ({ outcome: e.outcome ?? "", label: e.label, shape: looks[e.outcome ?? ""]?.shape ?? "circle", hollow: looks[e.outcome ?? ""]?.hollow === true }));
104
+ }
105
+ /** Put a chart together: the options, the layers and the ways to draw them. */
106
+ export function assemble(options, layers, parts) {
107
+ const all = { ...options, layers };
108
+ const language = options.language === "ja" ? "ja" : "en";
109
+ const chart = {
110
+ options: all,
111
+ layers,
112
+ title: parts.title,
113
+ summary: parts.summary,
114
+ totals: parts.totals,
115
+ key: [],
116
+ draw(extra = {}) {
117
+ const merged = { ...all, ...extra };
118
+ // The key runs along one edge of the surface as the eye sees it wide: a tall drawing leaves it out.
119
+ const shown = extra.layers ?? (isPortrait(merged) ? layers.filter((layer) => !isKey(layer)) : layers);
120
+ return drawKyogijo({ ...merged, layers: shown });
121
+ },
122
+ table(tableOptions = {}) {
123
+ return layerTable(layers.filter((layer) => !isKey(layer) && (parts.tableLayers === undefined || parts.tableLayers.includes(layer.id ?? ""))), { ...tableOptions, language, surface: all });
124
+ },
125
+ };
126
+ return chart;
127
+ }
128
+ /** `shot`s counted by outcome, as the words of the key and in the key's order, for a layer's description: "Made 3, Missed 5." */
129
+ export function describeCounts(entries, counts, words) {
130
+ const list = entries
131
+ .filter((e) => (counts.get(e.outcome ?? "") ?? 0) > 0)
132
+ .map((e) => fill(words.layers.item, { name: e.label, n: counts.get(e.outcome ?? "") ?? 0 }))
133
+ .join(words.layers.listSep);
134
+ return list === "" ? "" : fill(words.layers.outcomes, { list });
135
+ }
136
+ /** Round to a number of decimal places. */
137
+ export const round = (value, places = 1) => Math.round(value * 10 ** places) / 10 ** places;
138
+ /** A percent, one decimal, or null with nothing to divide. */
139
+ export const percentOf = (part, whole) => (whole === 0 ? null : round((100 * part) / whole));
140
+ /** The marks counted into the zones of a layer, as zone totals. */
141
+ export function zoneTotals(o, layer) {
142
+ return zoneCounts(layer, envFor(o, false)).map((z) => ({ id: z.id, name: z.name, taken: z.n, made: z.hits, percent: percentOf(z.hits, z.n) }));
143
+ }
144
+ /** Count marks by outcome, skipping the ones with no place. */
145
+ export function countOutcomes(rows) {
146
+ const counts = new Map();
147
+ let placed = 0;
148
+ let skipped = 0;
149
+ for (const row of rows) {
150
+ if (isNum(row.x) && isNum(row.y)) {
151
+ placed += 1;
152
+ const key = row.outcome ?? "";
153
+ counts.set(key, (counts.get(key) ?? 0) + 1);
154
+ }
155
+ else
156
+ skipped += 1;
157
+ }
158
+ return { counts, placed, skipped };
159
+ }
160
+ /** True when a mark carries a place. */
161
+ export const hasPlace = (d) => isNum(d.x) && isNum(d.y);
162
+ /**
163
+ * Turn the shots at the left end of a surface a half turn so that every one is at the right end, in the numbers of the frame they are in.
164
+ * Rounded to hundredths of the frame's unit, so that a shot at the right end comes back as it was given.
165
+ */
166
+ export function foldToRight(rows, frame, o) {
167
+ const { box } = outlineOf(o);
168
+ const size = o.sport === "soccer" ? { length: box.w, width: box.h, unit: "ft" } : undefined;
169
+ return rows.map((row) => {
170
+ if (!isNum(row.x) || !isNum(row.y))
171
+ return row;
172
+ const [sx, sy] = toSurface([[row.x, row.y]], { from: frame, size })[0];
173
+ if (sx >= box.x + box.w / 2)
174
+ return row;
175
+ const [bx, by] = fromSurface([[box.x + box.w - (sx - box.x), box.y + box.h - (sy - box.y)]], { to: frame, size })[0];
176
+ return { ...row, x: round(bx, 2), y: round(by, 2) };
177
+ });
178
+ }
179
+ /** The marks' places on the surface, in feet, in the same frame and ends as a layer will draw them. */
180
+ export function placed(o, layer, rows) {
181
+ const env = envFor(o, false);
182
+ const unit = unitOfLayer(layer, env);
183
+ const at = place(layer, env, rows.filter(hasPlace).map((r) => [r.x, r.y]), unit);
184
+ let k = 0;
185
+ return rows.map((r) => (hasPlace(r) ? at[k++] : undefined));
186
+ }
187
+ // ----- basketball
188
+ const NBA_LOOKS = { made: {}, missed: { shape: "x", hollow: true } };
189
+ /**
190
+ * A shot chart for the NBA from stats.nba.com's shot data: each shot as a mark (filled when it went in, an open x when it missed), and the
191
+ * six shot zones coloured by their field-goal percentage, with the percentage and made over taken written on each, a legend and a table. Give
192
+ * `league` (the league's percentage) and the zones are coloured by how far they are above or below it, on a diverging ramp.
193
+ * @example
194
+ * ```ts
195
+ * import { nbaShotChart } from "@johnmorrisdotca/kyogijo-stadiums/charts";
196
+ * const chart = nbaShotChart(
197
+ * [
198
+ * { x: 0, y: 12, made: true },
199
+ * { x: -231, y: 18, made: false },
200
+ * { x: 120, y: 140, made: true },
201
+ * { x: -30, y: 70, made: true },
202
+ * ],
203
+ * { league: 46, minShots: 1 },
204
+ * );
205
+ * console.log(chart.totals.shots, chart.totals.made, chart.layers.map((layer) => layer.kind).join(" "));
206
+ * console.log(chart.draw().startsWith("<svg"), chart.table().includes("Restricted area"));
207
+ * // 4 3 zones points points text
208
+ * // true true
209
+ * ```
210
+ */
211
+ export function nbaShotChart(shots, options = {}) {
212
+ if (!Array.isArray(shots))
213
+ throw new TypeError("shots is a list of shots");
214
+ const language = languageOf(options);
215
+ const all = wordsOf(language);
216
+ const w = all.charts;
217
+ const frame = frameOf(options.from, "nba-stats");
218
+ const end = options.end === "left" ? "left" : "right";
219
+ // A shot chart puts every shot at the basket it was taken at; a frame fixed to the court says where each one is.
220
+ const shooter = frame.ends === "shooter";
221
+ const data = shots.map((s) => clean({ x: s.x, y: s.y, outcome: s.made === undefined ? s.outcome : s.made ? "made" : "missed", value: s.value, team: s.team, title: s.title }));
222
+ const { counts, placed: count, skipped } = countOutcomes(data);
223
+ const entries = [{ label: w.made, outcome: "made" }, { label: w.missed, outcome: "missed" }];
224
+ const o = drawingOptions("nba", options, w.titleNba);
225
+ const where = clean({ from: fromOf(options.from, frame), attacking: shooter && end === "left" ? "left" : undefined });
226
+ const layers = [];
227
+ const league = isNum(options.league) ? options.league : undefined;
228
+ const zonesLayer = options.zones === false
229
+ ? undefined
230
+ : clean({
231
+ kind: "zones",
232
+ id: "zones",
233
+ name: w.shots,
234
+ zones: "nba-shot",
235
+ ...where,
236
+ end: shooter || options.end !== undefined ? end : undefined,
237
+ data,
238
+ statistic: "rate",
239
+ statisticName: w.fgRate,
240
+ minCount: isNum(options.minShots) ? options.minShots : 3,
241
+ labels: options.labels === false ? undefined : { show: ["value", "ratio"] },
242
+ ramp: options.ramp,
243
+ center: league,
244
+ legend: options.legend === false ? undefined : { title: w.fgRate, position: end === "right" ? "bottom-left" : "bottom-right" },
245
+ });
246
+ if (zonesLayer !== undefined)
247
+ layers.push(zonesLayer);
248
+ if (options.marks !== false) {
249
+ layers.push(clean({
250
+ kind: "points",
251
+ id: "shots",
252
+ name: w.shots,
253
+ description: describeCounts(entries, counts, all),
254
+ ...where,
255
+ data,
256
+ style: { outcomes: NBA_LOOKS, scale: zonesLayer === undefined ? 0.85 : 0.6, opacity: 0.85 },
257
+ }));
258
+ }
259
+ if (options.key !== false)
260
+ layers.push(...keyLayers(o, entries, NBA_LOOKS, all));
261
+ const made = counts.get("made") ?? 0;
262
+ const zones = zonesLayer === undefined ? [] : zoneTotals(o, zonesLayer);
263
+ const totals = { shots: count, made, percent: percentOf(made, count), skipped, zones };
264
+ const summary = [fill(w.sumNba, { shots: count, made, pct: totals.percent ?? 0 }), league !== undefined && zonesLayer !== undefined ? fill(w.leagueNote, { league }) : ""].filter((s) => s !== "").join(all.said.gap);
265
+ const chart = assemble(o, layers, { title: w.titleNba, summary, totals });
266
+ chart.key = keyItems(entries, NBA_LOOKS);
267
+ return chart;
268
+ }
269
+ // ----- hockey
270
+ const NHL_LOOKS = {
271
+ goal: { shape: "star", scale: 1.4 },
272
+ saved: { hollow: true },
273
+ missed: { shape: "x", hollow: true },
274
+ blocked: { shape: "triangle", hollow: true },
275
+ shot: { hollow: true },
276
+ };
277
+ /** A shot's outcome word: its own, `goal` for a goal, else `shot`. */
278
+ const hockeyOutcome = (s) => (s.goal === true ? "goal" : typeof s.outcome === "string" && s.outcome !== "" ? s.outcome.toLowerCase() : "shot");
279
+ /** The key entries a set of outcomes calls for, in the order shots are usually read: goals first. */
280
+ function shotEntries(counts, w, order) {
281
+ const label = { goal: w.goal, saved: w.saved, missed: w.missedNet, blocked: w.blocked, shot: w.shot, post: w.post };
282
+ const present = [...counts.keys()].filter((k) => k !== "");
283
+ const known = order.filter((k) => present.includes(k));
284
+ const other = present.filter((k) => !order.includes(k));
285
+ return [...known, ...other].map((k) => ({ label: label[k] ?? k, outcome: k }));
286
+ }
287
+ /** Per-team totals of shots, goals and expected goals. */
288
+ export function teamTotals(rows) {
289
+ var _a;
290
+ const out = {};
291
+ for (const r of rows) {
292
+ if (!hasPlace(r) || typeof r.team !== "string" || r.team === "")
293
+ continue;
294
+ const t = (out[_a = r.team] ?? (out[_a] = { shots: 0, goals: 0, xg: null }));
295
+ t.shots += 1;
296
+ if (r.outcome === "goal")
297
+ t.goals += 1;
298
+ if (isNum(r.value))
299
+ t.xg = round((t.xg ?? 0) + r.value, 2);
300
+ }
301
+ return out;
302
+ }
303
+ /**
304
+ * A shot map for the NHL from play-by-play shots: each shot a mark (a goal a star, a save an open circle, a miss an open x, a block an open
305
+ * triangle) sized by its chance of scoring, with an optional count of the shots into the slot zones or hexagons, a legend, a key and a table.
306
+ * With `fold` the shots at the left end are turned a half turn, so that every shot is at the right end.
307
+ * @example
308
+ * ```ts
309
+ * import { nhlShotMap } from "@johnmorrisdotca/kyogijo-stadiums/charts";
310
+ * const chart = nhlShotMap(
311
+ * [
312
+ * { x: 75, y: 4, outcome: "goal", xg: 0.31 },
313
+ * { x: -60, y: -12, outcome: "saved", xg: 0.07 },
314
+ * { x: 40, y: 20, outcome: "missed", xg: 0.03 },
315
+ * ],
316
+ * { fold: true, zones: true },
317
+ * );
318
+ * console.log(chart.totals.shots, chart.totals.goals, chart.totals.xg);
319
+ * console.log(chart.layers.map((layer) => layer.id).join(" "));
320
+ * // 3 1 0.41
321
+ * // zones shots key-marks key-text
322
+ * ```
323
+ */
324
+ export function nhlShotMap(shots, options = {}) {
325
+ if (!Array.isArray(shots))
326
+ throw new TypeError("shots is a list of shots");
327
+ const language = languageOf(options);
328
+ const all = wordsOf(language);
329
+ const w = all.charts;
330
+ const frame = frameOf(options.from, "nhl-api");
331
+ const o = drawingOptions("nhl", options, w.titleNhl);
332
+ const folded = options.fold === true;
333
+ const given = shots.map((s) => clean({ x: s.x, y: s.y, outcome: hockeyOutcome(s), value: s.xg, team: s.team, title: s.title }));
334
+ const data = folded ? foldToRight(given, frame, o) : given;
335
+ const { counts, placed: count, skipped } = countOutcomes(data);
336
+ const hasXg = data.some((d) => isNum(d.value));
337
+ const sized = options.sizeBy === "none" ? false : options.sizeBy === "xg" || hasXg;
338
+ const entries = shotEntries(counts, w, ["goal", "saved", "missed", "blocked", "shot"]);
339
+ const attacking = folded ? undefined : options.attacking === "left" ? "left" : undefined;
340
+ const where = clean({ from: fromOf(options.from, frame), attacking });
341
+ const single = folded || options.attacking !== undefined;
342
+ const statistic = options.statistic ?? "shots";
343
+ const stat = statistic === "xg" ? { statistic: "sum", statisticName: w.xgStat } : statistic === "goals" ? { statistic: "rate", statisticName: w.goalRate } : { statistic: "count" };
344
+ const legend = options.legend === false ? undefined : { position: single ? "bottom-left" : "bottom", ...(statistic === "shots" ? {} : { title: stat.statisticName }) };
345
+ const layers = [];
346
+ let zonesLayer;
347
+ if (options.zones !== undefined && options.zones !== false) {
348
+ zonesLayer = clean({
349
+ kind: "zones",
350
+ id: "zones",
351
+ name: w.shots,
352
+ zones: (options.zones === true ? "nhl-slot" : options.zones),
353
+ ...where,
354
+ end: single ? "right" : undefined,
355
+ data,
356
+ ...stat,
357
+ labels: options.labels === false ? undefined : true,
358
+ ramp: options.ramp,
359
+ legend,
360
+ });
361
+ layers.push(zonesLayer);
362
+ }
363
+ else if (options.hexbin !== undefined && options.hexbin !== false) {
364
+ layers.push(clean({
365
+ kind: "hexbin",
366
+ id: "hexagons",
367
+ name: w.shots,
368
+ ...where,
369
+ data,
370
+ size: typeof options.hexbin === "object" ? options.hexbin.size : undefined,
371
+ ...stat,
372
+ labels: options.labels === true ? true : undefined,
373
+ ramp: options.ramp,
374
+ legend,
375
+ }));
376
+ }
377
+ const xgMax = isNum(options.xgMax) && options.xgMax > 0 ? options.xgMax : 0.5;
378
+ const style = { outcomes: NHL_LOOKS, ...(sized ? { size: { domain: [0, xgMax], range: [1.5, 5.2] } } : {}) };
379
+ const xg = data.some((d) => isNum(d.value)) ? round(data.reduce((sum, d) => sum + (hasPlace(d) && isNum(d.value) ? d.value : 0), 0), 2) : null;
380
+ if (options.marks !== false) {
381
+ layers.push(clean({ kind: "points", id: "shots", name: w.shots, description: [describeCounts(entries, counts, all), sized ? w.sizeNote : ""].filter(Boolean).join(all.said.gap), ...where, data, style }));
382
+ }
383
+ const extras = [...(sized ? [w.sizeXg] : []), ...(xg === null ? [] : [fill(w.xgTotal, { n: xg })])];
384
+ if (options.key !== false)
385
+ layers.push(...keyLayers(o, entries, NHL_LOOKS, all, extras));
386
+ const goals = counts.get("goal") ?? 0;
387
+ const totals = { shots: count, goals, xg, skipped, zones: zonesLayer === undefined ? [] : zoneTotals(o, zonesLayer), teams: teamTotals(data) };
388
+ const summary = xg === null ? fill(w.sumShots, { shots: count, goals }) : fill(w.sumShotsXg, { shots: count, goals, xg });
389
+ const chart = assemble(o, layers, { title: w.titleNhl, summary, totals });
390
+ chart.key = keyItems(entries, NHL_LOOKS);
391
+ return chart;
392
+ }
393
+ // ----- football
394
+ const NFL_LOOKS = {
395
+ complete: {},
396
+ incomplete: { hollow: true },
397
+ touchdown: { shape: "star", scale: 1.5 },
398
+ interception: { shape: "x", scale: 1.25 },
399
+ };
400
+ /**
401
+ * A pass chart for the NFL from tracking data: where each pass came down as a mark (a completion filled, an incompletion open, a touchdown a
402
+ * star, an interception an x), the line of scrimmage, and the pass grid (short and deep by left, middle and right) coloured by completion
403
+ * percentage with the air yards of each pass in its table. The offence goes to the right whichever way `direction` says it went in the data.
404
+ * @example
405
+ * ```ts
406
+ * import { nflPassChart } from "@johnmorrisdotca/kyogijo-stadiums/charts";
407
+ * const chart = nflPassChart(
408
+ * [
409
+ * { x: 72, y: 26, outcome: "complete" },
410
+ * { x: 88, y: 14, outcome: "incomplete" },
411
+ * { x: 104, y: 30, outcome: "touchdown" },
412
+ * ],
413
+ * { los: 65, minPasses: 1 },
414
+ * );
415
+ * console.log(chart.totals.passes, chart.totals.complete, chart.totals.airYards);
416
+ * // 3 2 23
417
+ * ```
418
+ */
419
+ export function nflPassChart(passes, options = {}) {
420
+ if (!Array.isArray(passes))
421
+ throw new TypeError("passes is a list of passes");
422
+ const language = languageOf(options);
423
+ const all = wordsOf(language);
424
+ const w = all.charts;
425
+ const frame = frameOf(options.from, "nfl-tracking");
426
+ const o = drawingOptions("nfl", options, w.titleNfl);
427
+ const left = options.direction === "left";
428
+ const attacking = left ? "left" : undefined;
429
+ const where = clean({ from: fromOf(options.from, frame), attacking });
430
+ const { box } = outlineOf(o);
431
+ const los = isNum(options.los) ? options.los : undefined;
432
+ // The line of scrimmage on the surface, in feet from the left end line as the offence goes right: the frame's own number, placed.
433
+ const losFeet = los === undefined ? box.w / 2 : placed(o, where, [{ x: los, y: 0 }])[0][0];
434
+ const inYards = (feet) => feet / 3;
435
+ const rows = passes.map((p) => {
436
+ const outcome = typeof p.outcome === "string" && p.outcome !== "" ? p.outcome.toLowerCase() : undefined;
437
+ return { p, outcome };
438
+ });
439
+ // Air yards: given, else the yards past the line of scrimmage in the way the offence goes.
440
+ const at = placed(o, where, rows.map(({ p }) => ({ x: p.x, y: p.y })));
441
+ const data = rows.map(({ p, outcome }, i) => {
442
+ const place_ = at[i];
443
+ const air = isNum(p.airYards) ? p.airYards : place_ === undefined ? undefined : round(inYards(place_[0] - losFeet), 1);
444
+ return clean({ x: p.x, y: p.y, outcome, value: air, team: p.team, title: p.title });
445
+ });
446
+ const { counts, placed: count, skipped } = countOutcomes(data);
447
+ const entries = [["complete", w.complete], ["incomplete", w.incomplete], ["touchdown", w.touchdown], ["interception", w.interception]].filter(([k]) => counts.has(k) || counts.size === 0 || ["complete", "incomplete"].includes(k)).map(([outcome, label]) => ({ label, outcome }));
448
+ const layers = [];
449
+ const success = ["complete", "touchdown"];
450
+ let zonesLayer;
451
+ if (options.zones !== false) {
452
+ zonesLayer = clean({
453
+ kind: "zones",
454
+ id: "zones",
455
+ name: w.passes,
456
+ zones: "football-pass",
457
+ ...where,
458
+ los,
459
+ data,
460
+ statistic: "rate",
461
+ statisticName: w.completionRate,
462
+ success,
463
+ minCount: isNum(options.minPasses) ? options.minPasses : 3,
464
+ labels: options.labels === false ? undefined : { show: ["value", "ratio"] },
465
+ ramp: options.ramp,
466
+ legend: options.legend === false ? undefined : { title: w.completionRate, position: "bottom-left" },
467
+ });
468
+ layers.push(zonesLayer);
469
+ }
470
+ if (options.line !== false) {
471
+ const width = box.h / 3;
472
+ const line = clean({
473
+ kind: "lines",
474
+ id: "los",
475
+ name: w.lineOfScrimmage,
476
+ units: "ft",
477
+ data: [{ x: losFeet, y: 0, x2: losFeet, y2: width * 3 }],
478
+ style: { width: 0.9, dash: "dashed", color: "var(--ky-layer-ink)" },
479
+ });
480
+ layers.push(line);
481
+ }
482
+ if (options.arrows === true) {
483
+ const arrows = rows.filter(({ p }) => isNum(p.fromX) && isNum(p.fromY) && isNum(p.x) && isNum(p.y)).map(({ p, outcome }) => clean({ x: p.fromX, y: p.fromY, x2: p.x, y2: p.y, outcome, team: p.team, title: p.title }));
484
+ layers.push(clean({ kind: "arrows", id: "arrows", name: w.passes, ...where, data: arrows, style: { width: 0.7, opacity: 0.7, curve: 0.08, outcomes: { touchdown: {}, interception: {}, complete: {}, incomplete: { hollow: true } } } }));
485
+ }
486
+ const values = data.map((d) => d.value).filter(isNum);
487
+ if (options.marks !== false) {
488
+ const description = [describeCounts(entries, counts, all), values.length === 0 ? "" : fill(w.airYards, { min: Math.min(...values), max: Math.max(...values) })].filter(Boolean).join(all.said.gap);
489
+ layers.push(clean({ kind: "points", id: "passes", name: w.passes, description, ...where, data, style: { outcomes: NFL_LOOKS } }));
490
+ }
491
+ if (options.key !== false)
492
+ layers.push(...keyLayers(o, entries, NFL_LOOKS, all));
493
+ const complete = (counts.get("complete") ?? 0) + (counts.get("touchdown") ?? 0);
494
+ const totals = {
495
+ passes: count,
496
+ complete,
497
+ touchdowns: counts.get("touchdown") ?? 0,
498
+ interceptions: counts.get("interception") ?? 0,
499
+ percent: percentOf(complete, count),
500
+ airYards: values.length === 0 ? null : round(values.reduce((a, b) => a + b, 0) / values.length, 1),
501
+ skipped,
502
+ zones: zonesLayer === undefined ? [] : zoneTotals(o, zonesLayer),
503
+ };
504
+ const summary = fill(w.sumNfl, { passes: count, complete, pct: totals.percent ?? 0, td: totals.touchdowns, int: totals.interceptions });
505
+ const chart = assemble(o, layers, { title: w.titleNfl, summary, totals });
506
+ chart.key = keyItems(entries, NFL_LOOKS);
507
+ return chart;
508
+ }
@@ -0,0 +1,66 @@
1
+ import type { Chart, ChartOptions, KeyItem } from "./charts.types.ts";
2
+ import type { Frame } from "./frames.ts";
3
+ import type { SoccerProviderId } from "./surfaces/soccer-providers.ts";
4
+ import type { PassDatum } from "./team.types.ts";
5
+ import type { SurfaceOptions } from "./types.ts";
6
+ /** A pass of a pass network: who made it and who received it, from where to where. */
7
+ export type PassNetworkPass = PassDatum;
8
+ /** What `passNetwork` takes. */
9
+ export interface PassNetworkOptions extends ChartOptions {
10
+ /** The provider the passes are in (`opta`, `statsbomb`, `wyscout`, ...): the pitch is drawn in its numbers. Or a `Frame` of your own. `international` (metres) by default. */
11
+ provider?: SoccerProviderId | Frame;
12
+ /** The end the passes attack, in the provider's numbers: `right` (the default) or `left`. */
13
+ attacking?: "left" | "right";
14
+ /** Leave out links of fewer passes than this. 3 by default, so that a network of a match is readable; 1 draws every link. */
15
+ minPasses?: number;
16
+ /** Draw a link for each direction, with a head. */
17
+ directed?: boolean;
18
+ /** What is written in a node: the shirt `number` (the default), the `name`, the `id` or `none`. */
19
+ labels?: "number" | "name" | "id" | "none";
20
+ /** The pitch: a preset or a size, as `pitch` of a drawing. */
21
+ pitch?: SurfaceOptions["pitch"];
22
+ /** The ground, as `ground` of a drawing. */
23
+ ground?: string;
24
+ }
25
+ /** The numbers behind a pass network. */
26
+ export interface PassNetworkTotals {
27
+ /** Passes counted, and rows left out for want of a passer or a receiver. */
28
+ passes: number;
29
+ skipped: number;
30
+ /** Players with a place, and links drawn (those of at least `minPasses` passes). */
31
+ players: number;
32
+ links: number;
33
+ /** The player with most touches, or null with none. */
34
+ busiest: {
35
+ id: string | number;
36
+ name: string;
37
+ touches: number;
38
+ } | null;
39
+ /** The link with most passes, or null with none. */
40
+ strongest: {
41
+ from: string;
42
+ to: string;
43
+ passes: number;
44
+ } | null;
45
+ }
46
+ /**
47
+ * A pass network for soccer: a node for each player at the average place of the passes he made and received, sized by touches, and a link for each pair of
48
+ * players, wider and darker the more passes there were between them. Links of fewer than `minPasses` passes (3) are left out. The numbers behind it are in `totals`,
49
+ * and `table()` lists the players and the links as text.
50
+ * @example
51
+ * ```ts
52
+ * import { passNetwork } from "@johnmorrisdotca/kyogijo-stadiums/charts";
53
+ * const passes = [
54
+ * { from: 4, to: 8, x: 30, y: 40, x2: 45, y2: 55, team: "home" },
55
+ * { from: 8, to: 4, x: 45, y: 55, x2: 32, y2: 42, team: "home" },
56
+ * { from: 4, to: 8, x: 31, y: 41, x2: 46, y2: 54, team: "home" },
57
+ * { from: 8, to: 9, x: 46, y: 54, x2: 70, y2: 50, team: "home" },
58
+ * ];
59
+ * const chart = passNetwork(passes, { provider: "opta", minPasses: 2 });
60
+ * console.log(chart.totals.passes, chart.totals.players, chart.totals.links, chart.totals.strongest);
61
+ * // 4 3 1 { from: '4', to: '8', passes: 3 }
62
+ * ```
63
+ */
64
+ export declare function passNetwork(passes: readonly PassNetworkPass[], options?: PassNetworkOptions): Chart<PassNetworkTotals> & {
65
+ key: KeyItem[];
66
+ };
@@ -0,0 +1,64 @@
1
+ // The pass network chart: who passed to whom on a pitch, each player a node at the average place of his passes, sized by touches, and each pair of players a link
2
+ // sized by their passes, with a key and a table. The idea is that of mplsoccer's pass-network example (MIT); no code of it, nor of sportypy or sportyR (GPL), is used.
3
+ import { envFor } from "./layers.env.js";
4
+ import { fill } from "./strings.js";
5
+ import { assemble, clean, drawingOptions, frameOf, fromOf, keyLayers, languageOf, wordsOf } from "./charts.js";
6
+ import { computeNetwork, nodeName } from "./team.network.js";
7
+ /**
8
+ * A pass network for soccer: a node for each player at the average place of the passes he made and received, sized by touches, and a link for each pair of
9
+ * players, wider and darker the more passes there were between them. Links of fewer than `minPasses` passes (3) are left out. The numbers behind it are in `totals`,
10
+ * and `table()` lists the players and the links as text.
11
+ * @example
12
+ * ```ts
13
+ * import { passNetwork } from "@johnmorrisdotca/kyogijo-stadiums/charts";
14
+ * const passes = [
15
+ * { from: 4, to: 8, x: 30, y: 40, x2: 45, y2: 55, team: "home" },
16
+ * { from: 8, to: 4, x: 45, y: 55, x2: 32, y2: 42, team: "home" },
17
+ * { from: 4, to: 8, x: 31, y: 41, x2: 46, y2: 54, team: "home" },
18
+ * { from: 8, to: 9, x: 46, y: 54, x2: 70, y2: 50, team: "home" },
19
+ * ];
20
+ * const chart = passNetwork(passes, { provider: "opta", minPasses: 2 });
21
+ * console.log(chart.totals.passes, chart.totals.players, chart.totals.links, chart.totals.strongest);
22
+ * // 4 3 1 { from: '4', to: '8', passes: 3 }
23
+ * ```
24
+ */
25
+ export function passNetwork(passes, options = {}) {
26
+ if (!Array.isArray(passes))
27
+ throw new TypeError("passes is a list of passes");
28
+ const language = languageOf(options);
29
+ const all = wordsOf(language);
30
+ const w = all.charts;
31
+ const provider = options.provider ?? "international";
32
+ const frame = frameOf(provider, "international");
33
+ const o = drawingOptions("soccer", options, w.titlePassNetwork, {
34
+ ...(typeof provider === "string" ? { pitchType: provider } : {}),
35
+ ...(options.pitch === undefined ? {} : { pitch: options.pitch }),
36
+ ...(options.ground === undefined ? {} : { ground: options.ground }),
37
+ });
38
+ const attacking = options.attacking === "left" ? "left" : undefined;
39
+ const layer = clean({
40
+ kind: "network",
41
+ id: "network",
42
+ name: w.passes,
43
+ ...clean({ from: fromOf(typeof provider === "object" ? provider : undefined, frame), attacking }),
44
+ data: passes,
45
+ minPasses: typeof options.minPasses === "number" ? options.minPasses : 3,
46
+ directed: options.directed,
47
+ labels: options.labels,
48
+ });
49
+ const layers = [layer];
50
+ if (options.key !== false)
51
+ layers.push(...keyLayers(o, [], {}, all, [w.sizeTouches, w.widthPasses]));
52
+ const net = computeNetwork(layer, envFor(o, false));
53
+ let busiest = null;
54
+ for (const n of net.nodes)
55
+ if (busiest === null || n.touches > busiest.touches)
56
+ busiest = { id: n.id, name: nodeName(n), touches: n.touches };
57
+ let strongest = null;
58
+ for (const l of net.links)
59
+ if (strongest === null || l.n > strongest.passes)
60
+ strongest = { from: nodeName(l.a), to: nodeName(l.b), passes: l.n };
61
+ const totals = { passes: net.passes, skipped: net.skipped, players: net.nodes.length, links: net.links.length, busiest, strongest };
62
+ const summary = fill(w.sumNetwork, { passes: totals.passes, players: totals.players, links: totals.links });
63
+ return assemble(o, layers, { title: w.titlePassNetwork, summary, totals });
64
+ }