@tennisvisuals/scoring-visualizations 0.2.1

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 (158) hide show
  1. package/dist/__tests__/LiveEngine.test.d.ts +2 -0
  2. package/dist/__tests__/LiveEngine.test.d.ts.map +1 -0
  3. package/dist/__tests__/LiveEngine.test.js +142 -0
  4. package/dist/__tests__/LiveEngine.test.js.map +1 -0
  5. package/dist/__tests__/buildEpisodes.test.d.ts +2 -0
  6. package/dist/__tests__/buildEpisodes.test.d.ts.map +1 -0
  7. package/dist/__tests__/buildEpisodes.test.js +150 -0
  8. package/dist/__tests__/buildEpisodes.test.js.map +1 -0
  9. package/dist/__tests__/extractors.test.d.ts +2 -0
  10. package/dist/__tests__/extractors.test.d.ts.map +1 -0
  11. package/dist/__tests__/extractors.test.js +50 -0
  12. package/dist/__tests__/extractors.test.js.map +1 -0
  13. package/dist/__tests__/feedMatchUp.test.d.ts +2 -0
  14. package/dist/__tests__/feedMatchUp.test.d.ts.map +1 -0
  15. package/dist/__tests__/feedMatchUp.test.js +96 -0
  16. package/dist/__tests__/feedMatchUp.test.js.map +1 -0
  17. package/dist/engine/LiveEngine.d.ts +42 -0
  18. package/dist/engine/LiveEngine.d.ts.map +1 -0
  19. package/dist/engine/LiveEngine.js +82 -0
  20. package/dist/engine/LiveEngine.js.map +1 -0
  21. package/dist/engine/buildSetMap.d.ts +14 -0
  22. package/dist/engine/buildSetMap.d.ts.map +1 -0
  23. package/dist/engine/buildSetMap.js +42 -0
  24. package/dist/engine/buildSetMap.js.map +1 -0
  25. package/dist/engine/createPlaybackEngine.d.ts +34 -0
  26. package/dist/engine/createPlaybackEngine.d.ts.map +1 -0
  27. package/dist/engine/createPlaybackEngine.js +90 -0
  28. package/dist/engine/createPlaybackEngine.js.map +1 -0
  29. package/dist/engine/feedMatchUp.d.ts +82 -0
  30. package/dist/engine/feedMatchUp.d.ts.map +1 -0
  31. package/dist/engine/feedMatchUp.js +116 -0
  32. package/dist/engine/feedMatchUp.js.map +1 -0
  33. package/dist/episodes/buildEpisodes.d.ts +10 -0
  34. package/dist/episodes/buildEpisodes.d.ts.map +1 -0
  35. package/dist/episodes/buildEpisodes.js +166 -0
  36. package/dist/episodes/buildEpisodes.js.map +1 -0
  37. package/dist/episodes/types.d.ts +45 -0
  38. package/dist/episodes/types.d.ts.map +1 -0
  39. package/dist/episodes/types.js +2 -0
  40. package/dist/episodes/types.js.map +1 -0
  41. package/dist/index.d.ts +23 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +24 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/statistics/index.d.ts +3 -0
  46. package/dist/statistics/index.d.ts.map +1 -0
  47. package/dist/statistics/index.js +2 -0
  48. package/dist/statistics/index.js.map +1 -0
  49. package/dist/statistics/matchStatistics.d.ts +22 -0
  50. package/dist/statistics/matchStatistics.d.ts.map +1 -0
  51. package/dist/statistics/matchStatistics.js +59 -0
  52. package/dist/statistics/matchStatistics.js.map +1 -0
  53. package/dist/visualizations/coronaChart.d.ts +54 -0
  54. package/dist/visualizations/coronaChart.d.ts.map +1 -0
  55. package/dist/visualizations/coronaChart.js +254 -0
  56. package/dist/visualizations/coronaChart.js.map +1 -0
  57. package/dist/visualizations/data/mcpFixtures.json +13166 -0
  58. package/dist/visualizations/data/sampleGame.d.ts +37 -0
  59. package/dist/visualizations/data/sampleGame.d.ts.map +1 -0
  60. package/dist/visualizations/data/sampleGame.js +521 -0
  61. package/dist/visualizations/data/sampleGame.js.map +1 -0
  62. package/dist/visualizations/gameFish.d.ts +16 -0
  63. package/dist/visualizations/gameFish.d.ts.map +1 -0
  64. package/dist/visualizations/gameFish.js +692 -0
  65. package/dist/visualizations/gameFish.js.map +1 -0
  66. package/dist/visualizations/gameTree.d.ts +17 -0
  67. package/dist/visualizations/gameTree.d.ts.map +1 -0
  68. package/dist/visualizations/gameTree.js +837 -0
  69. package/dist/visualizations/gameTree.js.map +1 -0
  70. package/dist/visualizations/groupGames.d.ts +6 -0
  71. package/dist/visualizations/groupGames.d.ts.map +1 -0
  72. package/dist/visualizations/groupGames.js +35 -0
  73. package/dist/visualizations/groupGames.js.map +1 -0
  74. package/dist/visualizations/helpers/JsonViewer.d.ts +27 -0
  75. package/dist/visualizations/helpers/JsonViewer.d.ts.map +1 -0
  76. package/dist/visualizations/helpers/JsonViewer.js +276 -0
  77. package/dist/visualizations/helpers/JsonViewer.js.map +1 -0
  78. package/dist/visualizations/helpers/PlaybackControls.d.ts +12 -0
  79. package/dist/visualizations/helpers/PlaybackControls.d.ts.map +1 -0
  80. package/dist/visualizations/helpers/PlaybackControls.js +98 -0
  81. package/dist/visualizations/helpers/PlaybackControls.js.map +1 -0
  82. package/dist/visualizations/horizonChart.d.ts +59 -0
  83. package/dist/visualizations/horizonChart.d.ts.map +1 -0
  84. package/dist/visualizations/horizonChart.js +215 -0
  85. package/dist/visualizations/horizonChart.js.map +1 -0
  86. package/dist/visualizations/index.d.ts +21 -0
  87. package/dist/visualizations/index.d.ts.map +1 -0
  88. package/dist/visualizations/index.js +23 -0
  89. package/dist/visualizations/index.js.map +1 -0
  90. package/dist/visualizations/legacyRally.d.ts +3 -0
  91. package/dist/visualizations/legacyRally.d.ts.map +1 -0
  92. package/dist/visualizations/legacyRally.js +26 -0
  93. package/dist/visualizations/legacyRally.js.map +1 -0
  94. package/dist/visualizations/matchDashboard.d.ts +39 -0
  95. package/dist/visualizations/matchDashboard.d.ts.map +1 -0
  96. package/dist/visualizations/matchDashboard.js +235 -0
  97. package/dist/visualizations/matchDashboard.js.map +1 -0
  98. package/dist/visualizations/momentumChart.d.ts +12 -0
  99. package/dist/visualizations/momentumChart.d.ts.map +1 -0
  100. package/dist/visualizations/momentumChart.js +454 -0
  101. package/dist/visualizations/momentumChart.js.map +1 -0
  102. package/dist/visualizations/ptsChart.d.ts +14 -0
  103. package/dist/visualizations/ptsChart.d.ts.map +1 -0
  104. package/dist/visualizations/ptsChart.js +925 -0
  105. package/dist/visualizations/ptsChart.js.map +1 -0
  106. package/dist/visualizations/ptsHorizon.d.ts +83 -0
  107. package/dist/visualizations/ptsHorizon.d.ts.map +1 -0
  108. package/dist/visualizations/ptsHorizon.js +290 -0
  109. package/dist/visualizations/ptsHorizon.js.map +1 -0
  110. package/dist/visualizations/rallyTree.d.ts +78 -0
  111. package/dist/visualizations/rallyTree.d.ts.map +1 -0
  112. package/dist/visualizations/rallyTree.js +410 -0
  113. package/dist/visualizations/rallyTree.js.map +1 -0
  114. package/dist/visualizations/simpleChart.d.ts +6 -0
  115. package/dist/visualizations/simpleChart.d.ts.map +1 -0
  116. package/dist/visualizations/simpleChart.js +126 -0
  117. package/dist/visualizations/simpleChart.js.map +1 -0
  118. package/dist/visualizations/statView.d.ts +52 -0
  119. package/dist/visualizations/statView.d.ts.map +1 -0
  120. package/dist/visualizations/statView.js +200 -0
  121. package/dist/visualizations/statView.js.map +1 -0
  122. package/dist/visualizations/typeOf.d.ts +6 -0
  123. package/dist/visualizations/typeOf.d.ts.map +1 -0
  124. package/dist/visualizations/typeOf.js +18 -0
  125. package/dist/visualizations/typeOf.js.map +1 -0
  126. package/dist/visualizations/types/events.d.ts +92 -0
  127. package/dist/visualizations/types/events.d.ts.map +1 -0
  128. package/dist/visualizations/types/events.js +2 -0
  129. package/dist/visualizations/types/events.js.map +1 -0
  130. package/dist/visualizations/types/index.d.ts +79 -0
  131. package/dist/visualizations/types/index.d.ts.map +1 -0
  132. package/dist/visualizations/types/index.js +8 -0
  133. package/dist/visualizations/types/index.js.map +1 -0
  134. package/dist/visualizations/utils/arrays.d.ts +18 -0
  135. package/dist/visualizations/utils/arrays.d.ts.map +1 -0
  136. package/dist/visualizations/utils/arrays.js +46 -0
  137. package/dist/visualizations/utils/arrays.js.map +1 -0
  138. package/dist/visualizations/utils/colorUtils.d.ts +36 -0
  139. package/dist/visualizations/utils/colorUtils.d.ts.map +1 -0
  140. package/dist/visualizations/utils/colorUtils.js +112 -0
  141. package/dist/visualizations/utils/colorUtils.js.map +1 -0
  142. package/dist/visualizations/utils/generateId.d.ts +2 -0
  143. package/dist/visualizations/utils/generateId.d.ts.map +1 -0
  144. package/dist/visualizations/utils/generateId.js +3 -0
  145. package/dist/visualizations/utils/generateId.js.map +1 -0
  146. package/dist/visualizations/utils/keyWalk.d.ts +2 -0
  147. package/dist/visualizations/utils/keyWalk.d.ts.map +1 -0
  148. package/dist/visualizations/utils/keyWalk.js +19 -0
  149. package/dist/visualizations/utils/keyWalk.js.map +1 -0
  150. package/dist/visualizations/utils/math.d.ts +10 -0
  151. package/dist/visualizations/utils/math.d.ts.map +1 -0
  152. package/dist/visualizations/utils/math.js +14 -0
  153. package/dist/visualizations/utils/math.js.map +1 -0
  154. package/dist/visualizations/utils/setDev.d.ts +29 -0
  155. package/dist/visualizations/utils/setDev.d.ts.map +1 -0
  156. package/dist/visualizations/utils/setDev.js +35 -0
  157. package/dist/visualizations/utils/setDev.js.map +1 -0
  158. package/package.json +57 -0
