@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,925 @@
1
+ /* eslint-disable */
2
+ // @ts-nocheck
3
+ import { select, scaleLinear, scaleBand, range, line } from 'd3';
4
+ import { rallyCount } from './legacyRally';
5
+ import { buildEpisodes } from '../episodes/buildEpisodes';
6
+ import { keyWalk } from './utils/keyWalk';
7
+ import { generateId } from './utils/generateId';
8
+ function groupGames(point_episodes) {
9
+ const episodes = point_episodes;
10
+ const games = [{ points: [], range: [0, 0] }];
11
+ let gameCounter = 0;
12
+ let currentGame = 0;
13
+ episodes.forEach((episode) => {
14
+ const point = episode.point;
15
+ if (point.game != currentGame) {
16
+ gameCounter += 1;
17
+ currentGame = point.game;
18
+ games[gameCounter] = { points: [], range: [point.index, point.index] };
19
+ }
20
+ games[gameCounter].points.push(point);
21
+ games[gameCounter].index = gameCounter;
22
+ games[gameCounter].set = episode.set.index;
23
+ games[gameCounter].score = episode.game.games;
24
+ games[gameCounter].complete = episode.game.complete;
25
+ games[gameCounter].range[1] = point.index;
26
+ if (episode.game.complete) {
27
+ games[gameCounter].winner = point.winner;
28
+ }
29
+ });
30
+ return games;
31
+ }
32
+ function add_index(d, i) {
33
+ for (const item of d) {
34
+ item['_i'] = i;
35
+ }
36
+ return d;
37
+ }
38
+ export function ptsMatch() {
39
+ let match_data;
40
+ let participantNames = ['Player 1', 'Player 2'];
41
+ const options = {
42
+ id: generateId(),
43
+ class: 'ptsMatch',
44
+ resize: true,
45
+ width: 600,
46
+ height: 80,
47
+ maxHeight: 100,
48
+ margins: {
49
+ top: 0,
50
+ right: 0,
51
+ bottom: 0,
52
+ left: 0,
53
+ },
54
+ set: {
55
+ averagePoints: 56,
56
+ },
57
+ lines: {
58
+ width: 2,
59
+ interpolation: 'linear',
60
+ },
61
+ points: {
62
+ maxWidthPoints: 100,
63
+ },
64
+ score: {
65
+ font: 'Arial',
66
+ fontSize: '12px',
67
+ fontWeight: 'bold',
68
+ reverse: true,
69
+ },
70
+ header: {
71
+ font: 'Arial',
72
+ fontSize: '14px',
73
+ fontWeight: 'bold',
74
+ },
75
+ display: {
76
+ sizeToFit: true,
77
+ transitionTime: 0,
78
+ pointHighlighting: true,
79
+ pointOpacity: 0.4,
80
+ winErrHighlight: true,
81
+ gameHighlighting: true,
82
+ gameOpacity: 0.2,
83
+ gameBoundaries: true,
84
+ gamepoints: false,
85
+ score: true,
86
+ points: true,
87
+ winner: true,
88
+ },
89
+ colors: {
90
+ orientation: 'yellow',
91
+ gamepoints: 'black',
92
+ players: { 0: '#a55194', 1: '#6b6ecf' },
93
+ },
94
+ };
95
+ // functions which should be accessible via ACCESSORS
96
+ let update;
97
+ // programmatic
98
+ const pts_sets = [];
99
+ let dom_parent;
100
+ // prepare charts
101
+ const pts_charts = [];
102
+ for (let s = 0; s < 5; s++) {
103
+ pts_charts.push(ptsChart());
104
+ }
105
+ // DEFINABLE EVENTS
106
+ // Define with ACCESSOR function chart.events()
107
+ const events = {
108
+ update: { begin: null, end: null },
109
+ setBox: { mouseover: null, mouseout: null },
110
+ pointBars: { mouseover: null, mouseout: null, click: null },
111
+ };
112
+ function chart(selection) {
113
+ dom_parent = selection;
114
+ if (options.display.sizeToFit) {
115
+ const dims = selection.node().getBoundingClientRect();
116
+ options.width = Math.max(dims.width, 400);
117
+ }
118
+ // append svg
119
+ const root = dom_parent
120
+ .append('div')
121
+ .attr('class', options.class + 'root')
122
+ .style('width', options.width + 'px')
123
+ .style('height', options.height + 'px');
124
+ for (let s = 0; s < 5; s++) {
125
+ pts_sets[s] = root.append('div').attr('class', 'pts').style('display', 'none');
126
+ pts_sets[s].call(pts_charts[s]);
127
+ }
128
+ update = function (opts) {
129
+ const sets = match_data.sets();
130
+ if (options.display.sizeToFit || opts?.sizeToFit) {
131
+ const dims = selection.node().getBoundingClientRect();
132
+ options.width = Math.max(dims.width, 400);
133
+ options.height = Math.max((dims.height - (+options.margins.top + +options.margins.bottom)) / sets.length, 20);
134
+ if (options.height > options.maxHeight)
135
+ options.height = options.maxHeight;
136
+ }
137
+ let true_height = 0;
138
+ for (let s = 0; s < pts_charts.length; s++) {
139
+ if (sets?.[s]?.history.points().length) {
140
+ pts_sets[s].style('display', 'inline');
141
+ pts_charts[s].width(options.width);
142
+ pts_charts[s].height(options.height);
143
+ pts_charts[s].update();
144
+ true_height += +options.height + 5;
145
+ }
146
+ else {
147
+ pts_sets[s].style('display', 'none');
148
+ }
149
+ }
150
+ root.style('width', options.width + 'px').style('height', true_height + 'px');
151
+ };
152
+ }
153
+ // ACCESSORS
154
+ // allows updating individual options and suboptions
155
+ // while preserving state of other options
156
+ chart.options = function (values) {
157
+ if (!arguments.length)
158
+ return options;
159
+ keyWalk(values, options);
160
+ if (values.events)
161
+ keyWalk(values.events, events);
162
+ return chart;
163
+ };
164
+ chart.events = function (functions) {
165
+ if (!arguments.length)
166
+ return events;
167
+ keyWalk(functions, events);
168
+ return chart;
169
+ };
170
+ chart.colors = function (colores) {
171
+ if (!arguments.length)
172
+ return options.colors;
173
+ options.colors.players = colores;
174
+ return chart;
175
+ };
176
+ chart.width = function (value) {
177
+ if (!arguments.length)
178
+ return options.width;
179
+ options.width = value;
180
+ if (typeof update === 'function')
181
+ update(true);
182
+ pts_charts.forEach(function (e) {
183
+ e.width(value);
184
+ });
185
+ return chart;
186
+ };
187
+ chart.height = function (value) {
188
+ if (!arguments.length)
189
+ return options.height;
190
+ options.height = value;
191
+ if (typeof update === 'function')
192
+ update(true);
193
+ pts_charts.forEach(function (e) {
194
+ e.height(value);
195
+ });
196
+ return chart;
197
+ };
198
+ chart.duration = function (value) {
199
+ if (!arguments.length)
200
+ return options.display.transitionTime;
201
+ options.display.transitionTime = value;
202
+ return chart;
203
+ };
204
+ chart.update = function (opts) {
205
+ if (!match_data) {
206
+ return false;
207
+ }
208
+ if (events.update.begin)
209
+ events.update.begin();
210
+ const sets = match_data.sets();
211
+ if (!sets || sets.length === 0) {
212
+ return false;
213
+ }
214
+ const maxWidthPoints = Math.max(...sets.map((set, index) => {
215
+ if (!set?.history?.points) {
216
+ return 0;
217
+ }
218
+ const setPoints = set.history.points();
219
+ const filtered = setPoints.filter((f) => f.set == index);
220
+ return filtered.length;
221
+ }));
222
+ if (sets.length > 1)
223
+ chart.options({ points: { maxWidthPoints } });
224
+ sets.forEach(function (set, i) {
225
+ pts_charts[i].data(set);
226
+ pts_charts[i].options({ id: `${options.id}_${i}`, setIndex: i });
227
+ pts_charts[i].options({
228
+ lines: options.lines,
229
+ points: options.points,
230
+ score: options.score,
231
+ header: options.header,
232
+ });
233
+ pts_charts[i].options({ set: options.set, display: options.display, colors: options.colors });
234
+ pts_charts[i].events(events);
235
+ pts_charts[i].width(options.width).height(options.height).update(opts);
236
+ });
237
+ if (typeof update === 'function')
238
+ update(opts);
239
+ setTimeout(function () {
240
+ if (events.update.end)
241
+ events.update.end();
242
+ }, options.display.transitionTime);
243
+ return true;
244
+ };
245
+ chart.data = function (matchObjectOrEpisodes) {
246
+ if (!arguments.length) {
247
+ return match_data;
248
+ }
249
+ // Support both UMO objects (for main app) and episode arrays (for standalone)
250
+ if (Array.isArray(matchObjectOrEpisodes)) {
251
+ const episodes = matchObjectOrEpisodes;
252
+ // Plain array of episodes - create simple accessor object
253
+ match_data = {
254
+ sets: () => {
255
+ // Group episodes by set
256
+ const setsMap = new Map();
257
+ episodes.forEach((ep) => {
258
+ const setIndex = ep.set?.index ?? 0;
259
+ if (!setsMap.has(setIndex)) {
260
+ setsMap.set(setIndex, []);
261
+ }
262
+ setsMap.get(setIndex).push(ep);
263
+ });
264
+ // Return array of set objects with all required accessors for ptsSet
265
+ return Array.from(setsMap.values()).map((episodes, _setIndex) => {
266
+ const points = episodes.map((ep) => ep.point);
267
+ const lastEpisode = episodes[episodes.length - 1];
268
+ const isComplete = lastEpisode?.set?.complete ?? false;
269
+ return {
270
+ history: {
271
+ points: () => points,
272
+ action: (actionType) => (actionType === 'addPoint' ? episodes : []),
273
+ },
274
+ complete: () => isComplete,
275
+ winner: () => {
276
+ // Determine winner based on game score if set is complete
277
+ if (isComplete) {
278
+ const games = lastEpisode?.game?.games || [0, 0];
279
+ if (games[0] > games[1])
280
+ return 0;
281
+ if (games[1] > games[0])
282
+ return 1;
283
+ }
284
+ return undefined;
285
+ },
286
+ metadata: {
287
+ players: () => {
288
+ const name0 = participantNames[0];
289
+ const name1 = participantNames[1];
290
+ const parts0 = name0.split(' ');
291
+ const parts1 = name1.split(' ');
292
+ return [
293
+ {
294
+ index: 0,
295
+ firstName: parts0[0] || name0,
296
+ lastName: parts0.slice(1).join(' ') || '0',
297
+ participantName: name0,
298
+ },
299
+ {
300
+ index: 1,
301
+ firstName: parts1[0] || name1,
302
+ lastName: parts1.slice(1).join(' ') || '1',
303
+ participantName: name1,
304
+ },
305
+ ];
306
+ },
307
+ },
308
+ scoreboard: (_perspective) => {
309
+ const games = lastEpisode?.game?.games || [0, 0];
310
+ return `${games[0]}-${games[1]}`;
311
+ },
312
+ };
313
+ });
314
+ },
315
+ history: {
316
+ points: () => matchObjectOrEpisodes.map((ep) => ep.point),
317
+ action: (actionType) => (actionType === 'addPoint' ? matchObjectOrEpisodes : []),
318
+ },
319
+ };
320
+ }
321
+ else {
322
+ // UMO object - use as-is
323
+ match_data = matchObjectOrEpisodes;
324
+ }
325
+ // Only call update if chart has been initialized (dom_parent exists)
326
+ if (dom_parent) {
327
+ chart.update();
328
+ }
329
+ return chart;
330
+ };
331
+ chart.players = function (names) {
332
+ participantNames = names;
333
+ return chart;
334
+ };
335
+ chart.matchUp = function (matchUpState, names) {
336
+ if (names)
337
+ participantNames = names;
338
+ const episodes = buildEpisodes(matchUpState);
339
+ chart.data(episodes);
340
+ return chart;
341
+ };
342
+ return chart;
343
+ }
344
+ function ptsChart() {
345
+ let set_data;
346
+ let game_data;
347
+ let pointsToSet;
348
+ const winners = new Set(['Ace', 'Winner', 'Serve Winner']);
349
+ const errors = new Set(['Forced Error', 'Unforced Error', 'Double Fault', 'Penalty', 'Out', 'Net']);
350
+ const options = {
351
+ id: generateId(),
352
+ setIndex: 0,
353
+ class: 'ptsChart',
354
+ resize: true,
355
+ width: 600,
356
+ height: 80,
357
+ margins: {
358
+ top: 5,
359
+ right: 15,
360
+ bottom: 5,
361
+ left: 5,
362
+ },
363
+ set: {
364
+ averagePoints: 56,
365
+ },
366
+ lines: {
367
+ width: 2,
368
+ interpolation: 'linear',
369
+ },
370
+ points: {
371
+ maxWidthPoints: 100,
372
+ },
373
+ score: {
374
+ font: 'Arial',
375
+ fontSize: '12px',
376
+ fontWeight: 'bold',
377
+ reverse: true,
378
+ },
379
+ header: {
380
+ font: 'Arial',
381
+ fontSize: '14px',
382
+ fontWeight: 'bold',
383
+ },
384
+ display: {
385
+ transitionTime: 0,
386
+ pointHighlighting: true,
387
+ pointOpacity: 0.4,
388
+ winErrHighlight: true,
389
+ gameHighlighting: true,
390
+ gameOpacity: 0.2,
391
+ gameBoundaries: false,
392
+ gamepoints: false,
393
+ score: true,
394
+ points: true,
395
+ winner: true,
396
+ },
397
+ colors: {
398
+ orientation: 'yellow',
399
+ gamepoints: 'black',
400
+ players: { 0: 'blue', 1: 'purple' },
401
+ },
402
+ };
403
+ // functions which should be accessible via ACCESSORS
404
+ let update;
405
+ // programmatic
406
+ let dom_parent;
407
+ // DEFINABLE EVENTS
408
+ // Define with ACCESSOR function chart.events()
409
+ const events = {
410
+ setBox: { mouseover: null, mouseout: null },
411
+ update: { begin: null, end: null },
412
+ pointBars: { mouseover: null, mouseout: null, click: null },
413
+ };
414
+ function chart(selection) {
415
+ selection.each(function (_, i, n) {
416
+ dom_parent = select(n[i]);
417
+ // append svg
418
+ const root = dom_parent
419
+ .append('svg')
420
+ .attr('class', options.class + 'root')
421
+ .style('width', options.width + 'px')
422
+ .style('height', options.height + 'px');
423
+ // append children g
424
+ const pts = root
425
+ .append('g')
426
+ .attr('class', options.class + 'pts')
427
+ .attr('transform', 'translate(5, 5)');
428
+ // For Point Bars which must always be on top
429
+ const ptsHover = root
430
+ .append('g')
431
+ .attr('class', options.class + 'pts')
432
+ .attr('transform', 'translate(5, 5)');
433
+ // append labels
434
+ const set_winner = pts
435
+ .append('text')
436
+ .attr('class', options.class + 'Header')
437
+ .attr('opacity', 0)
438
+ .attr('font-size', options.header.fontSize)
439
+ .attr('font-weight', options.header.fontWeight)
440
+ .attr('x', function () {
441
+ return options.margins.left + 'px';
442
+ })
443
+ .attr('y', function () {
444
+ return options.margins.top + 8 + 'px';
445
+ });
446
+ const set_score = pts
447
+ .append('text')
448
+ .attr('class', options.class + 'Score')
449
+ .attr('opacity', 0)
450
+ .attr('font-size', options.score.fontSize)
451
+ .attr('font-weight', options.score.fontWeight)
452
+ .attr('x', function () {
453
+ return options.margins.left + 'px';
454
+ })
455
+ .attr('y', function () {
456
+ return options.margins.top + 20 + 'px';
457
+ });
458
+ // resize used to disable transitions during resize operation
459
+ update = function (_, resize) {
460
+ if (!set_data) {
461
+ return false;
462
+ }
463
+ root
464
+ .transition()
465
+ .duration(options.display.transitionTime)
466
+ .style('width', options.width + 'px')
467
+ .style('height', options.height + 'px');
468
+ const allActionPoints = set_data.history.action('addPoint');
469
+ const points = allActionPoints.filter((f) => f.point.set == options.setIndex);
470
+ if (!points || points.length === 0) {
471
+ return false;
472
+ }
473
+ const range_start = points[0].point.index;
474
+ game_data = groupGames(points);
475
+ pointsToSet = points.map((p) => p.needed?.pointsToSet);
476
+ const pts_max = Math.max(...[].concat(pointsToSet.map((p) => p[0]), pointsToSet.map((p) => p[1])));
477
+ const pts_start = Math.max(...pointsToSet[0]);
478
+ // add pts prior to first point
479
+ pointsToSet.unshift([pts_start, pts_start]);
480
+ const longest_rally = Math.max.apply(null, points.map((m) => {
481
+ // Prefer rallyLength if available, fallback to rally
482
+ const rallyValue = m.point.rallyLength || (m.point.rally ? rallyCount(m.point.rally) : 0);
483
+ return rallyValue;
484
+ })) + 2;
485
+ displayScore(resize);
486
+ const xScale = scaleLinear()
487
+ .domain([0, calcWidth()])
488
+ .range([0, options.width - (options.margins.left + options.margins.right)]);
489
+ function pointScale(d, r, a) {
490
+ if (d.range[r] < range_start)
491
+ return xScale(d.range[r] + a);
492
+ return xScale(d.range[r] + a - range_start);
493
+ }
494
+ const yScale = scaleLinear()
495
+ .range([options.height - (options.margins.top + options.margins.bottom), options.margins.bottom])
496
+ .domain([-2, pts_max - 1]);
497
+ // Set Box
498
+ pts.selectAll('.' + options.class + 'SetBox').data([options.id]) // # of list elements only used for index, data not important
499
+ .join(enter => enter
500
+ .append('rect')
501
+ .attr('class', options.class + 'SetBox')
502
+ .style('position', 'relative')
503
+ .attr('stroke', 'black')
504
+ .attr('stroke-width', 1)
505
+ .attr('fill', 'none')
506
+ .on('mouseover', (d, i) => {
507
+ if (events.setBox.mouseover)
508
+ events.setBox.mouseover(d, i);
509
+ })
510
+ .on('mouseout', (d, i) => {
511
+ if (events.setBox.mouseout)
512
+ events.setBox.mouseout(d, i);
513
+ }), update => update, exit => exit
514
+ .transition()
515
+ .duration(resize ? 0 : options.display.transitionTime)
516
+ .style('opacity', 0)
517
+ .remove())
518
+ .transition()
519
+ .duration(resize ? 0 : options.display.transitionTime)
520
+ .attr('height', () => {
521
+ return options.height - (options.margins.top + options.margins.bottom);
522
+ })
523
+ .attr('width', () => {
524
+ return xScale(boxWidth() + 1);
525
+ });
526
+ // Game Boundaries
527
+ pts.selectAll('.' + options.class + 'GameBoundary').data(game_data)
528
+ .join(enter => enter.append('rect').attr('class', options.class + 'GameBoundary'))
529
+ .attr('id', function (_, i) {
530
+ return options.class + options.id + 'boundary' + i;
531
+ })
532
+ .transition()
533
+ .duration(resize ? 0 : options.display.transitionTime)
534
+ .attr('opacity', function () {
535
+ return options.display.gameBoundaries ? 0.02 : 0;
536
+ })
537
+ .attr('transform', function (d) {
538
+ return 'translate(' + pointScale(d, 0, 0) + ', 0)';
539
+ })
540
+ .attr('height', yScale(-2))
541
+ .attr('width', function (d) {
542
+ return pointScale(d, 1, 1) - pointScale(d, 0, 0);
543
+ })
544
+ .attr('stroke', 'black')
545
+ .attr('stroke-width', 1)
546
+ .attr('fill', 'none');
547
+ // Game Boxes
548
+ pts.selectAll('.' + options.class + 'Game').data(game_data)
549
+ .join(enter => enter.append('rect').attr('class', options.class + 'Game'))
550
+ .attr('id', (_, i) => {
551
+ return options.class + options.id + 'game' + i;
552
+ })
553
+ .transition()
554
+ .duration(resize ? 0 : options.display.transitionTime)
555
+ .attr('opacity', () => {
556
+ return options.display.gameBoundaries ? 0.02 : 0;
557
+ })
558
+ .attr('transform', (d) => {
559
+ return 'translate(' + pointScale(d, 0, 0) + ', 0)';
560
+ })
561
+ .attr('height', yScale(-2))
562
+ .attr('width', (d) => {
563
+ return pointScale(d, 1, 1) - pointScale(d, 0, 0);
564
+ })
565
+ .attr('stroke', (d) => {
566
+ return options.colors.players[d.winner];
567
+ })
568
+ .attr('stroke-width', 1)
569
+ .attr('fill', (d) => {
570
+ return d.winner === undefined ? 'none' : options.colors.players[d.winner];
571
+ });
572
+ // Player PTS Lines
573
+ const lineGen = line()
574
+ .x((_, i) => {
575
+ return xScale(i);
576
+ })
577
+ .y((d) => {
578
+ return yScale(pts_max - d);
579
+ });
580
+ pts.selectAll('.' + options.class + 'Line').data([0, 1])
581
+ .join(enter => enter
582
+ .append('path')
583
+ .attr('class', options.class + 'Line')
584
+ .attr('fill', 'none'))
585
+ .attr('id', (d) => {
586
+ return options.class + options.id + 'player' + d + 'Line';
587
+ })
588
+ .transition()
589
+ .duration(resize ? 0 : options.display.transitionTime / 2)
590
+ .style('opacity', 0.1)
591
+ .transition()
592
+ .duration(resize ? 0 : options.display.transitionTime / 2)
593
+ .style('opacity', 1)
594
+ .attr('stroke', (d) => {
595
+ return options.colors.players[d];
596
+ })
597
+ .attr('stroke-width', () => {
598
+ return options.lines.width;
599
+ })
600
+ // .attr('d', function(d: any) { return lineGen(player_data[d]) })
601
+ .attr('d', (d) => {
602
+ return lineGen(pointsToSet.map((p) => p[d]));
603
+ });
604
+ const bp_data = [
605
+ pointsToSet.map((p) => {
606
+ return { pts: p[0] };
607
+ }),
608
+ pointsToSet.map((p) => {
609
+ return { pts: p[1] };
610
+ }),
611
+ ];
612
+ const bp_wrappers = pts.selectAll('.' + options.class + 'BPWrapper').data(bp_data)
613
+ .join(enter => enter.append('g').attr('class', options.class + 'BPWrapper'));
614
+ bp_wrappers.selectAll('.' + options.class + 'Breakpoint').data((d, i) => {
615
+ return add_index(d, i);
616
+ })
617
+ .join(enter => enter
618
+ .append('circle')
619
+ .attr('class', options.class + 'Breakpoint')
620
+ .attr('opacity', '0'), update => update, exit => exit.attr('opacity', '0').remove())
621
+ .transition()
622
+ .duration(resize ? 0 : options.display.transitionTime / 2)
623
+ .style('opacity', 0)
624
+ .transition()
625
+ .duration(resize ? 0 : options.display.transitionTime / 2)
626
+ .attr('fill', (d, i) => {
627
+ if (points[i - 1] && points[i - 1].point.isBreakpoint != undefined) {
628
+ return options.colors.players[d._i];
629
+ }
630
+ })
631
+ .style('opacity', (d, i) => {
632
+ if (points[i - 1] && points[i - 1].point.isBreakpoint != undefined) {
633
+ // return points[i - 1].point.isBreakpoint == d._i ? 1 : 0
634
+ return points[i - 1].point.server == 1 - d._i ? 1 : 0;
635
+ }
636
+ })
637
+ .attr('cx', (_, i) => {
638
+ return xScale(i);
639
+ })
640
+ .attr('cy', (d) => {
641
+ return yScale(pts_max - d.pts);
642
+ })
643
+ .attr('r', 2);
644
+ const points_index = range(points.length);
645
+ const barsX = scaleBand()
646
+ .domain(points_index)
647
+ .range([0, xScale(points.length)])
648
+ .round(true);
649
+ const bX = scaleLinear()
650
+ .domain([0, points.length])
651
+ .range([0, xScale(points.length)]);
652
+ // gradients cause hover errors when data is replaced
653
+ pts.selectAll('.gradient' + options.id).remove();
654
+ const gradients = pts.selectAll('.gradient' + options.id).data(range(points.length)) // data not important, only length of array
655
+ .join(enter => enter
656
+ .append('linearGradient')
657
+ .attr('id', (_, i) => {
658
+ return 'gradient' + options.id + i;
659
+ })
660
+ .attr('class', () => {
661
+ return 'gradient' + options.id;
662
+ })
663
+ .attr('gradientUnits', 'userSpaceOnUse')
664
+ .attr('x1', () => {
665
+ return barsX.bandwidth() / 2;
666
+ })
667
+ .attr('y1', () => {
668
+ return 0;
669
+ })
670
+ .attr('x2', () => {
671
+ return barsX.bandwidth() / 2;
672
+ })
673
+ .attr('y2', () => {
674
+ return yScale(-2);
675
+ }))
676
+ .attr('transform', (_, i) => {
677
+ return 'translate(' + bX(i) + ', 0)';
678
+ });
679
+ gradients.selectAll('.points_stop').data((d) => {
680
+ return calcStops(points[d].point);
681
+ })
682
+ .join(enter => enter.append('stop').attr('class', 'points_stop'))
683
+ .attr('offset', (d) => {
684
+ return d.offset;
685
+ })
686
+ .attr('stop-color', (d) => {
687
+ return d.color;
688
+ });
689
+ // Pre-compute point-index → game-index lookup for O(1) hover
690
+ const pointToGame = {};
691
+ game_data.forEach((g, gi) => {
692
+ g.points.forEach((p) => { pointToGame[p.index] = gi; });
693
+ });
694
+ ptsHover.selectAll('.' + options.class + 'Bar').data(range(points.length)) // data not important, only length of array
695
+ .join(enter => enter
696
+ .append('line')
697
+ .attr('class', options.class + 'Bar')
698
+ .attr('opacity', '0')
699
+ .style('pointer-events', 'all')
700
+ .on('mouseover', function (event, d) {
701
+ const i = d; // In D3 v7, d is the datum (index in this case)
702
+ if (options.display.pointHighlighting) {
703
+ select(this).attr('opacity', options.display.pointOpacity);
704
+ }
705
+ if (options.display.gameHighlighting && points[i]) {
706
+ const gameIndex = pointToGame[points[i].point.index];
707
+ if (gameIndex >= 0) {
708
+ pts.select('[id="' + options.class + options.id + 'game' + gameIndex + '"]').attr('opacity', options.display.gameOpacity);
709
+ }
710
+ }
711
+ if (events.pointBars.mouseover) {
712
+ events.pointBars.mouseover(points[i], i);
713
+ }
714
+ if (i == 0) {
715
+ ptsHover.selectAll('.' + options.class + 'Bar').attr('opacity', options.display.pointOpacity);
716
+ }
717
+ highlightScore(i);
718
+ })
719
+ .on('mouseout', function (event, d) {
720
+ const i = d;
721
+ ptsHover.selectAll('.' + options.class + 'Bar').attr('opacity', 0);
722
+ pts.selectAll('.' + options.class + 'Game').attr('opacity', '0');
723
+ if (events.pointBars.mouseout) {
724
+ events.pointBars.mouseout(points[i], i);
725
+ }
726
+ displayScore();
727
+ })
728
+ .on('click', function (event, d) {
729
+ const i = d;
730
+ if (events.pointBars.click) {
731
+ events.pointBars.click(points[d], i, n[i]);
732
+ }
733
+ }), update => update, exit => exit
734
+ .transition()
735
+ .duration(resize ? 0 : options.display.transitionTime)
736
+ .attr('opacity', '0')
737
+ .remove())
738
+ .attr('opacity', () => {
739
+ const opacity = options.display.winErrHighlight ? '.4' : '0';
740
+ return opacity;
741
+ })
742
+ .attr('transform', (_, i) => {
743
+ return 'translate(' + bX(i) + ', 0)';
744
+ })
745
+ .attr('x1', () => {
746
+ return barsX.bandwidth() / 2;
747
+ })
748
+ .attr('y1', () => {
749
+ const y1 = 0;
750
+ return y1;
751
+ })
752
+ .attr('x2', () => {
753
+ return barsX.bandwidth() / 2;
754
+ })
755
+ .attr('y2', () => {
756
+ const y2 = yScale(-2);
757
+ return y2;
758
+ })
759
+ .attr('stroke-width', () => {
760
+ const width = barsX.bandwidth();
761
+ return width;
762
+ })
763
+ .attr('stroke', (_, i) => {
764
+ return 'url(#gradient' + options.id + i + ')';
765
+ })
766
+ .attr('uid', (_, i) => {
767
+ return 'point' + i;
768
+ });
769
+ function displayScore(resize) {
770
+ const winner = set_data.winner();
771
+ const players = set_data.metadata.players();
772
+ function lastName(name) {
773
+ const split = name.split(' ');
774
+ return split[split.length - 1];
775
+ }
776
+ const legend = winner === undefined
777
+ ? `${lastName(players[0].participantName)}/${lastName(players[1].participantName)}`
778
+ : players[winner].participantName;
779
+ set_winner
780
+ .transition()
781
+ .duration(resize ? 0 : options.display.transitionTime)
782
+ .attr('opacity', 1)
783
+ .attr('fill', winner === undefined ? 'black' : options.colors.players[winner])
784
+ .text(legend);
785
+ const game_score = set_data.scoreboard(winner);
786
+ set_score
787
+ .transition()
788
+ .duration(resize ? 0 : options.display.transitionTime)
789
+ .attr('opacity', 1)
790
+ .attr('fill', winner === undefined ? 'black' : options.colors.players[winner])
791
+ .text(game_score);
792
+ }
793
+ function highlightScore(i) {
794
+ const point = points[i]?.point;
795
+ if (!point)
796
+ return;
797
+ const result = point.result || '';
798
+ const rallyLength = point.rallyLength || 0;
799
+ const label = `${result}${rallyLength ? ' (Rally: ' + rallyLength + ')' : ''}`;
800
+ set_winner
801
+ .attr('opacity', 1)
802
+ .attr('fill', options.colors.players[point.winner])
803
+ .text(label);
804
+ set_score
805
+ .attr('opacity', 1)
806
+ .attr('fill', options.colors.players[point.winner])
807
+ .text(point.score || '');
808
+ }
809
+ function calcStops(point) {
810
+ let win_pct = 0;
811
+ let err_pct = 0;
812
+ let u_pct = 0;
813
+ if (options.display.winErrHighlight) {
814
+ const result = point.result;
815
+ const rallyLength = point.rallyLength || (point.rally ? rallyCount(point.rally) : 0);
816
+ const rally_pct = rallyLength ? 100 - Math.floor((rallyLength / longest_rally) * 100) : 100;
817
+ if (winners.has(result)) {
818
+ win_pct = rally_pct;
819
+ }
820
+ else if (errors.has(result)) {
821
+ err_pct = rally_pct;
822
+ }
823
+ else {
824
+ u_pct = rally_pct;
825
+ }
826
+ }
827
+ return [
828
+ { offset: '0%', color: 'blue' },
829
+ { offset: u_pct + '%', color: 'blue' },
830
+ { offset: u_pct + '%', color: 'green' },
831
+ { offset: u_pct + win_pct + '%', color: 'green' },
832
+ { offset: u_pct + win_pct + '%', color: 'red' },
833
+ { offset: u_pct + win_pct + err_pct + '%', color: 'red' },
834
+ { offset: u_pct + win_pct + err_pct + '%', color: options.colors.orientation },
835
+ { offset: '100%', color: options.colors.orientation },
836
+ ];
837
+ }
838
+ };
839
+ });
840
+ }
841
+ // REUSABLE functions
842
+ // ------------------
843
+ function boxWidth() {
844
+ const dl = set_data.history.points().filter((f) => f.set == options.setIndex).length - 1;
845
+ return set_data.complete() ? dl : Math.max(dl, options.set.averagePoints);
846
+ }
847
+ function calcWidth() {
848
+ const dl = set_data.history.points().filter((f) => f.set == options.setIndex).length - 1;
849
+ return Math.max(dl, options.points.maxWidthPoints, options.set.averagePoints);
850
+ }
851
+ // ACCESSORS
852
+ // allows updating individual options and suboptions
853
+ // while preserving state of other options
854
+ chart.options = function (values) {
855
+ if (!arguments.length)
856
+ return options;
857
+ const vKeys = Object.keys(values);
858
+ const oKeys = Object.keys(options);
859
+ for (const vKey of vKeys) {
860
+ if (oKeys.includes(vKey)) {
861
+ if (typeof options[vKey] == 'object') {
862
+ const sKeys = Object.keys(values[vKey]);
863
+ const osKeys = Object.keys(options[vKey]);
864
+ for (const sKey of sKeys) {
865
+ if (osKeys.includes(sKey)) {
866
+ options[vKey][sKey] = values[vKey][sKey];
867
+ }
868
+ }
869
+ }
870
+ else {
871
+ options[vKey] = values[vKey];
872
+ }
873
+ }
874
+ }
875
+ return chart;
876
+ };
877
+ chart.data = function (set_object) {
878
+ if (!arguments.length)
879
+ return set_data;
880
+ set_data = set_object;
881
+ };
882
+ chart.events = function (functions) {
883
+ if (!arguments.length)
884
+ return events;
885
+ keyWalk(functions, events);
886
+ return chart;
887
+ };
888
+ chart.colors = function (colores) {
889
+ if (!arguments.length)
890
+ return options.colors;
891
+ options.colors.players = colores;
892
+ return chart;
893
+ };
894
+ chart.width = function (value) {
895
+ if (!arguments.length)
896
+ return options.width;
897
+ options.width = value;
898
+ return chart;
899
+ };
900
+ chart.height = function (value) {
901
+ if (!arguments.length)
902
+ return options.height;
903
+ options.height = value;
904
+ return chart;
905
+ };
906
+ chart.update = function (opts) {
907
+ if (events.update.begin)
908
+ events.update.begin();
909
+ if (typeof update === 'function')
910
+ update(opts);
911
+ setTimeout(function () {
912
+ if (events.update.end)
913
+ events.update.end();
914
+ }, options.display.transitionTime);
915
+ return true;
916
+ };
917
+ chart.duration = function (value) {
918
+ if (!arguments.length)
919
+ return options.display.transitionTime;
920
+ options.display.transitionTime = value;
921
+ return chart;
922
+ };
923
+ return chart;
924
+ }
925
+ //# sourceMappingURL=ptsChart.js.map