@johnmorrisdotca/kyogijo-stadiums 0.4.0 → 0.6.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 (361) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/NOTICE.md +182 -2
  3. package/README.md +106 -19
  4. package/dist/annotate.js +1 -1
  5. package/dist/arenas-entry.d.ts +4 -0
  6. package/dist/arenas-entry.js +3 -0
  7. package/dist/arenas.d.ts +125 -0
  8. package/dist/arenas.data.d.ts +2 -0
  9. package/dist/arenas.data.js +3763 -0
  10. package/dist/arenas.js +72 -0
  11. package/dist/athletics/describe.d.ts +8 -0
  12. package/dist/athletics/describe.js +41 -0
  13. package/dist/athletics/events.d.ts +17 -0
  14. package/dist/athletics/events.js +48 -0
  15. package/dist/athletics/facilities.d.ts +80 -0
  16. package/dist/athletics/facilities.js +176 -0
  17. package/dist/athletics/infield.d.ts +79 -0
  18. package/dist/athletics/infield.js +166 -0
  19. package/dist/athletics/layout.d.ts +13 -0
  20. package/dist/athletics/layout.js +98 -0
  21. package/dist/athletics/oval.d.ts +57 -0
  22. package/dist/athletics/oval.js +116 -0
  23. package/dist/athletics/plan.d.ts +97 -0
  24. package/dist/athletics/plan.js +355 -0
  25. package/dist/athletics/races.d.ts +102 -0
  26. package/dist/athletics/races.js +355 -0
  27. package/dist/athletics/steeplechase.d.ts +72 -0
  28. package/dist/athletics/steeplechase.js +196 -0
  29. package/dist/athletics/stencil.d.ts +13 -0
  30. package/dist/athletics/stencil.js +85 -0
  31. package/dist/athletics/track.d.ts +54 -0
  32. package/dist/athletics/track.js +105 -0
  33. package/dist/athletics/types.d.ts +77 -0
  34. package/dist/athletics/types.js +1 -0
  35. package/dist/athletics-entry.d.ts +17 -0
  36. package/dist/athletics-entry.js +11 -0
  37. package/dist/athletics.style.d.ts +16 -0
  38. package/dist/athletics.style.js +51 -0
  39. package/dist/atlas-entry.d.ts +6 -2
  40. package/dist/atlas-entry.js +4 -2
  41. package/dist/atlas.d.ts +117 -23
  42. package/dist/atlas.js +197 -12
  43. package/dist/background.js +2 -1
  44. package/dist/budo/describe.d.ts +13 -0
  45. package/dist/budo/describe.js +120 -0
  46. package/dist/budo/dojo.api.d.ts +64 -0
  47. package/dist/budo/dojo.api.js +46 -0
  48. package/dist/budo/dojo.d.ts +72 -0
  49. package/dist/budo/dojo.js +300 -0
  50. package/dist/budo/judo.d.ts +61 -0
  51. package/dist/budo/judo.js +109 -0
  52. package/dist/budo/karate.d.ts +29 -0
  53. package/dist/budo/karate.js +80 -0
  54. package/dist/budo/kendo.d.ts +18 -0
  55. package/dist/budo/kendo.js +44 -0
  56. package/dist/budo/kimarite.d.ts +19 -0
  57. package/dist/budo/kimarite.js +33 -0
  58. package/dist/budo/kit.d.ts +16 -0
  59. package/dist/budo/kit.js +24 -0
  60. package/dist/budo/kyudo.d.ts +28 -0
  61. package/dist/budo/kyudo.js +66 -0
  62. package/dist/budo/looks.d.ts +28 -0
  63. package/dist/budo/looks.js +58 -0
  64. package/dist/budo/parts.d.ts +32 -0
  65. package/dist/budo/parts.js +10 -0
  66. package/dist/budo/plan.d.ts +64 -0
  67. package/dist/budo/plan.js +139 -0
  68. package/dist/budo/room.d.ts +133 -0
  69. package/dist/budo/room.js +111 -0
  70. package/dist/budo/sumo.d.ts +31 -0
  71. package/dist/budo/sumo.js +71 -0
  72. package/dist/budo/tatami.d.ts +55 -0
  73. package/dist/budo/tatami.js +156 -0
  74. package/dist/budo/types.d.ts +192 -0
  75. package/dist/budo/types.js +2 -0
  76. package/dist/budo-entry.d.ts +22 -0
  77. package/dist/budo-entry.js +13 -0
  78. package/dist/budo.board.d.ts +28 -0
  79. package/dist/budo.board.js +81 -0
  80. package/dist/budo.score.d.ts +18 -0
  81. package/dist/budo.score.js +52 -0
  82. package/dist/budo.style.d.ts +6 -0
  83. package/dist/budo.style.js +67 -0
  84. package/dist/charts-entry.d.ts +6 -1
  85. package/dist/charts-entry.js +3 -0
  86. package/dist/charts.athletics.d.ts +183 -0
  87. package/dist/charts.athletics.js +455 -0
  88. package/dist/charts.baseball.js +1 -1
  89. package/dist/charts.curling.heat.d.ts +24 -0
  90. package/dist/charts.curling.heat.js +116 -0
  91. package/dist/charts.curling.js +14 -3
  92. package/dist/charts.js +4 -4
  93. package/dist/charts.pool.d.ts +67 -0
  94. package/dist/charts.pool.js +76 -0
  95. package/dist/charts.skating.d.ts +204 -0
  96. package/dist/charts.skating.js +426 -0
  97. package/dist/charts.soccer.js +3 -3
  98. package/dist/charts.types.d.ts +63 -1
  99. package/dist/colour.d.ts +4 -4
  100. package/dist/colour.js +4 -4
  101. package/dist/court.d.ts +85 -2
  102. package/dist/court.js +102 -7
  103. package/dist/court.more.d.ts +46 -0
  104. package/dist/court.more.js +219 -0
  105. package/dist/courts.style.d.ts +14 -2
  106. package/dist/courts.style.js +51 -4
  107. package/dist/crowd.d.ts +164 -0
  108. package/dist/crowd.draw.d.ts +17 -0
  109. package/dist/crowd.draw.js +204 -0
  110. package/dist/crowd.js +480 -0
  111. package/dist/crowd.park.d.ts +19 -0
  112. package/dist/crowd.park.js +153 -0
  113. package/dist/curling.stats.d.ts +271 -0
  114. package/dist/curling.stats.js +301 -0
  115. package/dist/draw.d.ts +16 -0
  116. package/dist/draw.js +462 -54
  117. package/dist/element-entry.d.ts +3 -1
  118. package/dist/element-entry.js +2 -1
  119. package/dist/element.d.ts +13 -2
  120. package/dist/element.js +79 -4
  121. package/dist/emblems-source.d.ts +50 -0
  122. package/dist/emblems-source.js +56 -0
  123. package/dist/eras.d.ts +3 -1
  124. package/dist/eras.js +8 -3
  125. package/dist/features.js +204 -0
  126. package/dist/field.data.d.ts +16 -0
  127. package/dist/field.data.js +50 -0
  128. package/dist/fields.kit.d.ts +2 -2
  129. package/dist/fields.types.d.ts +24 -4
  130. package/dist/formations.js +18 -7
  131. package/dist/formations.sports.d.ts +8 -0
  132. package/dist/formations.sports.js +251 -0
  133. package/dist/frames.providers.js +71 -8
  134. package/dist/geojson.js +3 -0
  135. package/dist/geometry-entry.d.ts +2 -1
  136. package/dist/geometry-entry.js +2 -1
  137. package/dist/geometry.d.ts +20 -0
  138. package/dist/geometry.js +78 -1
  139. package/dist/grounds.d.ts +4 -0
  140. package/dist/grounds.data.js +12 -0
  141. package/dist/halls.d.ts +65 -0
  142. package/dist/halls.js +122 -0
  143. package/dist/hardwood.d.ts +31 -0
  144. package/dist/hardwood.js +157 -0
  145. package/dist/heat.js +21 -13
  146. package/dist/heat.types.d.ts +5 -0
  147. package/dist/heat.zones.d.ts +4 -2
  148. package/dist/heat.zones.js +207 -7
  149. package/dist/homeside.js +24 -0
  150. package/dist/index.d.ts +30 -4
  151. package/dist/index.js +18 -2
  152. package/dist/layers-entry.d.ts +2 -1
  153. package/dist/layers-entry.js +1 -0
  154. package/dist/layers.d.ts +2 -1
  155. package/dist/layers.env.d.ts +12 -1
  156. package/dist/layers.env.js +36 -4
  157. package/dist/layers.js +32 -13
  158. package/dist/layers.types.d.ts +9 -2
  159. package/dist/leagues/athletics.d.ts +63 -0
  160. package/dist/leagues/athletics.js +87 -0
  161. package/dist/leagues/budo.d.ts +196 -0
  162. package/dist/leagues/budo.js +105 -0
  163. package/dist/leagues/courts.d.ts +269 -4
  164. package/dist/leagues/courts.js +155 -1
  165. package/dist/leagues/hockey.js +41 -19
  166. package/dist/leagues/skating.d.ts +145 -0
  167. package/dist/leagues/skating.js +73 -0
  168. package/dist/leagues/sources.js +41 -0
  169. package/dist/leagues/types.d.ts +1 -1
  170. package/dist/leagues-entry.d.ts +4 -2
  171. package/dist/leagues-entry.js +2 -1
  172. package/dist/leagues.d.ts +17 -4
  173. package/dist/leagues.js +246 -5
  174. package/dist/lineup.codes.d.ts +23 -0
  175. package/dist/lineup.codes.js +122 -0
  176. package/dist/logo.d.ts +25 -0
  177. package/dist/logo.js +47 -0
  178. package/dist/look.d.ts +3 -3
  179. package/dist/look.js +39 -6
  180. package/dist/outline.d.ts +1 -1
  181. package/dist/outline.js +46 -4
  182. package/dist/pageant.d.ts +120 -0
  183. package/dist/pageant.draw.d.ts +21 -0
  184. package/dist/pageant.draw.js +171 -0
  185. package/dist/pageant.js +211 -0
  186. package/dist/pageant.style.d.ts +8 -0
  187. package/dist/pageant.style.js +42 -0
  188. package/dist/png.d.ts +8 -1
  189. package/dist/png.js +21 -1
  190. package/dist/pointer-entry.d.ts +2 -0
  191. package/dist/pointer-entry.js +2 -0
  192. package/dist/pointer.d.ts +113 -0
  193. package/dist/pointer.js +151 -0
  194. package/dist/pool.d.ts +176 -0
  195. package/dist/pool.figures.d.ts +306 -0
  196. package/dist/pool.figures.js +148 -0
  197. package/dist/pool.js +304 -0
  198. package/dist/pool.kit.d.ts +48 -0
  199. package/dist/pool.kit.js +141 -0
  200. package/dist/pool.labels.d.ts +20 -0
  201. package/dist/pool.labels.js +258 -0
  202. package/dist/pool.race.d.ts +65 -0
  203. package/dist/pool.race.js +140 -0
  204. package/dist/pool.replay.d.ts +46 -0
  205. package/dist/pool.replay.js +127 -0
  206. package/dist/pool.section.d.ts +13 -0
  207. package/dist/pool.section.js +83 -0
  208. package/dist/pool.style.d.ts +20 -0
  209. package/dist/pool.style.js +169 -0
  210. package/dist/pool.types.d.ts +145 -0
  211. package/dist/pool.types.js +3 -0
  212. package/dist/pool.words.d.ts +13 -0
  213. package/dist/pool.words.js +101 -0
  214. package/dist/ranges.names.js +53 -3
  215. package/dist/replay-entry.d.ts +3 -0
  216. package/dist/replay-entry.js +2 -0
  217. package/dist/restyle.js +1 -1
  218. package/dist/road.draw.js +3 -2
  219. package/dist/road.js +2 -1
  220. package/dist/road.types.d.ts +3 -0
  221. package/dist/rules.d.ts +138 -0
  222. package/dist/rules.js +511 -0
  223. package/dist/rules.types.d.ts +4 -0
  224. package/dist/rules.types.js +64 -0
  225. package/dist/score.d.ts +3 -0
  226. package/dist/score.js +93 -6
  227. package/dist/sheet.d.ts +3 -0
  228. package/dist/sheet.js +7 -3
  229. package/dist/situation.d.ts +135 -0
  230. package/dist/situation.draw.d.ts +34 -0
  231. package/dist/situation.draw.js +202 -0
  232. package/dist/situation.js +337 -0
  233. package/dist/situation.style.d.ts +2 -0
  234. package/dist/situation.style.js +45 -0
  235. package/dist/situation.types.d.ts +77 -0
  236. package/dist/situation.types.js +3 -0
  237. package/dist/situation.widgets.d.ts +39 -0
  238. package/dist/situation.widgets.js +129 -0
  239. package/dist/skating/describe.d.ts +7 -0
  240. package/dist/skating/describe.js +44 -0
  241. package/dist/skating/events.d.ts +24 -0
  242. package/dist/skating/events.js +47 -0
  243. package/dist/skating/figure.d.ts +24 -0
  244. package/dist/skating/figure.js +37 -0
  245. package/dist/skating/long.d.ts +54 -0
  246. package/dist/skating/long.js +111 -0
  247. package/dist/skating/parts.d.ts +43 -0
  248. package/dist/skating/parts.js +40 -0
  249. package/dist/skating/plan.d.ts +55 -0
  250. package/dist/skating/plan.js +197 -0
  251. package/dist/skating/short.d.ts +47 -0
  252. package/dist/skating/short.js +89 -0
  253. package/dist/skating/track.d.ts +66 -0
  254. package/dist/skating/track.js +94 -0
  255. package/dist/skating/types.d.ts +57 -0
  256. package/dist/skating/types.js +3 -0
  257. package/dist/skating-entry.d.ts +17 -0
  258. package/dist/skating-entry.js +11 -0
  259. package/dist/skating.style.d.ts +4 -0
  260. package/dist/skating.style.js +24 -0
  261. package/dist/strings.d.ts +470 -0
  262. package/dist/strings.js +674 -13
  263. package/dist/style.d.ts +2 -0
  264. package/dist/style.js +19 -2
  265. package/dist/surfaces/artistic.d.ts +9 -0
  266. package/dist/surfaces/artistic.js +94 -0
  267. package/dist/surfaces/athletics.d.ts +16 -0
  268. package/dist/surfaces/athletics.js +132 -0
  269. package/dist/surfaces/baseball.js +17 -0
  270. package/dist/surfaces/basketball.floor.d.ts +27 -0
  271. package/dist/surfaces/basketball.floor.js +83 -0
  272. package/dist/surfaces/basketball.gear.d.ts +27 -0
  273. package/dist/surfaces/basketball.gear.js +90 -0
  274. package/dist/surfaces/basketball.js +44 -22
  275. package/dist/surfaces/budo.d.ts +6 -0
  276. package/dist/surfaces/budo.js +104 -0
  277. package/dist/surfaces/curling.d.ts +16 -0
  278. package/dist/surfaces/curling.fold.d.ts +67 -0
  279. package/dist/surfaces/curling.fold.js +107 -0
  280. package/dist/surfaces/curling.js +37 -3
  281. package/dist/surfaces/dive.d.ts +9 -0
  282. package/dist/surfaces/dive.js +128 -0
  283. package/dist/surfaces/field.d.ts +7 -2
  284. package/dist/surfaces/field.js +15 -9
  285. package/dist/surfaces/football.js +7 -0
  286. package/dist/surfaces/hockey.js +4 -2
  287. package/dist/surfaces/net.d.ts +2 -1
  288. package/dist/surfaces/net.js +160 -15
  289. package/dist/surfaces/polo.d.ts +12 -0
  290. package/dist/surfaces/polo.js +159 -0
  291. package/dist/surfaces/pool.d.ts +27 -0
  292. package/dist/surfaces/pool.js +198 -0
  293. package/dist/surfaces/skating.d.ts +6 -0
  294. package/dist/surfaces/skating.js +62 -0
  295. package/dist/surfaces/soccer.js +2 -1
  296. package/dist/surfaces/surface.d.ts +76 -0
  297. package/dist/surfaces/venue.d.ts +15 -0
  298. package/dist/surfaces/venue.js +95 -0
  299. package/dist/target-entry.d.ts +2 -0
  300. package/dist/target-entry.js +2 -0
  301. package/dist/target.d.ts +200 -0
  302. package/dist/target.js +525 -0
  303. package/dist/team.common.d.ts +1 -1
  304. package/dist/team.common.js +3 -3
  305. package/dist/team.formation.d.ts +3 -1
  306. package/dist/team.formation.js +3 -3
  307. package/dist/team.layers.d.ts +2 -2
  308. package/dist/team.layers.js +4 -1
  309. package/dist/team.lineup.d.ts +27 -0
  310. package/dist/team.lineup.js +207 -0
  311. package/dist/team.network.js +6 -5
  312. package/dist/team.shape.js +5 -5
  313. package/dist/team.types.d.ts +40 -1
  314. package/dist/tenants.d.ts +77 -0
  315. package/dist/tenants.data.d.ts +8 -0
  316. package/dist/tenants.data.js +1793 -0
  317. package/dist/tenants.js +46 -0
  318. package/dist/turn-entry.d.ts +12 -0
  319. package/dist/turn-entry.js +7 -0
  320. package/dist/turn.camera.d.ts +116 -0
  321. package/dist/turn.camera.js +184 -0
  322. package/dist/turn.dom.d.ts +40 -0
  323. package/dist/turn.dom.js +353 -0
  324. package/dist/turn.kit.d.ts +86 -0
  325. package/dist/turn.kit.js +110 -0
  326. package/dist/turn.markup.d.ts +48 -0
  327. package/dist/turn.markup.js +108 -0
  328. package/dist/turn.model.d.ts +78 -0
  329. package/dist/turn.model.js +91 -0
  330. package/dist/turn.style.d.ts +5 -0
  331. package/dist/turn.style.js +56 -0
  332. package/dist/types.d.ts +221 -20
  333. package/dist/units.d.ts +1 -1
  334. package/dist/units.js +22 -1
  335. package/dist/uprights.board.d.ts +17 -0
  336. package/dist/uprights.board.js +59 -0
  337. package/dist/uprights.budo.d.ts +17 -0
  338. package/dist/uprights.budo.js +87 -0
  339. package/dist/uprights.d.ts +37 -0
  340. package/dist/uprights.fields.d.ts +32 -0
  341. package/dist/uprights.fields.js +172 -0
  342. package/dist/uprights.goal.d.ts +49 -0
  343. package/dist/uprights.goal.js +69 -0
  344. package/dist/uprights.js +169 -0
  345. package/dist/uprights.more.d.ts +7 -0
  346. package/dist/uprights.more.js +183 -0
  347. package/dist/uprights.net.d.ts +12 -0
  348. package/dist/uprights.net.js +109 -0
  349. package/dist/uprights.netkit.d.ts +28 -0
  350. package/dist/uprights.netkit.js +67 -0
  351. package/dist/uprights.pools.d.ts +8 -0
  352. package/dist/uprights.pools.js +190 -0
  353. package/dist/uprights.rinks.d.ts +27 -0
  354. package/dist/uprights.rinks.js +106 -0
  355. package/dist/uprights.track.d.ts +50 -0
  356. package/dist/uprights.track.js +246 -0
  357. package/dist/version.d.ts +1 -1
  358. package/dist/version.js +1 -1
  359. package/dist/zones-entry.d.ts +2 -0
  360. package/dist/zones-entry.js +2 -0
  361. package/package.json +56 -3