@@ -0,0 +1,692 @@
1
+ import { select, scaleLinear, scaleBand, range } from "d3";
2
+ import { extractGamePoints } from "../engine/feedMatchUp";
3
+ import { keyWalk } from "./utils/keyWalk";
4
+ import { generateId } from "./utils/generateId";
5
+ export function gameFish() {
6
+ let data;
7
+ let fishWidth;
8
+ let fishHeight;
9
+ let coords = [0, 0];
10
+ let lastCoords;
11
+ let update;
12
+ const images = { left: undefined, right: undefined };
13
+ const options = {
14
+ id: generateId(),
15
+ score: [0, 0],
16
+ width: 600,
17
+ height: 600,
18
+ margins: {
19
+ top: 10,
20
+ bottom: 10,
21
+ left: 10,
22
+ right: 10,
23
+ },
24
+ fish: {
25
+ school: false,
26
+ gridcells: ["0", "15", "30", "40", "G"],
27
+ maxRally: undefined,
28
+ cellSize: undefined,
29
+ minCellSize: 5,
30
+ maxCellSize: 20,
31
+ },
32
+ set: {
33
+ tiebreakTo: 7,
34
+ },
35
+ display: {
36
+ orientation: "vertical",
37
+ transitionTime: 0,
38
+ sizeToFit: false,
39
+ leftImg: false,
40
+ rightImg: false,
41
+ showImages: undefined,
42
+ reverse: false,
43
+ pointScore: true,
44
+ service: true,
45
+ player: true,
46
+ rally: true,
47
+ score: true,
48
+ grid: true,
49
+ },
50
+ colors: {
51
+ players: { 0: "red", 1: "black" },
52
+ results: {
53
+ Out: "red",
54
+ Net: "coral",
55
+ "Unforced Error": "red",
56
+ Forced: "orange",
57
+ Ace: "lightgreen",
58
+ "Serve Winner": "lightgreen",
59
+ Winner: "lightgreen",
60
+ "Forced Volley Error": "orange",
61
+ "Forced Error": "orange",
62
+ In: "yellow",
63
+ "Passing Shot": "lightgreen",
64
+ "Out Passing Shot": "red",
65
+ "Net Cord": "yellow",
66
+ "Out Wide": "red",
67
+ "Out Long": "red",
68
+ "Double Fault": "red",
69
+ Unknown: "blue",
70
+ Error: "red",
71
+ },
72
+ },
73
+ };
74
+ const STROKE_WIDTH = "stroke-width";
75
+ const FILL_OPACITY = "fill-opacity";
76
+ const default_colors = { default: "#235dba" };
77
+ let colors = structuredClone(default_colors);
78
+ const events = {
79
+ leftImage: { click: null },
80
+ rightImage: { click: null },
81
+ update: { begin: null, end: null },
82
+ point: { mouseover: null, mouseout: null, click: null },
83
+ };
84
+ let fishFrame;
85
+ let root;
86
+ let bars;
87
+ let fish;
88
+ let game;
89
+ function findOffset(point) {
90
+ // In school mode (momentum chart), use cumulative set points for nose-to-tail alignment.
91
+ // In standalone mode, use game-level points so the grid stays centered.
92
+ const pts = (options.fish.school && point.setCumulativePoints) || point.points;
93
+ if (!pts || pts.length < 2)
94
+ return 0;
95
+ return (pts[options.display.reverse ? 0 : 1] -
96
+ pts[options.display.reverse ? 1 : 0]);
97
+ }
98
+ function chart(selection) {
99
+ const parentType = selection._groups[0][0].tagName.toLowerCase();
100
+ if (parentType !== "svg") {
101
+ root = selection.append("div").attr("class", "fishRoot");
102
+ fishFrame = root
103
+ .append("svg")
104
+ .attr("id", "gameFish" + options.id)
105
+ .attr("class", "fishFrame");
106
+ bars = fishFrame.append("g");
107
+ fish = fishFrame.append("g");
108
+ game = fishFrame.append("g");
109
+ }
110
+ update = function (opts) {
111
+ if (bars === undefined || fish === undefined || game === undefined)
112
+ return;
113
+ if (options.display.sizeToFit || opts?.sizeToFit) {
114
+ const dims = selection.node().getBoundingClientRect();
115
+ options.width = Math.max(dims.width, 100);
116
+ options.height = Math.max(dims.height, 100);
117
+ }
118
+ if (options.fish.cellSize && !options.fish.school) {
119
+ const multiplier = Math.max(10, data.length + 2);
120
+ options.height = options.fish.cellSize * multiplier * 0.9;
121
+ }
122
+ let tiebreak = false;
123
+ let maxRally = 0;
124
+ data.forEach((e) => {
125
+ const rlen = e.rallyLength;
126
+ if (rlen > maxRally)
127
+ maxRally = rlen;
128
+ if (e.score && e.score.indexOf("T") > 0)
129
+ tiebreak = true;
130
+ });
131
+ if (options.fish.maxRally && options.fish.maxRally > maxRally)
132
+ maxRally = options.fish.maxRally;
133
+ fishWidth =
134
+ options.width - (options.margins.left + options.margins.right);
135
+ fishHeight =
136
+ options.height - (options.margins.top + options.margins.bottom);
137
+ // Ensure dimensions are valid
138
+ if (Number.isNaN(fishWidth) || fishWidth <= 0)
139
+ fishWidth = 100;
140
+ if (Number.isNaN(fishHeight) || fishHeight <= 0)
141
+ fishHeight = 100;
142
+ const vert = options.display.orientation === "vertical" ? 1 : 0;
143
+ const fish_offset = vert ? fishWidth : fishHeight;
144
+ const fish_length = vert ? fishHeight : fishWidth;
145
+ const midpoint = (vert ? options.margins.left : options.margins.top) + fish_offset / 2;
146
+ const sw = 1; // service box % offset
147
+ const rw = 0.9; // rally_width % offset
148
+ bars.attr("transform", translate(vert ? 0 : coords[0], vert ? coords[1] : 0, 0));
149
+ fish.attr("transform", translate(coords[0], coords[1], 0));
150
+ game.attr("transform", translate(coords[0], coords[1], 0));
151
+ let cellSize;
152
+ if (options.fish.cellSize) {
153
+ cellSize = options.fish.cellSize;
154
+ }
155
+ else {
156
+ const offset_divisor = tiebreak
157
+ ? options.set.tiebreakTo + 4
158
+ : options.fish.gridcells.length + 2;
159
+ const cell_offset = fish_offset /
160
+ (options.fish.gridcells.length +
161
+ (options.display.service ? offset_divisor : 0));
162
+ const cell_length = fish_length / (data.length + 2);
163
+ cellSize = Math.min(cell_offset, cell_length);
164
+ cellSize = Math.max(options.fish.minCellSize, cellSize);
165
+ cellSize = Math.min(options.fish.maxCellSize, cellSize);
166
+ }
167
+ // Ensure cellSize is valid
168
+ if (!cellSize || Number.isNaN(cellSize) || cellSize <= 0) {
169
+ cellSize = options.fish.minCellSize || 5;
170
+ }
171
+ const diag = Math.sqrt(2 * Math.pow(cellSize, 2));
172
+ const radius = diag / 2;
173
+ // In school mode (momentum chart), compute the lateral offset for the grid
174
+ // so it aligns with the fish body's starting position.
175
+ // gridBaseOffset = cumulative set differential BEFORE the first point of this game.
176
+ let gridBaseOffset = 0;
177
+ if (data.length > 0 && options.fish.school) {
178
+ const firstPt = data[0];
179
+ const cumPts = firstPt.setCumulativePoints;
180
+ const gamePts = firstPt.points;
181
+ if (cumPts && gamePts) {
182
+ const rev = options.display.reverse;
183
+ const cumDiff = cumPts[rev ? 0 : 1] - cumPts[rev ? 1 : 0];
184
+ const gameDiff = gamePts[rev ? 0 : 1] - gamePts[rev ? 1 : 0];
185
+ gridBaseOffset = cumDiff - gameDiff;
186
+ }
187
+ }
188
+ const grid_data = [];
189
+ const grid_labels = [];
190
+ const grid_side = tiebreak
191
+ ? options.set.tiebreakTo
192
+ : options.fish.gridcells.length - 1;
193
+ for (let g = 0; g < grid_side; g++) {
194
+ const label = tiebreak ? g : options.fish.gridcells[g];
195
+ // l = length, o = offset
196
+ grid_labels.push({
197
+ label: label,
198
+ l: (g + (vert ? 1.25 : 0.75)) * radius,
199
+ o: (g + (vert ? 0.75 : 1.25)) * radius,
200
+ rotate: 45,
201
+ });
202
+ grid_labels.push({
203
+ label: label,
204
+ l: (g + 1.25) * radius,
205
+ o: -1 * (g + 0.75) * radius,
206
+ rotate: -45,
207
+ });
208
+ for (let c = 0; c < grid_side; c++) {
209
+ grid_data.push([g, c]);
210
+ }
211
+ }
212
+ // check if this is a standalone SVG or part of larger SVG
213
+ if (root) {
214
+ root
215
+ .attr("width", options.width + "px")
216
+ .attr("height", options.height + "px");
217
+ fishFrame
218
+ .attr("width", options.width + "px")
219
+ .attr("height", options.height + "px");
220
+ }
221
+ if (options.display.pointScore) {
222
+ fish
223
+ .selectAll(".game_score" + options.id)
224
+ .data(grid_labels)
225
+ .join("text")
226
+ .attr("class", "game_score" + options.id)
227
+ .attr("font-size", radius * 0.8 + "px")
228
+ .attr("transform", gscoreT)
229
+ .attr("text-anchor", "middle")
230
+ .text(function (d) {
231
+ return d.label;
232
+ });
233
+ }
234
+ else {
235
+ fish.selectAll(".game_score" + options.id).remove();
236
+ }
237
+ if (options.display.grid) {
238
+ fish
239
+ .selectAll(".gridcell" + options.id)
240
+ .data(grid_data)
241
+ .join("rect")
242
+ .attr("class", "gridcell" + options.id)
243
+ .attr("stroke", "#ccccdd")
244
+ .attr(STROKE_WIDTH, lineWidth)
245
+ .attr("transform", gridCT)
246
+ .attr("width", cellSize)
247
+ .attr("height", cellSize)
248
+ .attr(FILL_OPACITY, 0);
249
+ }
250
+ else {
251
+ fish.selectAll(".gridcell" + options.id).remove();
252
+ }
253
+ game
254
+ .selectAll(".gamecell" + options.id)
255
+ .data(data)
256
+ .join("rect")
257
+ .attr("id", (d, i) => {
258
+ return options.id + "Gs" + d.set + "g" + d.game + "p" + i;
259
+ })
260
+ .attr("class", "gamecell" + options.id)
261
+ .attr("width", cellSize)
262
+ .attr("height", cellSize)
263
+ .attr("transform", gameCT)
264
+ .attr("stroke", "#ccccdd")
265
+ .attr(STROKE_WIDTH, lineWidth)
266
+ .attr("opacity", options.display.player ? 1 : 0)
267
+ .style("fill", function (d) {
268
+ return options.colors.players[d.winner];
269
+ });
270
+ game
271
+ .selectAll(".result" + options.id)
272
+ .data(data)
273
+ .join("circle")
274
+ .attr("id", function (d, i) {
275
+ return options.id + "Rs" + d.set + "g" + d.game + "p" + i;
276
+ })
277
+ .attr("class", "result" + options.id)
278
+ .attr("stroke", "black")
279
+ .attr("opacity", 1)
280
+ .attr(STROKE_WIDTH, lineWidth)
281
+ .attr("cx", zX)
282
+ .attr("cy", zY)
283
+ .attr("r", circleRadius)
284
+ .style("fill", function (d) {
285
+ return options.colors.results[d.result];
286
+ });
287
+ // offset Scale
288
+ const oScale = scaleLinear()
289
+ .range([0, fish_offset * rw])
290
+ .domain([0, maxRally]);
291
+ // lengthScale
292
+ const lScale = scaleBand()
293
+ .domain(range(data.length).map(String))
294
+ .range([0, data.length * radius])
295
+ .round(true);
296
+ if (options.display.rally) {
297
+ bars
298
+ .selectAll(".rally_bar" + options.id)
299
+ .data(data)
300
+ .join((enter) => enter
301
+ .append("rect")
302
+ .on("mouseover", function (event, d) {
303
+ select(this).attr("fill", "yellow");
304
+ if (events.point.mouseover)
305
+ events.point.mouseover(d, event);
306
+ })
307
+ .on("mouseout", function (event, d) {
308
+ select(this).attr("fill", "#eeeeff");
309
+ if (events.point.mouseout)
310
+ events.point.mouseout(d, event);
311
+ })
312
+ .on("click", function (event, d) {
313
+ if (events.point.click)
314
+ events.point.click(d, event);
315
+ }))
316
+ .attr("class", "rally_bar" + options.id)
317
+ .attr("id", function (d, i) {
318
+ return options.id + "Bs" + d.set + "g" + d.game + "p" + i;
319
+ })
320
+ .attr("opacity", 1)
321
+ .attr("stroke", "white")
322
+ .attr(STROKE_WIDTH, lineWidth)
323
+ .attr("fill", "#eeeeff")
324
+ .attr("transform", rallyT)
325
+ .attr("height", vert ? lScale.bandwidth() : rallyCalc)
326
+ .attr("width", vert ? rallyCalc : lScale.bandwidth());
327
+ }
328
+ else {
329
+ bars.selectAll(".rally_bar" + options.id).remove();
330
+ }
331
+ if (options.display.score) {
332
+ const score = options.score.slice();
333
+ if (options.display.reverse)
334
+ score.reverse();
335
+ bars
336
+ .selectAll(".set_score" + options.id)
337
+ .data(score)
338
+ .join("text")
339
+ .attr("class", "set_score" + options.id)
340
+ .attr("transform", sscoreT)
341
+ .attr("font-size", radius * 0.8 + "px")
342
+ .attr("text-anchor", "middle")
343
+ .text(function (d) {
344
+ return d;
345
+ });
346
+ bars
347
+ .selectAll(".ssb" + options.id)
348
+ .data(options.score)
349
+ .join("rect")
350
+ .attr("class", "ssb" + options.id)
351
+ .attr("transform", ssbT)
352
+ .attr("stroke", "black")
353
+ .attr(STROKE_WIDTH, lineWidth)
354
+ .attr(FILL_OPACITY, 0)
355
+ .attr("height", radius + "px")
356
+ .attr("width", radius + "px");
357
+ }
358
+ else {
359
+ bars.selectAll(".set_score" + options.id).remove();
360
+ bars.selectAll(".ssb" + options.id).remove();
361
+ }
362
+ if (options.display.service) {
363
+ const serves = [];
364
+ data.forEach(function (s, i) {
365
+ let first_serve = false;
366
+ const serve_outcomes = ["Ace", "Serve Winner", "Double Fault"];
367
+ if (s.first_serve) {
368
+ first_serve = true;
369
+ serves.push({
370
+ point: i,
371
+ serve: "first",
372
+ server: s.server,
373
+ result: s.first_serve.error,
374
+ });
375
+ }
376
+ serves.push({
377
+ point: i,
378
+ serve: first_serve ? "second" : "first",
379
+ server: s.server,
380
+ result: serve_outcomes.includes(s.result) ? s.result : "In",
381
+ });
382
+ });
383
+ bars
384
+ .selectAll(".serve" + options.id)
385
+ .data(serves)
386
+ .join("circle")
387
+ .attr("class", "serve" + options.id)
388
+ .attr("cx", sX)
389
+ .attr("cy", sY)
390
+ .attr("r", circleRadius)
391
+ .attr("stroke", colorShot)
392
+ .attr(STROKE_WIDTH, lineWidth)
393
+ .attr("fill", colorShot);
394
+ bars
395
+ .selectAll(".sbox" + options.id)
396
+ .data(data)
397
+ .join("rect")
398
+ .attr("class", "sbox" + options.id)
399
+ .attr("stroke", "#ccccdd")
400
+ .attr(FILL_OPACITY, 0)
401
+ .attr("transform", sBoxT)
402
+ .attr(STROKE_WIDTH, lineWidth)
403
+ .attr("height", vert ? lScale.bandwidth() : 1.5 * radius)
404
+ .attr("width", vert ? 1.5 * radius : lScale.bandwidth());
405
+ bars
406
+ .selectAll(".return" + options.id)
407
+ .data(data)
408
+ .join("circle")
409
+ .attr("class", "return" + options.id)
410
+ .attr("cx", rX)
411
+ .attr("cy", rY)
412
+ .attr("r", circleRadius)
413
+ .attr("stroke", colorReturn)
414
+ .attr(STROKE_WIDTH, lineWidth)
415
+ .attr("fill", colorReturn);
416
+ }
417
+ else {
418
+ bars.selectAll(".sbox" + options.id).remove();
419
+ bars.selectAll(".return" + options.id).remove();
420
+ bars.selectAll(".serve" + options.id).remove();
421
+ }
422
+ if (options.display.rightImg) {
423
+ images.right = fishFrame
424
+ .selectAll("image.rightImage")
425
+ .data([0])
426
+ .join((enter) => enter
427
+ .append("image")
428
+ .attr("class", "rightImage")
429
+ .attr("y", 5)
430
+ .attr("height", "20px")
431
+ .attr("width", "20px")
432
+ .attr("opacity", options.display.showImages ? 1 : 0)
433
+ .on("click", function () {
434
+ if (events.rightImage.click)
435
+ events.rightImage.click(options.id);
436
+ }))
437
+ .attr("x", options.width - 30)
438
+ .attr("xlink:href", options.display.rightImg);
439
+ }
440
+ else if (fishFrame) {
441
+ fishFrame.selectAll("image.rightImage").remove();
442
+ }
443
+ if (options.display.leftImg) {
444
+ images.left = fishFrame
445
+ .selectAll("image.leftImage")
446
+ .data([0])
447
+ .join((enter) => enter
448
+ .append("image")
449
+ .attr("class", "leftImage")
450
+ .attr("x", 10)
451
+ .attr("y", 5)
452
+ .attr("height", "20px")
453
+ .attr("width", "20px")
454
+ .attr("opacity", options.display.showImages ? 1 : 0)
455
+ .on("click", function () {
456
+ if (events.leftImage.click)
457
+ events.leftImage.click(options.id);
458
+ }))
459
+ .attr("xlink:href", options.display.leftImg);
460
+ }
461
+ else if (fishFrame) {
462
+ fishFrame.selectAll("image.leftImage").remove();
463
+ }
464
+ // ancillary functions for update()
465
+ function circleRadius() {
466
+ return options.display.player || options.display.service
467
+ ? radius / 3
468
+ : radius / 2;
469
+ }
470
+ function lineWidth() {
471
+ return radius > 20 ? 1.5 : 0.75;
472
+ }
473
+ function colorShot(d) {
474
+ return options.colors.results[d.result];
475
+ }
476
+ function colorReturn(d) {
477
+ const rlen = d.rallyLength;
478
+ if (!rlen)
479
+ return "white";
480
+ if (rlen > 1)
481
+ return "yellow";
482
+ if (rlen === 1)
483
+ return options.colors.results[d.result];
484
+ return "white";
485
+ }
486
+ function rallyCalc(d) {
487
+ const rlen = d.rallyLength;
488
+ return rlen ? oScale(rlen) : 0;
489
+ }
490
+ function sscoreT(_, i) {
491
+ let o = i ? midpoint + radius * 0.5 : midpoint - radius * 0.5;
492
+ o = vert ? o : o + radius * 0.3;
493
+ const l = radius * (vert ? 0.8 : 0.5);
494
+ return translate(o, l, 0);
495
+ }
496
+ function ssbT(_, i) {
497
+ const o = i ? midpoint : midpoint - radius;
498
+ const l = 0;
499
+ return translate(o, l, 0);
500
+ }
501
+ function gscoreT(d) {
502
+ const o = +midpoint + d.o + gridBaseOffset * radius;
503
+ const l = radius + d.l;
504
+ return translate(o, l, d.rotate);
505
+ }
506
+ // for the momentum chart the midpoint needs to be adjusted
507
+ function gridCT(d) {
508
+ const o = midpoint + (d[1] - d[0] + vert - 1 + gridBaseOffset) * radius;
509
+ const l = (d[0] + d[1] + 3 - vert) * radius;
510
+ return translate(o, l, 45);
511
+ }
512
+ function gameCT(d, i) {
513
+ const o = midpoint + (findOffset(d) + vert - 1) * radius;
514
+ const l = (i + 4 - vert) * radius;
515
+ lastCoords = [o - midpoint, l - diag, diag];
516
+ return translate(o, l, 45);
517
+ }
518
+ function sBoxT(d, i) {
519
+ const o = d.server === 0
520
+ ? midpoint - (fish_offset / 2) * sw
521
+ : midpoint + (fish_offset / 2) * sw - 1.5 * radius;
522
+ const l = radius + cL(d, i);
523
+ return translate(o, l, 0);
524
+ }
525
+ function _rallyTstart(d, i) {
526
+ const o = midpoint;
527
+ const l = radius + cL(d, i);
528
+ return translate(o, l, 0);
529
+ }
530
+ function rallyT(d, i) {
531
+ const o = d.rallyLength ? midpoint - oScale(d.rallyLength) / 2 : 0;
532
+ const l = radius + cL(d, i);
533
+ return translate(o, l, 0);
534
+ }
535
+ function translate(o, l, rotate) {
536
+ const x = vert ? o : l;
537
+ const y = vert ? l : o;
538
+ return "translate(" + x + "," + y + ") rotate(" + rotate + ")";
539
+ }
540
+ function cL(_, i) {
541
+ return (i + 2.5) * radius;
542
+ }
543
+ function rX(d, i) {
544
+ return vert ? rO(d) : rL(d, i);
545
+ }
546
+ function rY(d, i) {
547
+ return vert ? rL(d, i) : rO(d);
548
+ }
549
+ function rL(_, i) {
550
+ return radius + (i + 3) * radius;
551
+ }
552
+ function rO(d) {
553
+ return d.server === 0
554
+ ? midpoint + (fish_offset / 2) * sw - 0.75 * radius
555
+ : midpoint - (fish_offset / 2) * sw + 0.75 * radius;
556
+ }
557
+ function sX(d) {
558
+ return vert ? sO(d) : sL(d);
559
+ }
560
+ function sY(d) {
561
+ return vert ? sL(d) : sO(d);
562
+ }
563
+ function sL(d) {
564
+ return radius + (d.point + 3) * radius;
565
+ }
566
+ function sO(d) {
567
+ const offset = (d.serve === "first" && d.server === 0) ||
568
+ (d.serve === "second" && d.server === 1)
569
+ ? 0.4
570
+ : 1.1;
571
+ return d.server === 0
572
+ ? midpoint - (fish_offset / 2) * sw + offset * radius
573
+ : midpoint + (fish_offset / 2) * sw - offset * radius;
574
+ }
575
+ function zX(d, i) {
576
+ return vert ? zO(d) : zL(d, i);
577
+ }
578
+ function zY(d, i) {
579
+ return vert ? zL(d, i) : zO(d);
580
+ }
581
+ function zL(_, i) {
582
+ return radius + (i + 3) * radius;
583
+ }
584
+ function zO(d) {
585
+ return +midpoint + findOffset(d) * radius;
586
+ }
587
+ };
588
+ }
589
+ // ACCESSORS
590
+ chart.g = function (values) {
591
+ if (!arguments.length)
592
+ return chart;
593
+ if (typeof values != "object" || values.constructor === Array)
594
+ return;
595
+ if (values.bars)
596
+ bars = values.bars;
597
+ if (values.fish)
598
+ fish = values.fish;
599
+ if (values.game)
600
+ game = values.game;
601
+ };
602
+ // allows updating individual options and suboptions
603
+ // while preserving state of other options
604
+ chart.options = function (values) {
605
+ if (!arguments.length)
606
+ return options;
607
+ keyWalk(values, options);
608
+ if (values.events)
609
+ keyWalk(values.events, events);
610
+ return chart;
611
+ };
612
+ chart.events = function (functions) {
613
+ if (!arguments.length)
614
+ return events;
615
+ keyWalk(functions, events);
616
+ return chart;
617
+ };
618
+ chart.width = function (value) {
619
+ if (!arguments.length)
620
+ return options.width;
621
+ options.width = value;
622
+ return chart;
623
+ };
624
+ chart.height = function (value) {
625
+ if (!arguments.length)
626
+ return options.height;
627
+ options.height = value;
628
+ return chart;
629
+ };
630
+ chart.coords = function (value) {
631
+ if (!arguments.length)
632
+ return lastCoords;
633
+ coords = value;
634
+ return chart;
635
+ };
636
+ chart.data = function (value) {
637
+ if (!arguments.length)
638
+ return data;
639
+ // gameFish receives GameGroup which contains points array
640
+ // If value contains episodes that need normalization, handle it
641
+ if (value?.points &&
642
+ Array.isArray(value.points) &&
643
+ value.points.length > 0) {
644
+ // This is a GameGroup - points might be UMO v4 Episodes
645
+ // For gameFish, we just need the point data which is already extracted in groupGames
646
+ data = structuredClone(value);
647
+ }
648
+ else {
649
+ data = value;
650
+ }
651
+ return chart;
652
+ };
653
+ chart.matchUp = function (matchUpState, setIdx, gameIdx) {
654
+ const points = extractGamePoints(matchUpState, setIdx ?? 0, gameIdx ?? 0);
655
+ chart.data(points);
656
+ return chart;
657
+ };
658
+ chart.update = function (opts) {
659
+ if (events.update.begin)
660
+ events.update.begin();
661
+ if (typeof update === "function" && data)
662
+ update(opts);
663
+ setTimeout(function () {
664
+ if (events.update.end)
665
+ events.update.end();
666
+ }, options.display.transitionTime);
667
+ };
668
+ chart.colors = function (color3s) {
669
+ if (!arguments.length)
670
+ return colors;
671
+ if (typeof color3s !== "object")
672
+ return false;
673
+ const keys = Object.keys(color3s);
674
+ if (!keys.length)
675
+ return false;
676
+ // remove all properties that are not colors
677
+ keys.forEach(function (f) {
678
+ if (!/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color3s[f]))
679
+ delete color3s[f];
680
+ });
681
+ if (Object.keys(color3s).length) {
682
+ colors = color3s;
683
+ }
684
+ else {
685
+ colors = structuredClone(default_colors);
686
+ }
687
+ return chart;
688
+ };
689
+ return chart;
690
+ // ancillary functions
691
+ }
692
+ //# sourceMappingURL=gameFish.js.map