@@ -0,0 +1,426 @@
1
+ // The skating data: the pairs of a speed skating race on the lines of its start (an inner and an outer lane, the armbands the Rules give them), the starting positions of a short track race, the results
2
+ // with their medals, times and splits, a replay of the race (every skater on his own way round the track, the lane changed once a lap), and the elements of a figure skating programme as marks on the ice.
3
+ // Written for this package from the ISU's Special Regulations and Technical Rules for speed skating (Rules 203, 227, 253, 254 and 280) and short track speed skating (Rule 280); nothing of any other package
4
+ // is used. The flags are the caller's: an SVG document as text, or an address, as a team's logo is.
5
+ import { assemble, clean, drawingOptions, languageOf, round, wordsOf } from "./charts.js";
6
+ import { esc } from "./format.js";
7
+ import { fill } from "./strings.js";
8
+ import { ELEMENT_KINDS, ELEMENT_NAMES, raceById } from "./skating/events.js";
9
+ import { ISU_SHORT_TRACK } from "./leagues/skating.js";
10
+ import { disciplineOf, skatingPlan } from "./skating/plan.js";
11
+ import { raceName } from "./skating/describe.js";
12
+ import { markRace } from "./skating/long.js";
13
+ import { shortPose, startingPositions, startsOnOppositeStraight } from "./skating/short.js";
14
+ import { finishAt, laneRadius, pose, startOf } from "./skating/track.js";
15
+ const isNum = (v) => typeof v === "number" && Number.isFinite(v);
16
+ const WORDS = (language) => wordsOf(language).skating;
17
+ /** A time in seconds as it is written: `34.32`, `1:40.91`, `6:01.03`. */
18
+ export function formatTime(seconds, places = 2) {
19
+ if (!isNum(seconds) || seconds < 0)
20
+ return "";
21
+ const r = Math.round(seconds * 10 ** places) / 10 ** places;
22
+ const h = Math.floor(r / 3600);
23
+ const m = Math.floor((r - h * 3600) / 60);
24
+ const s = r - h * 3600 - m * 60;
25
+ const sec = s.toFixed(places).padStart(places > 0 ? places + 3 : 2, "0");
26
+ return h > 0 ? `${h}:${String(m).padStart(2, "0")}:${sec}` : m > 0 ? `${m}:${sec}` : s.toFixed(places);
27
+ }
28
+ /** The discipline of the options' rink: a chart is for a track, the long one unless the league says the short one. */
29
+ function trackOf(options) {
30
+ const d = disciplineOf({ sport: "skating", league: options.surface?.league });
31
+ if (d === "figure")
32
+ throw new RangeError('A race chart is for a speed skating track: use league "isu-long-track" or "isu-short-track"');
33
+ return d;
34
+ }
35
+ function raceOf(options) {
36
+ const id = options.race ?? "500m";
37
+ if (raceById(id) === undefined)
38
+ throw new Error(`Unknown race "${String(id)}"`);
39
+ return id;
40
+ }
41
+ /** The plan of the race's drawing, close up on its start or its finish (or whole), with the race marked. */
42
+ function planOf(options, race, range, title, extraStyle = {}) {
43
+ const surface = options.surface ?? {};
44
+ const style = { races: [race], start: race, ...(surface.skating ?? {}), ...extraStyle };
45
+ const o = drawingOptions("skating", { ...options, surface: { ...(range === undefined ? {} : { range }), ...surface, skating: style } }, title, {});
46
+ const plan = skatingPlan({ sport: "skating", league: surface.league, skating: style, range: o.range });
47
+ return { o, plan };
48
+ }
49
+ const AT = { from: "skating", units: "m" };
50
+ const ARMBAND = { inner: "#ffffff", outer: "#d6342a" };
51
+ /** The point and the heading of a skater of a long track race who has skated `d` metres. */
52
+ function longAt(oval, race, lane, d) {
53
+ const m = markRace(oval, race);
54
+ const nc = m.noChange;
55
+ const dist = raceById(race)?.distance ?? 0;
56
+ const start = startOf(oval, lane, dist, finishAt(oval, m.finish), nc);
57
+ const p = pose(oval, lane, start.v0 + d, nc);
58
+ return { x: p.x, y: p.y, heading: p.heading };
59
+ }
60
+ /** Where a skater of a short track race is when `d` metres have been skated from the starting position `position` (1 on the inner side). */
61
+ function shortAt(oval, race, position, d) {
62
+ const laps = ISU_SHORT_TRACK.laps[race];
63
+ const total = laps * oval.lap;
64
+ // The finishing line is the middle of the bottom straight: S/2 along it. A half-lap race starts at the middle of the top one.
65
+ const finish = oval.straight / 2;
66
+ const v0 = (((finish - total) % oval.lap) + oval.lap) % oval.lap;
67
+ const offset = 0.5 + 1.3 * (position - 1) - 0.5;
68
+ const out = offset * Math.max(0, 1 - d / 25);
69
+ return shortPose(oval, v0 + d, out);
70
+ }
71
+ // ----- the pairs and the starting positions
72
+ /**
73
+ * The pairs of a long track race on the start's close-up: the pair `pair` (the first by default) with each skater's name and flag behind the starting line of his lane, and an armband's colour (white
74
+ * for the inner lane, red for the outer: Rule 253.3.1); a table lists every pair. The starting lines are the ISU's: the 500 m on the finishing straight, the 1000 m, 1500 m, 3000 m and 5000 m on the
75
+ * crossing straight, where each lane's line stands a different distance from the finish.
76
+ * @example
77
+ * ```ts
78
+ * import { skatingPairs } from "@johnmorrisdotca/kyogijo-stadiums/skating";
79
+ * const chart = skatingPairs([{ pair: 1, inner: { name: "A. Skater" }, outer: { name: "B. Skater" } }], { race: "1000m" });
80
+ * console.log(chart.totals, chart.title);
81
+ * // { skaters: 2, pairs: 1 } Pairs, 1000 m
82
+ * ```
83
+ */
84
+ export function skatingPairs(pairs, options = {}) {
85
+ if (!Array.isArray(pairs))
86
+ throw new TypeError("pairs is a list");
87
+ const track = trackOf(options);
88
+ if (track !== "long-track")
89
+ throw new RangeError("Pairs are for the long track: the short track has starting positions (skatingGrid)");
90
+ const seen = new Set();
91
+ for (const p of pairs) {
92
+ if (typeof p !== "object" || p === null || !Number.isInteger(p.pair) || p.pair < 1)
93
+ throw new TypeError("each pair has a number, a whole number from 1");
94
+ if (seen.has(p.pair))
95
+ throw new RangeError(`pair ${p.pair} is given twice`);
96
+ seen.add(p.pair);
97
+ }
98
+ const race = raceOf(options);
99
+ const language = languageOf(options);
100
+ const w = WORDS(language);
101
+ const title = fill(w.data.draw, { race: raceName(race, language) });
102
+ const { o, plan } = planOf(options, race, "start", title);
103
+ const oval = plan.oval;
104
+ const which = options.pair ?? [...pairs].sort((a, b) => a.pair - b.pair)[0]?.pair;
105
+ const shown = pairs.find((p) => p.pair === which);
106
+ const names = [];
107
+ const flags = [];
108
+ const bands = [];
109
+ const m = markRace(oval, race);
110
+ for (const [laneName, entry] of [["inner", shown?.inner], ["outer", shown?.outer]]) {
111
+ if (entry === undefined)
112
+ continue;
113
+ const lane = laneName === "inner" ? 0 : 1;
114
+ const start = m.starts.find((s) => s.lane === lane || s.lane === "both");
115
+ if (start === undefined)
116
+ continue;
117
+ const fx = Math.cos(start.heading);
118
+ const fy = Math.sin(start.heading);
119
+ // The skater stands in his lane, on the line of its measurement, behind the starting line.
120
+ const sign = start.y >= 0 ? 1 : -1;
121
+ const base = { x: start.x, y: sign * laneRadius(oval, lane) };
122
+ const back = (d) => [base.x - fx * d, base.y - fy * d];
123
+ const [bx, by] = back(1.6);
124
+ bands.push({ x: bx, y: by, shape: "circle", size: 1.2, color: ARMBAND[laneName], title: laneName === "inner" ? w.data.lanes.inner : w.data.lanes.outer });
125
+ if (entry.flag !== undefined) {
126
+ const [px, py] = back(3.6);
127
+ flags.push(clean({ x: px, y: py, shape: "logo", logo: entry.flag, size: 1.6, title: entry.country }));
128
+ }
129
+ const [nx, ny] = back(entry.flag !== undefined ? 5.4 : 3);
130
+ names.push(clean({ x: nx, y: ny, text: entry.name, anchor: fx < 0 ? "start" : "end", size: 1.9, title: entry.country === undefined ? undefined : `${entry.name}, ${entry.country}` }));
131
+ }
132
+ const layers = [];
133
+ layers.push({ kind: "points", id: "armbands", name: w.data.layers.names, ...AT, clip: "none", data: bands, style: { size: 1.2 } });
134
+ if (flags.length > 0)
135
+ layers.push({ kind: "points", id: "flags", name: w.data.layers.flags, ...AT, clip: "none", data: flags, style: { shape: "logo", size: 1.6 } });
136
+ layers.push({ kind: "text", id: "names", name: w.data.layers.names, ...AT, clip: "none", data: names, style: { size: 1.9, anchor: "end", weight: 800 } });
137
+ const skaters = pairs.reduce((n, p) => n + (p.inner === undefined ? 0 : 1) + (p.outer === undefined ? 0 : 1), 0);
138
+ const totals = { skaters, pairs: pairs.length };
139
+ const last = pairs.reduce((n, p) => Math.max(n, p.pair), 0);
140
+ const summary = pairs.length === 0 ? "" : fill(w.data.summaryDraw, { n: skaters, race: raceName(race, language), last });
141
+ const chart = assemble(o, layers, { title, summary, totals, tableLayers: ["names"] });
142
+ chart.table = () => pairsTable([...pairs].sort((a, b) => a.pair - b.pair), language);
143
+ return chart;
144
+ }
145
+ function pairsTable(pairs, language) {
146
+ const w = WORDS(language).data;
147
+ const c = w.columns;
148
+ const cell = (e) => (e === undefined ? "" : `${esc(e.name)}${e.country === undefined ? "" : ` (${esc(e.country)})`}`);
149
+ const rows = pairs.map((p) => `<tr><td>${p.pair}</td><td>${cell(p.inner)}</td><td>${cell(p.outer)}</td></tr>`).join("");
150
+ return `<table class="ky-table"><thead><tr><th>${c.pair}</th><th>${w.lanes.inner}</th><th>${w.lanes.outer}</th></tr></thead><tbody>${rows}</tbody></table>`;
151
+ }
152
+ /**
153
+ * The starting positions of a short track race on the start's close-up: the five dots of the 500 m and the 1000 m (every 1.3 m from 50 cm out from the inner side, Rule 280.2c), each with its starter's
154
+ * name and flag. The 500 m and the 1500 m start on the straight opposite the finishing line; the others at the finishing line.
155
+ * @example
156
+ * ```ts
157
+ * import { skatingGrid } from "@johnmorrisdotca/kyogijo-stadiums/skating";
158
+ * const chart = skatingGrid([{ position: 1, name: "A. Skater" }, { position: 2, name: "B. Skater" }], { race: "500m", surface: { league: "isu-short-track" } });
159
+ * console.log(chart.totals.skaters);
160
+ * // 2
161
+ * ```
162
+ */
163
+ export function skatingGrid(starters, options = {}) {
164
+ if (!Array.isArray(starters))
165
+ throw new TypeError("starters is a list");
166
+ const surface = { ...(options.surface ?? {}), league: options.surface?.league ?? "isu-short-track" };
167
+ const opts = { ...options, surface };
168
+ if (trackOf(opts) !== "short-track")
169
+ throw new RangeError("Starting positions are for the short track: the long track has pairs (skatingPairs)");
170
+ const seen = new Set();
171
+ for (const s of starters) {
172
+ if (typeof s !== "object" || s === null || !Number.isInteger(s.position) || s.position < 1 || s.position > 5)
173
+ throw new RangeError("each starter has a position from 1 to 5 (Rule 280.2c)");
174
+ if (seen.has(s.position))
175
+ throw new RangeError(`position ${s.position} is given twice`);
176
+ seen.add(s.position);
177
+ }
178
+ const race = raceOf(opts);
179
+ const language = languageOf(opts);
180
+ const w = WORDS(language);
181
+ const title = fill(w.data.draw, { race: raceName(race, language) });
182
+ const { o, plan } = planOf(opts, race, "start", title);
183
+ const oval = plan.short;
184
+ const top = startsOnOppositeStraight(race);
185
+ const spots = startingPositions(oval, 0, top ? -1 : 1);
186
+ const names = [];
187
+ const flags = [];
188
+ for (const s of [...starters].sort((a, b) => a.position - b.position)) {
189
+ const spot = spots[s.position - 1];
190
+ // The skaters go left along the top straight and right along the bottom one: the names stand behind the line.
191
+ const dir = top ? -1 : 1;
192
+ names.push(clean({ x: spot.x - dir * (s.flag === undefined ? 1.2 : 2.6), y: spot.y, text: s.name, anchor: top ? "start" : "end", size: 0.9, title: s.country === undefined ? undefined : `${s.name}, ${s.country}` }));
193
+ if (s.flag !== undefined)
194
+ flags.push(clean({ x: spot.x - dir * 1.4, y: spot.y, shape: "logo", logo: s.flag, size: 0.8, title: s.country }));
195
+ }
196
+ const layers = [];
197
+ if (flags.length > 0)
198
+ layers.push({ kind: "points", id: "flags", name: w.data.layers.flags, ...AT, clip: "none", data: flags, style: { shape: "logo", size: 0.8 } });
199
+ layers.push({ kind: "text", id: "names", name: w.data.layers.names, ...AT, clip: "none", data: names, style: { size: 0.9, anchor: top ? "start" : "end", weight: 800 } });
200
+ const totals = { skaters: starters.length, pairs: starters.length === 0 ? 0 : 1 };
201
+ const summary = starters.length === 0 ? "" : fill(w.data.summaryGrid, { n: starters.length, race: raceName(race, language) });
202
+ const chart = assemble(o, layers, { title, summary, totals, tableLayers: ["names"] });
203
+ chart.table = () => `<table class="ky-table"><thead><tr><th>${w.data.columns.order}</th><th>${w.data.columns.skater}</th><th>${w.data.columns.country}</th></tr></thead><tbody>${[...starters].sort((a, b) => a.position - b.position).map((s) => `<tr><td>${s.position}</td><td>${esc(s.name)}</td><td>${esc(s.country ?? "")}</td></tr>`).join("")}</tbody></table>`;
204
+ return chart;
205
+ }
206
+ // ----- the results
207
+ const MEDAL_COLOURS = { gold: "#d4a017", silver: "#b8bec6", bronze: "#b87333" };
208
+ /**
209
+ * The results of a race as a leaderboard on the ice inside the track: for each skater with a place, a disc with the place (gold, silver and bronze for the first three), the name and the time; the splits
210
+ * as small marks along the skater's lane on the long track; and a table with the pair, the lane, the time, the splits and the lap times. Times are in seconds from the gun.
211
+ * @example
212
+ * ```ts
213
+ * import { skatingResults } from "@johnmorrisdotca/kyogijo-stadiums/skating";
214
+ * const chart = skatingResults([{ name: "A", place: 1, time: 34.3 }, { name: "B", place: 2, time: 34.5 }], { race: "500m" });
215
+ * console.log(chart.totals.winner?.name, chart.summary);
216
+ * // A A won the 500 m in 34.30.
217
+ * ```
218
+ */
219
+ export function skatingResults(results, options = {}) {
220
+ if (!Array.isArray(results))
221
+ throw new TypeError("results is a list");
222
+ const track = trackOf(options);
223
+ const race = raceOf(options);
224
+ const language = languageOf(options);
225
+ const w = WORDS(language);
226
+ const title = fill(w.data.results, { race: raceName(race, language) });
227
+ const { o, plan } = planOf(options, race, undefined, title);
228
+ const sorted = [...results].sort((a, b) => (a.place ?? 999) - (b.place ?? 999));
229
+ const long = track === "long-track";
230
+ const unit = long ? 4.4 : 1.35;
231
+ const rows = Math.min(options.rows ?? (long ? 8 : 5), sorted.filter((r) => r.place !== undefined).length);
232
+ const discs = [];
233
+ const places = [];
234
+ const words = [];
235
+ const x0 = long ? -28 : -9;
236
+ const top = -((rows - 1) / 2) * unit * 1.25;
237
+ sorted.filter((r) => r.place !== undefined && isNum(r.time)).slice(0, rows).forEach((r, i) => {
238
+ const y = top + i * unit * 1.25;
239
+ const medal = r.place === 1 ? "gold" : r.place === 2 ? "silver" : r.place === 3 ? "bronze" : undefined;
240
+ discs.push(clean({ x: x0, y, shape: "circle", size: unit * 0.9, color: medal === undefined ? "#2f3a44" : MEDAL_COLOURS[medal], title: `${r.name}, ${medal === undefined ? "" : `${w.data.medals[medal]}, `}${formatTime(r.time)}` }));
241
+ places.push({ x: x0, y, text: r.place, size: unit * 0.55, color: medal === undefined ? "#ffffff" : "#1c1f1d" });
242
+ words.push({ x: x0 + unit * 0.9, y, text: `${r.name} ${formatTime(r.time)}`, anchor: "start", size: unit * 0.55 });
243
+ });
244
+ const marks = [];
245
+ if (long && options.splits !== false) {
246
+ const oval = plan.oval;
247
+ for (const r of results) {
248
+ if (r.lane === undefined || r.splits === undefined)
249
+ continue;
250
+ const lane = r.lane === "inner" ? 0 : 1;
251
+ for (const sp of r.splits) {
252
+ if (!isNum(sp.distance) || !isNum(sp.time))
253
+ continue;
254
+ const p = longAt(oval, race, lane, sp.distance);
255
+ marks.push({ x: round(p.x, 3), y: round(p.y, 3), shape: "circle", size: 1.1, title: fill(w.data.split, { d: round(sp.distance, 1), t: formatTime(sp.time) }), value: sp.time });
256
+ }
257
+ }
258
+ }
259
+ const layers = [];
260
+ if (marks.length > 0)
261
+ layers.push({ kind: "points", id: "splits", name: w.data.layers.splits, ...AT, clip: "none", data: marks, style: { size: 1.1, fill: "#f2c230" } });
262
+ layers.push({ kind: "points", id: "places", name: w.data.layers.medals, ...AT, clip: "none", data: discs, style: { size: unit * 0.9 } });
263
+ layers.push({ kind: "text", id: "place-numbers", name: w.data.layers.medals, ...AT, clip: "none", data: places, style: { size: unit * 0.55, weight: 800, halo: false } });
264
+ layers.push({ kind: "text", id: "skaters", name: w.data.layers.names, ...AT, clip: "none", data: words, style: { size: unit * 0.55, anchor: "start", weight: 800 } });
265
+ const winner = sorted.find((r) => r.place === 1 && isNum(r.time));
266
+ const totals = { skaters: results.length, finished: results.filter((r) => isNum(r.time) && r.place !== undefined).length, winner: winner === undefined ? null : { name: winner.name, time: winner.time } };
267
+ const summary = winner === undefined ? "" : fill(w.data.summaryResults, { winner: winner.name, race: raceName(race, language), time: formatTime(totals.winner?.time ?? 0) });
268
+ const chart = assemble(o, layers, { title, summary, totals, tableLayers: ["places"] });
269
+ chart.table = () => resultsTable(sorted, language);
270
+ return chart;
271
+ }
272
+ function resultsTable(results, language) {
273
+ const w = WORDS(language).data;
274
+ const c = w.columns;
275
+ const rows = results
276
+ .map((r) => {
277
+ const splits = (r.splits ?? []).map((s) => `${round(s.distance, 1)} ${w.metres}: ${formatTime(s.time)}`).join("; ");
278
+ const laps = (r.laps ?? []).map((t, i) => fill(w.lap, { n: i + 1, t: formatTime(t) })).join("; ");
279
+ return `<tr><td>${r.place ?? ""}</td><td>${r.pair ?? ""}</td><td>${r.lane === undefined ? "" : r.lane === "inner" ? w.lanes.inner : w.lanes.outer}</td><td>${esc(r.name)}</td><td>${esc(r.country ?? "")}</td><td>${isNum(r.time) ? formatTime(r.time) : esc(r.note ?? "")}</td><td>${esc(splits)}</td><td>${esc(laps)}</td></tr>`;
280
+ })
281
+ .join("");
282
+ return `<table class="ky-table"><thead><tr><th>${c.place}</th><th>${c.pair}</th><th>${c.lane}</th><th>${c.skater}</th><th>${c.country}</th><th>${c.time}</th><th>${c.splits}</th><th>${c.laps}</th></tr></thead><tbody>${rows}</tbody></table>`;
283
+ }
284
+ /** The distance a skater has skated at a time, by the splits and the finish: straight lines between them, coasting for three seconds after the finish. */
285
+ export function distanceSkated(runner, total, t) {
286
+ if (t <= 0)
287
+ return 0;
288
+ const pts = [[0, 0]];
289
+ for (const s of [...(runner.splits ?? [])].sort((a, b) => a.time - b.time))
290
+ if (s.time > 0 && s.time < runner.time && s.distance > 0 && s.distance < total)
291
+ pts.push([s.time, s.distance]);
292
+ pts.push([runner.time, total]);
293
+ for (let i = 1; i < pts.length; i += 1) {
294
+ if (t <= pts[i][0]) {
295
+ const [t0, d0] = pts[i - 1];
296
+ const [t1, d1] = pts[i];
297
+ return d0 + ((t - t0) / (t1 - t0)) * (d1 - d0);
298
+ }
299
+ }
300
+ const [t0, d0] = pts[pts.length - 2];
301
+ const [t1, d1] = pts[pts.length - 1];
302
+ const v = (d1 - d0) / (t1 - t0);
303
+ const u = Math.min(t - t1, 3);
304
+ return d1 + v * (u - (u * u) / 6);
305
+ }
306
+ /**
307
+ * A replay of a race: every skater, from the gun, on his own way round the track (the long track's lane changed once a lap, on the crossing straight, and the first straight of the 1000 m and the 1500 m
308
+ * kept in the lane; the short track's oval of 111.12 m, the starters coming in from their positions), at the pace of the splits and the finish time. The positions are in the skating frame (metres
309
+ * from the middle of the rink). The team races, the mass start and the relays are not replayed.
310
+ * @example
311
+ * ```ts
312
+ * import { skatingReplay } from "@johnmorrisdotca/kyogijo-stadiums/skating";
313
+ * const replay = skatingReplay([{ name: "A", lane: "inner", time: 34.4 }, { name: "B", lane: "outer", time: 34.6 }], { race: "500m" });
314
+ * console.log(replay.timeline.frames.length > 40, replay.duration);
315
+ * // true 34.6
316
+ * ```
317
+ */
318
+ export function skatingReplay(runners, options = {}) {
319
+ if (!Array.isArray(runners))
320
+ throw new TypeError("runners is a list");
321
+ trackOf(options);
322
+ const race = raceOf(options);
323
+ const info = raceById(race);
324
+ if (race === "mass-start" || race === "team-pursuit" || race === "team-sprint")
325
+ throw new RangeError(`The ${race} is not replayed: its skaters are not in lanes`);
326
+ const language = languageOf(options);
327
+ const w = WORDS(language);
328
+ const title = fill(w.data.replay, { race: raceName(race, language) });
329
+ const { o, plan } = planOf(options, race, undefined, title);
330
+ const step = isNum(options.step) && options.step > 0 ? options.step : 0.25;
331
+ for (const r of runners)
332
+ if (!isNum(r.time) || r.time <= 0)
333
+ throw new RangeError(`${String(r.name)}: the time is a number of seconds above zero`);
334
+ const total = info.distance;
335
+ const shortTotal = plan.short === undefined ? 0 : ISU_SHORT_TRACK.laps[race] * plan.short.lap;
336
+ const duration = Math.max(...runners.map((r) => r.time), 0);
337
+ const frames = [];
338
+ const at = (r, d) => {
339
+ if (plan.short !== undefined)
340
+ return shortAt(plan.short, race, r.position ?? 1, d);
341
+ return longAt(plan.oval, race, r.lane === "outer" ? 1 : 0, d);
342
+ };
343
+ const distance = plan.short === undefined ? total : shortTotal;
344
+ for (let k = 0; k * step <= duration + 3.2 + 1e-9; k += 1) {
345
+ const t = Math.round(k * step * 1000) / 1000;
346
+ const objects = [];
347
+ runners.forEach((r, i) => {
348
+ const p = at(r, distanceSkated(r, distance, t));
349
+ objects.push(clean({ id: `skater-${i + 1}`, team: r.lane === "outer" ? "away" : "home", x: round(p.x, 3), y: round(p.y, 3), label: r.name, number: plan.short === undefined ? (r.lane === "outer" ? 2 : 1) : (r.position ?? i + 1) }));
350
+ });
351
+ frames.push({ t, objects });
352
+ }
353
+ const events = [{ t: 0, kind: "gun", label: w.data.gun }];
354
+ runners.forEach((r, i) => {
355
+ const p = at(r, distance);
356
+ events.push({ t: r.time, kind: "finish", team: r.lane === "outer" ? "away" : "home", x: round(p.x, 3), y: round(p.y, 3), by: `skater-${i + 1}`, label: fill(w.data.finish, { name: r.name, time: formatTime(r.time) }) });
357
+ });
358
+ events.sort((a, b) => a.t - b.t);
359
+ const timeline = { frames, events, from: "skating", units: "m" };
360
+ const summary = fill(w.data.summaryReplay, { n: runners.length, race: raceName(race, language) });
361
+ return { timeline, options: { ...o, title }, look: { trail: 0, scale: plan.short === undefined ? 0.8 : 0.7, numbers: true, hold: 2 }, title, summary, duration };
362
+ }
363
+ const KIND_SHAPE = { jump: "circle", spin: "star", "step-sequence": "diamond", "choreographic-sequence": "diamond", lift: "triangle", spiral: "square", twizzle: "x", "death-spiral": "star", throw: "cross" };
364
+ /**
365
+ * The elements of a programme as marks on the rink: a mark for each element where it starts, its shape for its kind, the track of a step sequence or a spiral as a line to where it ends, and the order
366
+ * and the name beside it. Put them on the rink (`league: "isu-figure"`, the default for this chart).
367
+ * @example
368
+ * ```ts
369
+ * import { figureElements } from "@johnmorrisdotca/kyogijo-stadiums/skating";
370
+ * const chart = figureElements([{ kind: "jump", name: "3Lz", x: -12, y: 4, order: 1 }, { kind: "spin", name: "CCoSp4", x: 6, y: -3, order: 2 }]);
371
+ * console.log(chart.totals.elements, chart.totals.kinds.jump);
372
+ * // 2 1
373
+ * ```
374
+ */
375
+ export function figureElements(elements, options = {}) {
376
+ if (!Array.isArray(elements))
377
+ throw new TypeError("elements is a list");
378
+ for (const e of elements) {
379
+ if (typeof e !== "object" || e === null || !ELEMENT_KINDS.includes(e.kind))
380
+ throw new Error(`Unknown kind of element "${String(e?.kind)}": use one of ${ELEMENT_KINDS.join(", ")}`);
381
+ if (!isNum(e.x) || !isNum(e.y))
382
+ throw new TypeError("each element has a place: x and y in metres from the middle of the rink");
383
+ }
384
+ const language = languageOf(options);
385
+ const w = WORDS(language);
386
+ const surface = { ...(options.surface ?? {}), league: options.surface?.league ?? "isu-figure" };
387
+ if (disciplineOf({ sport: "skating", league: surface.league }) !== "figure")
388
+ throw new RangeError('figureElements is for the figure skating rink: use league "isu-figure"');
389
+ const title = w.data.elements;
390
+ const o = drawingOptions("skating", { ...options, surface }, title, {});
391
+ const plan = skatingPlan({ sport: "skating", league: surface.league, skating: surface.skating });
392
+ const half = (plan.rink?.length ?? 60) / 2;
393
+ const wide = (plan.rink?.width ?? 30) / 2;
394
+ const sorted = [...elements].sort((a, b) => (a.order ?? 1e9) - (b.order ?? 1e9));
395
+ const points = [];
396
+ const lines = [];
397
+ const labels = [];
398
+ const kinds = {};
399
+ sorted.forEach((e, i) => {
400
+ kinds[e.kind] = (kinds[e.kind] ?? 0) + 1;
401
+ const x = Math.max(-half, Math.min(half, e.x));
402
+ const y = Math.max(-wide, Math.min(wide, e.y));
403
+ const kindName = ELEMENT_NAMES[e.kind][language];
404
+ points.push(clean({ x, y, shape: KIND_SHAPE[e.kind], team: e.team ?? "home", outcome: e.kind, title: e.title ?? `${e.order === undefined ? i + 1 : e.order}. ${e.name ?? kindName}` }));
405
+ if (isNum(e.x2) && isNum(e.y2))
406
+ lines.push(clean({ x, y, x2: Math.max(-half, Math.min(half, e.x2)), y2: Math.max(-wide, Math.min(wide, e.y2)), team: e.team }));
407
+ if (options.labels !== false)
408
+ labels.push({ x: x + 1.1, y: y - 1.1, text: `${e.order === undefined ? i + 1 : e.order}${e.name === undefined ? "" : ` ${e.name}`}`, size: 1.15, anchor: "start" });
409
+ });
410
+ const layers = [];
411
+ if (lines.length > 0)
412
+ layers.push({ kind: "lines", id: "paths", name: w.data.layers.paths, ...AT, clip: "none", data: lines.map((l) => clean({ x: l.x, y: l.y, x2: l.x2, y2: l.y2, team: l.team ?? "home" })), style: { width: 0.35, dash: "dashed" } });
413
+ layers.push({ kind: "points", id: "elements", name: w.data.layers.elements, ...AT, clip: "none", data: points, style: { size: 1.5 } });
414
+ if (labels.length > 0)
415
+ layers.push({ kind: "text", id: "labels", name: w.data.layers.elements, ...AT, clip: "none", data: labels, style: { size: 1.15, weight: 800 } });
416
+ const totals = { elements: elements.length, kinds };
417
+ const who = sorted.map((e) => e.name ?? ELEMENT_NAMES[e.kind][language]).slice(0, 6).join(language === "ja" ? "、" : ", ");
418
+ const summary = elements.length === 0 ? "" : fill(w.data.summaryElements, { n: elements.length, who });
419
+ const chart = assemble(o, layers, { title, summary, totals, tableLayers: ["elements"] });
420
+ chart.table = () => {
421
+ const c = w.data.columns;
422
+ const rows = sorted.map((e, i) => `<tr><td>${e.order ?? i + 1}</td><td>${esc(e.name ?? "")}</td><td>${esc(ELEMENT_NAMES[e.kind][language])}</td><td>${round(e.x, 1)}, ${round(e.y, 1)}</td></tr>`).join("");
423
+ return `<table class="ky-table"><thead><tr><th>${c.order}</th><th>${c.element}</th><th>${c.kind}</th><th>x, y (m)</th></tr></thead><tbody>${rows}</tbody></table>`;
424
+ };
425
+ return chart;
426
+ }
@@ -65,7 +65,7 @@ export function soccerShotMap(shots, options = {}) {
65
65
  });
66
66
  const attacking = options.attacking === "left" ? "left" : undefined;
67
67
  const where = clean({ from: fromOf(typeof provider === "object" ? provider : undefined, frame), attacking });
68
- const data = shots.map((s) => clean({ x: s.x, y: s.y, outcome: outcomeOf(s), value: s.xg, team: s.team, title: s.title }));
68
+ const data = shots.map((s) => clean({ id: s.id, x: s.x, y: s.y, outcome: outcomeOf(s), value: s.xg, team: s.team, title: s.title }));
69
69
  const { counts, placed: count, skipped } = countOutcomes(data);
70
70
  const hasXg = data.some((d) => isNum(d.value));
71
71
  const sized = options.xg === false ? false : hasXg;
@@ -140,7 +140,7 @@ export function soccerGoalMouth(shots, options = {}) {
140
140
  };
141
141
  const id = options.id === undefined ? undefined : options.id;
142
142
  const surface = { sport: "soccer", pitch: { length: viewW, width: viewH, unit: "m" }, units: "m", language, ...(id === undefined ? {} : { id }), margin: 0 };
143
- const rows = shots.map((s) => clean({ x: isNum(s.goalY) ? round(s.goalY * k, 3) : null, y: isNum(s.goalZ) ? round(s.goalZ * k, 3) : null, outcome: outcomeOf(s), value: s.xg, team: s.team, title: s.title }));
143
+ const rows = shots.map((s) => clean({ id: s.id, x: isNum(s.goalY) ? round(s.goalY * k, 3) : null, y: isNum(s.goalZ) ? round(s.goalZ * k, 3) : null, outcome: outcomeOf(s), value: s.xg, team: s.team, title: s.title }));
144
144
  const { counts, placed: placedCount, skipped } = countOutcomes(rows);
145
145
  const hasXg = rows.some((r) => isNum(r.value));
146
146
  const sized = options.xg === false ? false : options.xg === true || hasXg;
@@ -150,7 +150,7 @@ export function soccerGoalMouth(shots, options = {}) {
150
150
  const style = { outcomes: LOOKS, opacity: 0.92, ...(sized ? { size: { domain: [0, xgMax], range: [0.25, 0.8] } } : { size: 0.45 }) };
151
151
  const layer = clean({ kind: "points", id: "goal-shots", name: w.shots, description: describeCounts(entries, counts, all), from: frame, units: "m", data: rows, style, clip: "none" });
152
152
  const layers = [layer];
153
- const rendered = renderLayers(surface, layers, false);
153
+ const rendered = renderLayers(surface, layers, false, undefined, undefined, () => undefined);
154
154
  const F = FEET_PER_METRE;
155
155
  const x0 = ((viewW - goalW) / 2) * F;
156
156
  const x1 = x0 + goalW * F;
@@ -2,7 +2,7 @@ import type { Frame } from "./frames.ts";
2
2
  import type { HeatRamp } from "./heat.types.ts";
3
3
  import type { Layer, LayerTeam, MarkShape, TableOptions } from "./layers.types.ts";
4
4
  import type { SoccerProviderId } from "./surfaces/soccer-providers.ts";
5
- import type { Language, SurfaceOptions } from "./types.ts";
5
+ import type { Language, SheetShow, SurfaceOptions } from "./types.ts";
6
6
  import type { Unit } from "./units.ts";
7
7
  /** What every chart gives back: the layers and the drawing options to put them on, a sentence, the totals, and the ways to draw them. */
8
8
  export interface Chart<Totals = Record<string, never>> {
@@ -48,6 +48,8 @@ export interface ChartOptions {
48
48
  }
49
49
  /** A shot as stats.nba.com's shot chart gives it: `LOC_X` and `LOC_Y` and whether it went in. */
50
50
  export interface NbaShot {
51
+ /** Your id for the shot, written on its mark as `data-mark-id`. */
52
+ id?: string | number;
51
53
  x: number | null;
52
54
  y: number | null;
53
55
  /** True for a shot that went in. Or give `outcome`. */
@@ -98,6 +100,8 @@ export interface NbaTotals {
98
100
  }
99
101
  /** A shot as the NHL's play-by-play gives it. */
100
102
  export interface NhlShot {
103
+ /** Your id for the shot, written on its mark as `data-mark-id`. */
104
+ id?: string | number;
101
105
  x: number | null;
102
106
  y: number | null;
103
107
  /** `goal`, `saved` (a shot on goal), `missed`, `blocked`, or `shot` for an attempt of no known result. */
@@ -152,6 +156,8 @@ export interface NhlTotals {
152
156
  }
153
157
  /** A ball in play. Give `x` and `y` in the frame, or for Retrosheet a `zone` (the location code, `8D`). */
154
158
  export interface Ball {
159
+ /** Your id for the shot, written on its mark as `data-mark-id`. */
160
+ id?: string | number;
155
161
  x?: number | null;
156
162
  y?: number | null;
157
163
  /** A Retrosheet hit-location code in place of `x` and `y`. */
@@ -197,6 +203,8 @@ export interface MlbTotals {
197
203
  }
198
204
  /** A pass. The destination is `x` and `y`; the passer's place is `fromX` and `fromY` when you have it. */
199
205
  export interface NflPass {
206
+ /** Your id for the shot, written on its mark as `data-mark-id`. */
207
+ id?: string | number;
200
208
  /** Where the ball came down or was caught, in the frame. */
201
209
  x: number | null;
202
210
  y: number | null;
@@ -246,6 +254,8 @@ export interface NflTotals {
246
254
  }
247
255
  /** A shot. The place is in the provider's frame; `goalY` and `goalZ` say where it crossed the goal line. */
248
256
  export interface SoccerShot {
257
+ /** Your id for the shot, written on its mark as `data-mark-id`. */
258
+ id?: string | number;
249
259
  x: number | null;
250
260
  y: number | null;
251
261
  /** `goal`, `saved`, `blocked`, `missed` or `post`. */
@@ -357,6 +367,58 @@ export interface CurlingOptions extends ChartOptions {
357
367
  units?: Unit;
358
368
  /** Write each stone's number on it. On by default, for the stones that have one. */
359
369
  numbers?: boolean;
370
+ /** How much of the sheet to draw: `half` (the default, the free guard zone and the house at `house`), `house`, `sheet` (the whole sheet), or `folded` (the whole sheet as a strip with both houses enlarged beside it). `surface.sheet.show` says the same and wins. */
371
+ show?: SheetShow;
372
+ }
373
+ /** What `curlingHeat` takes. */
374
+ export interface CurlingHeatOptions extends ChartOptions {
375
+ /** The house the stones are at: the `left` one (the top house of a tall drawing, the default) or the `right`. */
376
+ house?: "left" | "right";
377
+ /** The unit of the stones' places: feet by default. */
378
+ units?: Unit;
379
+ /** Only this team's stones. Both by default. */
380
+ team?: "home" | "away";
381
+ /** Which stones: `all` of those in play (the default), `guards` (the free guards only: between the tee line and the hog line, outside the house) or `house` (those in or touching it). */
382
+ only?: "all" | "guards" | "house";
383
+ /** How the stones are drawn: `heatmap` (counts in square cells, the default), `hexbin`, `contour` (a density), `zones` (counted into the button, the rings and the free guard zone) or `discs` (each stone a translucent disc in its team's colour, so that stones that fall on each other build up colour). */
384
+ view?: "heatmap" | "hexbin" | "contour" | "zones" | "discs";
385
+ /** The width of a cell of the `heatmap`, or the radius of a hexagon of the `hexbin`, in feet. 1 foot by default. */
386
+ cell?: number;
387
+ /** How much of the sheet to draw, as for `curlingStones`: `half` by default. */
388
+ show?: SheetShow;
389
+ /** The teams' names, for the summary. */
390
+ names?: {
391
+ home?: string;
392
+ away?: string;
393
+ };
394
+ /** Write the number of stones in each cell, hexagon or zone. Off by default, except for the zones. */
395
+ labels?: boolean;
396
+ }
397
+ /** The numbers behind a heat chart of stones. */
398
+ export interface CurlingHeatTotals {
399
+ /** Stones with a place, and the stones in play among them. */
400
+ stones: number;
401
+ inPlay: number;
402
+ /** The stones drawn: in play, and of the team and the kind asked for. */
403
+ drawn: number;
404
+ /** Of those in play, the free guards and the stones in the house, for each team. */
405
+ guards: {
406
+ home: number;
407
+ away: number;
408
+ };
409
+ house: {
410
+ home: number;
411
+ away: number;
412
+ };
413
+ /** Each zone of the house and the free guard zone, with the stones drawn that came to rest in it and their share of all drawn, as a percent. */
414
+ zones: {
415
+ id: string;
416
+ name: string;
417
+ stones: number;
418
+ share: number | null;
419
+ }[];
420
+ /** Rows left out for want of a place. */
421
+ skipped: number;
360
422
  }
361
423
  /** One stone as the chart judged it. */
362
424
  export interface CurlingStoneResult {
package/dist/colour.d.ts CHANGED
@@ -17,13 +17,13 @@ export declare function luminance(rgb: [number, number, number]): number;
17
17
  /** White or near-black, whichever reads better on the colour. White when the colour cannot be read. */
18
18
  export declare function inkOn(color: string | undefined): string;
19
19
  /**
20
- * A logo's address as it may be written into an image's `href`, or `undefined`. An `https:` or `http:` address, a
20
+ * A picture's address as it may be written into an image's `href`, or `undefined` (what `safeLogo` checks after it has looked for an emblem's id). An `https:` or `http:` address, a
21
21
  * relative path, an image `data:` address, or an SVG document as text (written as an image, which cannot run scripts).
22
22
  * @example
23
23
  * ```ts
24
- * import { safeLogo } from "@johnmorrisdotca/kyogijo-stadiums";
25
- * console.log(safeLogo("https://example.com/gulls.svg"), safeLogo("javascript:alert(1)"));
24
+ * import { safePicture } from "@johnmorrisdotca/kyogijo-stadiums";
25
+ * console.log(safePicture("https://example.com/gulls.svg"), safePicture("javascript:alert(1)"));
26
26
  * // https://example.com/gulls.svg undefined
27
27
  * ```
28
28
  */
29
- export declare function safeLogo(value: unknown): string | undefined;
29
+ export declare function safePicture(value: unknown): string | undefined;
package/dist/colour.js CHANGED
@@ -64,16 +64,16 @@ export function inkOn(color) {
64
64
  return luminance(rgb) > 0.4 ? "#161a18" : "#ffffff";
65
65
  }
66
66
  /**
67
- * A logo's address as it may be written into an image's `href`, or `undefined`. An `https:` or `http:` address, a
67
+ * A picture's address as it may be written into an image's `href`, or `undefined` (what `safeLogo` checks after it has looked for an emblem's id). An `https:` or `http:` address, a
68
68
  * relative path, an image `data:` address, or an SVG document as text (written as an image, which cannot run scripts).
69
69
  * @example
70
70
  * ```ts
71
- * import { safeLogo } from "@johnmorrisdotca/kyogijo-stadiums";
72
- * console.log(safeLogo("https://example.com/gulls.svg"), safeLogo("javascript:alert(1)"));
71
+ * import { safePicture } from "@johnmorrisdotca/kyogijo-stadiums";
72
+ * console.log(safePicture("https://example.com/gulls.svg"), safePicture("javascript:alert(1)"));
73
73
  * // https://example.com/gulls.svg undefined
74
74
  * ```
75
75
  */
76
- export function safeLogo(value) {
76
+ export function safePicture(value) {
77
77
  if (typeof value !== "string")
78
78
  return undefined;
79
79
  const text = value.trim();