@johnmorrisdotca/kyogijo-stadiums 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +129 -0
- package/LICENSE +21 -0
- package/NOTICE.md +293 -0
- package/README.md +600 -2
- package/dist/annotate.d.ts +47 -0
- package/dist/annotate.js +349 -0
- package/dist/atlas-entry.d.ts +4 -0
- package/dist/atlas-entry.js +3 -0
- package/dist/atlas.d.ts +131 -0
- package/dist/atlas.js +149 -0
- package/dist/background.d.ts +56 -0
- package/dist/background.js +127 -0
- package/dist/ballpark.d.ts +48 -0
- package/dist/ballpark.js +129 -0
- package/dist/canvas-entry.d.ts +2 -0
- package/dist/canvas-entry.js +2 -0
- package/dist/canvas.d.ts +85 -0
- package/dist/canvas.js +279 -0
- package/dist/charts-entry.d.ts +10 -0
- package/dist/charts-entry.js +8 -0
- package/dist/charts.baseball.d.ts +30 -0
- package/dist/charts.baseball.js +152 -0
- package/dist/charts.curling.d.ts +52 -0
- package/dist/charts.curling.js +196 -0
- package/dist/charts.d.ts +172 -0
- package/dist/charts.js +508 -0
- package/dist/charts.network.d.ts +66 -0
- package/dist/charts.network.js +64 -0
- package/dist/charts.soccer.d.ts +43 -0
- package/dist/charts.soccer.js +191 -0
- package/dist/charts.types.d.ts +396 -0
- package/dist/charts.types.js +1 -0
- package/dist/colour.d.ts +29 -0
- package/dist/colour.js +97 -0
- package/dist/common.d.ts +14 -0
- package/dist/common.js +32 -0
- package/dist/court.d.ts +89 -0
- package/dist/court.js +248 -0
- package/dist/courts.style.d.ts +20 -0
- package/dist/courts.style.js +54 -0
- package/dist/cricket.board.d.ts +18 -0
- package/dist/cricket.board.js +67 -0
- package/dist/curling.board.d.ts +24 -0
- package/dist/curling.board.js +147 -0
- package/dist/d3-entry.d.ts +3 -0
- package/dist/d3-entry.js +2 -0
- package/dist/d3.d.ts +45 -0
- package/dist/d3.js +56 -0
- package/dist/draw.d.ts +52 -0
- package/dist/draw.js +705 -0
- package/dist/element-define.d.ts +2 -0
- package/dist/element-define.js +4 -0
- package/dist/element-entry.d.ts +1 -0
- package/dist/element-entry.js +2 -0
- package/dist/element.d.ts +39 -0
- package/dist/element.js +321 -0
- package/dist/eras-entry.d.ts +2 -0
- package/dist/eras-entry.js +2 -0
- package/dist/eras.d.ts +204 -0
- package/dist/eras.js +396 -0
- package/dist/events-entry.d.ts +3 -0
- package/dist/events-entry.js +3 -0
- package/dist/events.d.ts +54 -0
- package/dist/events.data.d.ts +59 -0
- package/dist/events.data.js +297 -0
- package/dist/events.js +127 -0
- package/dist/features.d.ts +31 -0
- package/dist/features.js +284 -0
- package/dist/field.score.d.ts +5 -0
- package/dist/field.score.js +33 -0
- package/dist/fields/afl.d.ts +11 -0
- package/dist/fields/afl.js +103 -0
- package/dist/fields/cricket.d.ts +9 -0
- package/dist/fields/cricket.js +81 -0
- package/dist/fields/fieldhockey.d.ts +6 -0
- package/dist/fields/fieldhockey.js +81 -0
- package/dist/fields/gaelic.d.ts +10 -0
- package/dist/fields/gaelic.js +86 -0
- package/dist/fields/handball.d.ts +6 -0
- package/dist/fields/handball.js +85 -0
- package/dist/fields/lacrosse.d.ts +17 -0
- package/dist/fields/lacrosse.js +169 -0
- package/dist/fields/rugby.d.ts +10 -0
- package/dist/fields/rugby.js +130 -0
- package/dist/fields.d.ts +12 -0
- package/dist/fields.js +44 -0
- package/dist/fields.kit.d.ts +63 -0
- package/dist/fields.kit.js +108 -0
- package/dist/fields.style.d.ts +4 -0
- package/dist/fields.style.js +19 -0
- package/dist/fields.types.d.ts +155 -0
- package/dist/fields.types.js +1 -0
- package/dist/format.d.ts +10 -0
- package/dist/format.js +28 -0
- package/dist/formations.d.ts +18 -0
- package/dist/formations.js +211 -0
- package/dist/frames-entry.d.ts +8 -0
- package/dist/frames-entry.js +7 -0
- package/dist/frames.d.ts +199 -0
- package/dist/frames.js +271 -0
- package/dist/frames.providers.d.ts +16 -0
- package/dist/frames.providers.js +302 -0
- package/dist/geojson-entry.d.ts +2 -0
- package/dist/geojson-entry.js +2 -0
- package/dist/geojson.d.ts +79 -0
- package/dist/geojson.js +147 -0
- package/dist/geometry-entry.d.ts +7 -0
- package/dist/geometry-entry.js +5 -0
- package/dist/geometry.d.ts +295 -0
- package/dist/geometry.js +361 -0
- package/dist/grounds-entry.d.ts +4 -0
- package/dist/grounds-entry.js +3 -0
- package/dist/grounds.d.ts +134 -0
- package/dist/grounds.data.d.ts +2 -0
- package/dist/grounds.data.js +1849 -0
- package/dist/grounds.js +74 -0
- package/dist/heat.colour.d.ts +37 -0
- package/dist/heat.colour.js +54 -0
- package/dist/heat.d.ts +43 -0
- package/dist/heat.js +614 -0
- package/dist/heat.stats.d.ts +123 -0
- package/dist/heat.stats.js +413 -0
- package/dist/heat.types.d.ts +145 -0
- package/dist/heat.types.js +1 -0
- package/dist/heat.zones.d.ts +54 -0
- package/dist/heat.zones.js +424 -0
- package/dist/homeside.d.ts +16 -0
- package/dist/homeside.js +43 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +29 -0
- package/dist/layers-entry.d.ts +19 -0
- package/dist/layers-entry.js +11 -0
- package/dist/layers.d.ts +41 -0
- package/dist/layers.env.d.ts +72 -0
- package/dist/layers.env.js +79 -0
- package/dist/layers.js +533 -0
- package/dist/layers.marks.d.ts +45 -0
- package/dist/layers.marks.js +168 -0
- package/dist/layers.types.d.ts +226 -0
- package/dist/layers.types.js +1 -0
- package/dist/layout.d.ts +97 -0
- package/dist/layout.js +381 -0
- package/dist/layout.style.d.ts +6 -0
- package/dist/layout.style.js +17 -0
- package/dist/layouts-entry.d.ts +5 -0
- package/dist/layouts-entry.js +4 -0
- package/dist/leagues/aussie.d.ts +57 -0
- package/dist/leagues/aussie.js +36 -0
- package/dist/leagues/baseball.d.ts +58 -0
- package/dist/leagues/baseball.js +171 -0
- package/dist/leagues/basketball.d.ts +67 -0
- package/dist/leagues/basketball.js +128 -0
- package/dist/leagues/courts.d.ts +212 -0
- package/dist/leagues/courts.js +114 -0
- package/dist/leagues/cricket.d.ts +45 -0
- package/dist/leagues/cricket.js +34 -0
- package/dist/leagues/curling.d.ts +64 -0
- package/dist/leagues/curling.js +53 -0
- package/dist/leagues/fieldhockey.d.ts +48 -0
- package/dist/leagues/fieldhockey.js +29 -0
- package/dist/leagues/fields.d.ts +10 -0
- package/dist/leagues/fields.js +6 -0
- package/dist/leagues/football.d.ts +8 -0
- package/dist/leagues/football.js +82 -0
- package/dist/leagues/gaelic.d.ts +50 -0
- package/dist/leagues/gaelic.js +29 -0
- package/dist/leagues/handball.d.ts +45 -0
- package/dist/leagues/handball.js +32 -0
- package/dist/leagues/hockey.d.ts +63 -0
- package/dist/leagues/hockey.js +166 -0
- package/dist/leagues/lacrosse.d.ts +103 -0
- package/dist/leagues/lacrosse.js +175 -0
- package/dist/leagues/rugby.d.ts +59 -0
- package/dist/leagues/rugby.js +72 -0
- package/dist/leagues/sources.d.ts +18 -0
- package/dist/leagues/sources.js +95 -0
- package/dist/leagues/types.d.ts +43 -0
- package/dist/leagues/types.js +5 -0
- package/dist/leagues-entry.d.ts +23 -0
- package/dist/leagues-entry.js +15 -0
- package/dist/leagues.d.ts +172 -0
- package/dist/leagues.js +462 -0
- package/dist/look.d.ts +50 -0
- package/dist/look.js +186 -0
- package/dist/mark.d.ts +35 -0
- package/dist/mark.js +59 -0
- package/dist/mount-entry.d.ts +2 -0
- package/dist/mount-entry.js +2 -0
- package/dist/mount.d.ts +63 -0
- package/dist/mount.js +167 -0
- package/dist/mow.d.ts +36 -0
- package/dist/mow.js +111 -0
- package/dist/names.d.ts +77 -0
- package/dist/names.js +97 -0
- package/dist/outline.d.ts +25 -0
- package/dist/outline.js +112 -0
- package/dist/parks-entry.d.ts +4 -0
- package/dist/parks-entry.js +3 -0
- package/dist/parks.d.ts +200 -0
- package/dist/parks.data.d.ts +2 -0
- package/dist/parks.data.js +4489 -0
- package/dist/parks.js +92 -0
- package/dist/plot-entry.d.ts +2 -0
- package/dist/plot-entry.js +2 -0
- package/dist/plot.d.ts +74 -0
- package/dist/plot.js +68 -0
- package/dist/png-entry.d.ts +2 -0
- package/dist/png-entry.js +2 -0
- package/dist/png.d.ts +26 -0
- package/dist/png.js +46 -0
- package/dist/ranges.d.ts +33 -0
- package/dist/ranges.js +178 -0
- package/dist/ranges.names.d.ts +31 -0
- package/dist/ranges.names.js +65 -0
- package/dist/react-entry.d.ts +2 -0
- package/dist/react-entry.js +2 -0
- package/dist/react.d.ts +53 -0
- package/dist/react.js +58 -0
- package/dist/replay-entry.d.ts +5 -0
- package/dist/replay-entry.js +4 -0
- package/dist/replay.control.d.ts +47 -0
- package/dist/replay.control.js +184 -0
- package/dist/replay.d.ts +170 -0
- package/dist/replay.dom.d.ts +21 -0
- package/dist/replay.dom.js +388 -0
- package/dist/replay.js +579 -0
- package/dist/replay.types.d.ts +152 -0
- package/dist/replay.types.js +1 -0
- package/dist/restyle.d.ts +21 -0
- package/dist/restyle.js +70 -0
- package/dist/road.d.ts +26 -0
- package/dist/road.draw.d.ts +6 -0
- package/dist/road.draw.js +269 -0
- package/dist/road.js +59 -0
- package/dist/road.model.d.ts +63 -0
- package/dist/road.model.js +215 -0
- package/dist/road.style.d.ts +7 -0
- package/dist/road.style.js +26 -0
- package/dist/road.types.d.ts +122 -0
- package/dist/road.types.js +1 -0
- package/dist/scene-entry.d.ts +7 -0
- package/dist/scene-entry.js +5 -0
- package/dist/scene.css.d.ts +44 -0
- package/dist/scene.css.js +296 -0
- package/dist/scene.d.ts +27 -0
- package/dist/scene.fit.d.ts +12 -0
- package/dist/scene.fit.js +18 -0
- package/dist/scene.flat.d.ts +38 -0
- package/dist/scene.flat.js +167 -0
- package/dist/scene.js +420 -0
- package/dist/scene.path.d.ts +40 -0
- package/dist/scene.path.js +357 -0
- package/dist/scene.types.d.ts +172 -0
- package/dist/scene.types.js +1 -0
- package/dist/scene.xml.d.ts +31 -0
- package/dist/scene.xml.js +81 -0
- package/dist/score.d.ts +70 -0
- package/dist/score.js +212 -0
- package/dist/sets.board.d.ts +22 -0
- package/dist/sets.board.js +89 -0
- package/dist/sheet.d.ts +18 -0
- package/dist/sheet.js +38 -0
- package/dist/special.d.ts +44 -0
- package/dist/special.js +204 -0
- package/dist/special.style.d.ts +8 -0
- package/dist/special.style.js +36 -0
- package/dist/strings.d.ts +581 -0
- package/dist/strings.js +411 -0
- package/dist/style.d.ts +35 -0
- package/dist/style.js +177 -0
- package/dist/surfaces/baseball.d.ts +33 -0
- package/dist/surfaces/baseball.js +208 -0
- package/dist/surfaces/baseball.walls.d.ts +16 -0
- package/dist/surfaces/baseball.walls.js +150 -0
- package/dist/surfaces/basketball.d.ts +8 -0
- package/dist/surfaces/basketball.js +199 -0
- package/dist/surfaces/curling.d.ts +23 -0
- package/dist/surfaces/curling.js +128 -0
- package/dist/surfaces/field.d.ts +12 -0
- package/dist/surfaces/field.js +163 -0
- package/dist/surfaces/football.d.ts +12 -0
- package/dist/surfaces/football.js +208 -0
- package/dist/surfaces/hockey.d.ts +10 -0
- package/dist/surfaces/hockey.js +166 -0
- package/dist/surfaces/net.d.ts +7 -0
- package/dist/surfaces/net.js +151 -0
- package/dist/surfaces/soccer-providers.d.ts +124 -0
- package/dist/surfaces/soccer-providers.js +334 -0
- package/dist/surfaces/soccer-spec.d.ts +138 -0
- package/dist/surfaces/soccer-spec.js +175 -0
- package/dist/surfaces/soccer.d.ts +25 -0
- package/dist/surfaces/soccer.js +270 -0
- package/dist/surfaces/surface.d.ts +180 -0
- package/dist/surfaces/surface.js +49 -0
- package/dist/team.common.d.ts +75 -0
- package/dist/team.common.js +97 -0
- package/dist/team.formation.d.ts +24 -0
- package/dist/team.formation.js +121 -0
- package/dist/team.geometry.d.ts +66 -0
- package/dist/team.geometry.js +304 -0
- package/dist/team.layers.d.ts +16 -0
- package/dist/team.layers.js +32 -0
- package/dist/team.motion.d.ts +9 -0
- package/dist/team.motion.js +325 -0
- package/dist/team.network.d.ts +43 -0
- package/dist/team.network.js +215 -0
- package/dist/team.shape.d.ts +12 -0
- package/dist/team.shape.js +217 -0
- package/dist/team.types.d.ts +189 -0
- package/dist/team.types.js +1 -0
- package/dist/text.d.ts +31 -0
- package/dist/text.js +88 -0
- package/dist/themes.d.ts +14 -0
- package/dist/themes.js +24 -0
- package/dist/types.d.ts +565 -0
- package/dist/types.js +1 -0
- package/dist/units.d.ts +45 -0
- package/dist/units.js +90 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +234 -4
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// The curling chart: the stones of an end on the house, each in its team's colour with its number, the stones that score ringed and the shot rock
|
|
2
|
+
// ringed twice, and the score of the end worked out by the rules (a stone in or touching the house counts when it is nearer the button than every
|
|
3
|
+
// stone of the other team). Written for this package from World Curling's R2, R12 and Curling Canada's Rules 4 and 13; nothing of any other
|
|
4
|
+
// package is used.
|
|
5
|
+
import { CURLING, STONE_DIAMETER } from "./geometry.js";
|
|
6
|
+
import { convertLength } from "./units.js";
|
|
7
|
+
import { sheetFor } from "./sheet.js";
|
|
8
|
+
import { assemble, clean, drawingOptions, keyItems, keyLayers, languageOf, round, wordsOf } from "./charts.js";
|
|
9
|
+
import { fill } from "./strings.js";
|
|
10
|
+
import { outlineOf } from "./outline.js";
|
|
11
|
+
import { isUnit } from "./units.js";
|
|
12
|
+
const R = STONE_DIAMETER / 2;
|
|
13
|
+
const EPS = 1e-6;
|
|
14
|
+
/** The colours of the two teams' stones, the handles' colours of a game: red and yellow. */
|
|
15
|
+
export const CURLING_COLORS = { home: "#cf2e2e", away: "#f2c230" };
|
|
16
|
+
const isNum = (value) => typeof value === "number" && Number.isFinite(value);
|
|
17
|
+
/**
|
|
18
|
+
* Judge the stones of an end by the rules. A stone is out of play, and taken off the sheet, when it touches a sideline, when it does not come to rest
|
|
19
|
+
* wholly beyond the hog line, or when it has wholly crossed the back line. A stone with any part within 6 feet of the button (in or touching the
|
|
20
|
+
* house) can score; the team with the stone nearest the button scores one point for each of its stones that are nearer than every stone of the other
|
|
21
|
+
* team. Stones as near as each other are equal, and equal stones do not count: if the two nearest are of different teams and as near, the end is
|
|
22
|
+
* blank. Places are feet from the button (in `units`, feet by default), x across and y along.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { curlingScore } from "@johnmorrisdotca/kyogijo-stadiums/charts";
|
|
26
|
+
* const end = curlingScore([
|
|
27
|
+
* { x: 0.3, y: 0.2, team: "home" },
|
|
28
|
+
* { x: -1.5, y: 1, team: "home" },
|
|
29
|
+
* { x: 2, y: -2, team: "away" },
|
|
30
|
+
* ]);
|
|
31
|
+
* console.log(end.scoring, end.points, end.shot);
|
|
32
|
+
* // home 2 0
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function curlingScore(stones, options = {}) {
|
|
36
|
+
if (!Array.isArray(stones))
|
|
37
|
+
throw new TypeError("stones is a list of stones");
|
|
38
|
+
const unit = options.units === undefined ? "ft" : options.units;
|
|
39
|
+
if (!isUnit(unit))
|
|
40
|
+
throw new RangeError(`Unknown units "${String(unit)}": use one of ft, in, yd, m, cm, mm`);
|
|
41
|
+
const half = (options.width ?? CURLING.width) / 2;
|
|
42
|
+
const results = [];
|
|
43
|
+
let skipped = 0;
|
|
44
|
+
const near = [];
|
|
45
|
+
stones.forEach((stone, index) => {
|
|
46
|
+
if (typeof stone !== "object" || stone === null || !isNum(stone.x) || !isNum(stone.y)) {
|
|
47
|
+
skipped += 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const x = convertLength(stone.x, unit, "ft");
|
|
51
|
+
const y = convertLength(stone.y, unit, "ft");
|
|
52
|
+
const distance = Math.hypot(x, y);
|
|
53
|
+
const inPlay = Math.abs(x) + R < half && y - R > -CURLING.hogLine && y - R <= CURLING.backLine;
|
|
54
|
+
const inHouse = inPlay && distance - R <= CURLING.rings.twelve + EPS;
|
|
55
|
+
results.push({ index, distance, inPlay, inHouse, counting: false, shot: false });
|
|
56
|
+
if (inHouse)
|
|
57
|
+
near.push({ index, distance, team: typeof stone.team === "string" && stone.team !== "" ? stone.team : "home" });
|
|
58
|
+
});
|
|
59
|
+
near.sort((a, b) => a.distance - b.distance || a.index - b.index);
|
|
60
|
+
let scoring = null;
|
|
61
|
+
let blank = true;
|
|
62
|
+
let shot = null;
|
|
63
|
+
const counting = [];
|
|
64
|
+
if (near.length > 0) {
|
|
65
|
+
const first = near[0];
|
|
66
|
+
const rival = near.find((s) => s.team !== first.team);
|
|
67
|
+
if (rival === undefined || rival.distance - first.distance > EPS) {
|
|
68
|
+
scoring = first.team;
|
|
69
|
+
blank = false;
|
|
70
|
+
shot = first.index;
|
|
71
|
+
for (const s of near)
|
|
72
|
+
if (s.team === first.team && (rival === undefined || rival.distance - s.distance > EPS))
|
|
73
|
+
counting.push(s.index);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const r of results) {
|
|
77
|
+
r.counting = counting.includes(r.index);
|
|
78
|
+
r.shot = r.index === shot;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
stones: results.length,
|
|
82
|
+
inPlay: results.filter((r) => r.inPlay).length,
|
|
83
|
+
out: results.filter((r) => !r.inPlay).length,
|
|
84
|
+
counting: counting.length,
|
|
85
|
+
points: counting.length,
|
|
86
|
+
scoring,
|
|
87
|
+
shot,
|
|
88
|
+
blank,
|
|
89
|
+
skipped,
|
|
90
|
+
results,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const LOOKS = { stone: {}, counting: { hollow: true }, "shot-rock": { hollow: true, scale: 1.3 } };
|
|
94
|
+
/**
|
|
95
|
+
* A chart of the stones of an end on the house: each stone as seen from above (grey granite and its team's handle, with its number), the stones
|
|
96
|
+
* that score ringed and the shot rock, the nearest, ringed twice, and the score of the end under the rules (`curlingScore`). It shows the half of
|
|
97
|
+
* the sheet from the hog line to the back line, with the free guard zone shaded, at the house on the left (the top of a tall drawing) or the right.
|
|
98
|
+
* Stones out of play are taken off, as the rules take them. `layerTable` and `chart.table()` list every stone with its distance from the button.
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { curlingStones } from "@johnmorrisdotca/kyogijo-stadiums/charts";
|
|
102
|
+
* const chart = curlingStones(
|
|
103
|
+
* [
|
|
104
|
+
* { x: 0.4, y: 0.3, team: "home", number: 8 },
|
|
105
|
+
* { x: -1.2, y: -0.8, team: "home", number: 6 },
|
|
106
|
+
* { x: 1.9, y: 1.1, team: "away", number: 7 },
|
|
107
|
+
* { x: 0, y: -12, team: "away", number: 3 },
|
|
108
|
+
* ],
|
|
109
|
+
* { end: 6, hammer: "home", names: { home: "Prairie Ridge", away: "Harbour City" } },
|
|
110
|
+
* );
|
|
111
|
+
* console.log(chart.totals.points, chart.totals.scoring, chart.title);
|
|
112
|
+
* // 2 home Curling stones, End 6
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function curlingStones(stones, options = {}) {
|
|
116
|
+
if (!Array.isArray(stones))
|
|
117
|
+
throw new TypeError("stones is a list of stones");
|
|
118
|
+
const language = languageOf(options);
|
|
119
|
+
const all = wordsOf(language);
|
|
120
|
+
const w = all.charts;
|
|
121
|
+
const house = options.house === "right" ? "right" : "left";
|
|
122
|
+
const title = options.end === undefined ? w.titleCurling : `${w.titleCurling}${all.said.sep}${fill(w.curlingEnd, { n: options.end })}`;
|
|
123
|
+
const surface = options.surface ?? {};
|
|
124
|
+
// The two stone colours and the half of the sheet with the free guard zone, under whatever the caller laid over them.
|
|
125
|
+
const sheet = { show: "half", end: house, guardZone: true, ...(surface.sheet ?? {}) };
|
|
126
|
+
const o = drawingOptions("curling", { ...options, surface: { ...surface, sheet, colors: { layerHome: CURLING_COLORS.home, layerAway: CURLING_COLORS.away, ...(surface.colors ?? {}) } } }, title, { margin: 4.5 });
|
|
127
|
+
const plan = sheetFor(o);
|
|
128
|
+
const totals = curlingScore(stones, { units: options.units, width: plan.spec.width });
|
|
129
|
+
const unit = options.units ?? "ft";
|
|
130
|
+
const feet = (value) => convertLength(value, unit, "ft");
|
|
131
|
+
const name = (team) => (team === "away" ? options.names?.away ?? all.said.away : team === "home" || team === undefined ? options.names?.home ?? all.said.home : team);
|
|
132
|
+
const at = { from: "curling", units: "ft", ...(house === "left" ? { attacking: "left" } : {}) };
|
|
133
|
+
const inPlay = totals.results.filter((r) => r.inPlay);
|
|
134
|
+
const stone = (r) => stones[r.index];
|
|
135
|
+
const handles = inPlay.map((r) => {
|
|
136
|
+
const s = stone(r);
|
|
137
|
+
const notes = [r.shot ? w.curlingNoteShot : undefined, r.counting && !r.shot ? w.curlingNoteCounting : undefined].filter((n) => n !== undefined);
|
|
138
|
+
const base = s.title ?? fill(w.curlingFrom, { name: `${name(s.team)}${isNum(s.number) ? ` ${s.number}` : ""}`, d: round(r.distance, 2) });
|
|
139
|
+
return clean({
|
|
140
|
+
x: feet(s.x),
|
|
141
|
+
y: feet(s.y),
|
|
142
|
+
team: s.team ?? "home",
|
|
143
|
+
outcome: r.shot ? "shot-rock" : r.counting ? "counting" : "stone",
|
|
144
|
+
value: round(r.distance, 2),
|
|
145
|
+
title: s.title ?? `${base}${notes.length === 0 ? "" : `${all.said.sep}${notes.join(all.said.sep)}`}`,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
const ring = (id, factor, cond, label) => {
|
|
149
|
+
const data = inPlay.filter(cond).map((r) => ({ x: feet(stone(r).x), y: feet(stone(r).y), outcome: id }));
|
|
150
|
+
return data.length === 0 ? undefined : { kind: "points", id, name: label, ...at, data, style: { shape: "circle", size: STONE_DIAMETER * factor, fill: "rgba(0,0,0,0)", stroke: "#d99a00" } };
|
|
151
|
+
};
|
|
152
|
+
const layers = [];
|
|
153
|
+
const shotRing = ring("shot-rock", 2, (r) => r.shot, w.curlingShot);
|
|
154
|
+
const countRing = ring("counting", 1.5, (r) => r.counting, w.curlingCounting);
|
|
155
|
+
if (shotRing !== undefined)
|
|
156
|
+
layers.push(shotRing);
|
|
157
|
+
if (countRing !== undefined)
|
|
158
|
+
layers.push(countRing);
|
|
159
|
+
// The granite of each stone, then its handle in the team's colour, which is the layer that carries the data.
|
|
160
|
+
layers.push({ kind: "points", id: "granite", name: w.curlingStone, ...at, data: handles.map((d) => ({ x: d.x, y: d.y })), style: { size: STONE_DIAMETER, fill: "#9aa3a9", stroke: "var(--ky-layer-edge)" } });
|
|
161
|
+
layers.push({
|
|
162
|
+
kind: "points",
|
|
163
|
+
id: "stones",
|
|
164
|
+
name: w.curlingStone,
|
|
165
|
+
title: fill(all.layers.count, { n: inPlay.length, noun: w.curlingStone.toLowerCase() === "stone" ? "stones" : w.curlingStone }),
|
|
166
|
+
...at,
|
|
167
|
+
data: handles,
|
|
168
|
+
style: { size: STONE_DIAMETER * 0.72, outcomes: { counting: {}, "shot-rock": {}, stone: {} } },
|
|
169
|
+
});
|
|
170
|
+
if (options.numbers !== false) {
|
|
171
|
+
const numbers = inPlay.filter((r) => isNum(stone(r).number)).map((r) => ({ x: feet(stone(r).x), y: feet(stone(r).y), text: stone(r).number }));
|
|
172
|
+
if (numbers.length > 0)
|
|
173
|
+
layers.push({ kind: "text", id: "numbers", name: w.curlingStone, ...at, data: numbers, style: { size: STONE_DIAMETER * 0.4, weight: 800 } });
|
|
174
|
+
}
|
|
175
|
+
// The key: a stone, a stone that scores, the shot rock; over the half of the sheet that is shown.
|
|
176
|
+
const entries = [
|
|
177
|
+
{ label: w.curlingStone, outcome: "stone" },
|
|
178
|
+
...(totals.counting > 0 ? [{ label: w.curlingCounting, outcome: "counting" }] : []),
|
|
179
|
+
...(totals.shot !== null ? [{ label: w.curlingShot, outcome: "shot-rock" }] : []),
|
|
180
|
+
];
|
|
181
|
+
if (options.key !== false) {
|
|
182
|
+
const { box } = outlineOf(o);
|
|
183
|
+
const tee = house === "left" ? box.w / 2 - CURLING.teeFromMiddle : box.w / 2 + CURLING.teeFromMiddle;
|
|
184
|
+
const shown = { x: house === "left" ? tee - CURLING.backLine : tee - CURLING.hogLine - 1, w: CURLING.backLine + CURLING.hogLine + 1 };
|
|
185
|
+
layers.push(...keyLayers(o, entries, LOOKS, all, [], shown));
|
|
186
|
+
}
|
|
187
|
+
const teamName = totals.scoring === null ? "" : name(totals.scoring);
|
|
188
|
+
const parts = [
|
|
189
|
+
totals.blank ? fill(w.sumCurlingBlank, { n: totals.inPlay }) : fill(w.sumCurling, { n: totals.inPlay, team: teamName, points: totals.points }),
|
|
190
|
+
totals.out > 0 ? fill(w.curlingOut, { n: totals.out }) : "",
|
|
191
|
+
options.hammer === undefined ? "" : fill(all.said.hammer, { name: name(options.hammer) }),
|
|
192
|
+
];
|
|
193
|
+
const chart = assemble(o, layers, { title, summary: parts.filter((p) => p !== "").join(all.said.gap), totals, tableLayers: ["stones"] });
|
|
194
|
+
chart.key = keyItems(entries, LOOKS);
|
|
195
|
+
return chart;
|
|
196
|
+
}
|
package/dist/charts.d.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { type Frame } from "./frames.ts";
|
|
2
|
+
import { isNum } from "./layers.env.ts";
|
|
3
|
+
import type { Layer, LayerBase, MarkDatum, OutcomeStyle } from "./layers.types.ts";
|
|
4
|
+
import { type KyogijoWords } from "./strings.ts";
|
|
5
|
+
import type { Chart, ChartOptions, KeyItem, NbaShot, NbaShotOptions, NbaTotals, NflPass, NflPassOptions, NflTotals, NhlShot, NhlShotOptions, NhlTotals, ZoneTotal } from "./charts.types.ts";
|
|
6
|
+
import type { Language, Sport, SurfaceOptions } from "./types.ts";
|
|
7
|
+
import type { ZonesLayer } from "./heat.types.ts";
|
|
8
|
+
/** The language of a chart's words. */
|
|
9
|
+
export declare const languageOf: (o: ChartOptions) => Language;
|
|
10
|
+
/** The words of a chart. */
|
|
11
|
+
export declare const wordsOf: (language: Language) => KyogijoWords;
|
|
12
|
+
export { isNum };
|
|
13
|
+
/** An object without the properties that are `undefined`, so that a layer is plain data and the same input gives the same JSON. */
|
|
14
|
+
export declare function clean<T extends object>(value: T): T;
|
|
15
|
+
/** The ground round each surface that a chart draws, in feet: room above the surface for the key. */
|
|
16
|
+
export declare const MARGIN: Readonly<Partial<Record<Sport, number>>>;
|
|
17
|
+
/** A frame by name, or the frame given. */
|
|
18
|
+
export declare const frameOf: (from: Frame | string | undefined, fallback: string) => Frame;
|
|
19
|
+
/** What a layer's `from` is: the frame given, or the name of the one asked for (a name stays a name in the layer's JSON). */
|
|
20
|
+
export declare const fromOf: (given: Frame | string | undefined, frame: Frame) => Frame | string;
|
|
21
|
+
/** The outcome words of a chart and the look of each: the shape, whether it is open, a multiple of the size. */
|
|
22
|
+
export type Look = OutcomeStyle;
|
|
23
|
+
/** One entry of a key: what it says and the outcome whose mark it shows. */
|
|
24
|
+
export interface KeyEntry {
|
|
25
|
+
label: string;
|
|
26
|
+
outcome?: string;
|
|
27
|
+
}
|
|
28
|
+
/** The drawing's options for a chart: the sport, the language, the title and the ground round the surface, then whatever the caller laid over them. */
|
|
29
|
+
export declare function drawingOptions(sport: Sport, options: ChartOptions, title: string, extra?: Partial<SurfaceOptions>): SurfaceOptions;
|
|
30
|
+
/**
|
|
31
|
+
* The key of a chart: one row of marks and their names in the margin above the surface (drawn over the ground round it), as two layers:
|
|
32
|
+
* the marks, in the look the chart's own marks have, and their words. `extra` is more words with no mark (the size of a mark, a total).
|
|
33
|
+
*/
|
|
34
|
+
export declare function keyLayers(o: SurfaceOptions, entries: readonly KeyEntry[], looks: Readonly<Record<string, Look>>, all: KyogijoWords, extra?: readonly string[], at?: KeyPlace): Layer[];
|
|
35
|
+
/** Where a key is put when it is not centred above the whole surface: the part of the surface it is centred on (`x`, `w`) and the height of the row (`y`), in feet. */
|
|
36
|
+
export interface KeyPlace {
|
|
37
|
+
x?: number;
|
|
38
|
+
w?: number;
|
|
39
|
+
y?: number;
|
|
40
|
+
}
|
|
41
|
+
/** The layers of the key are named `key-marks` and `key-text`. */
|
|
42
|
+
export declare const isKey: (layer: Layer) => boolean;
|
|
43
|
+
/** The key of a chart as data, from its entries and looks. */
|
|
44
|
+
export declare function keyItems(entries: readonly KeyEntry[], looks: Readonly<Record<string, Look>>): KeyItem[];
|
|
45
|
+
/** Put a chart together: the options, the layers and the ways to draw them. */
|
|
46
|
+
export declare function assemble<T>(options: SurfaceOptions, layers: Layer[], parts: {
|
|
47
|
+
title: string;
|
|
48
|
+
summary: string;
|
|
49
|
+
totals: T;
|
|
50
|
+
tableLayers?: readonly string[];
|
|
51
|
+
}): Chart<T> & {
|
|
52
|
+
key: KeyItem[];
|
|
53
|
+
};
|
|
54
|
+
/** `shot`s counted by outcome, as the words of the key and in the key's order, for a layer's description: "Made 3, Missed 5." */
|
|
55
|
+
export declare function describeCounts(entries: readonly KeyEntry[], counts: ReadonlyMap<string, number>, words: KyogijoWords): string;
|
|
56
|
+
/** Round to a number of decimal places. */
|
|
57
|
+
export declare const round: (value: number, places?: number) => number;
|
|
58
|
+
/** A percent, one decimal, or null with nothing to divide. */
|
|
59
|
+
export declare const percentOf: (part: number, whole: number) => number | null;
|
|
60
|
+
/** The marks counted into the zones of a layer, as zone totals. */
|
|
61
|
+
export declare function zoneTotals(o: SurfaceOptions, layer: ZonesLayer): ZoneTotal[];
|
|
62
|
+
/** Count marks by outcome, skipping the ones with no place. */
|
|
63
|
+
export declare function countOutcomes(rows: readonly {
|
|
64
|
+
x?: number | null;
|
|
65
|
+
y?: number | null;
|
|
66
|
+
outcome?: string;
|
|
67
|
+
}[]): {
|
|
68
|
+
counts: Map<string, number>;
|
|
69
|
+
placed: number;
|
|
70
|
+
skipped: number;
|
|
71
|
+
};
|
|
72
|
+
/** True when a mark carries a place. */
|
|
73
|
+
export declare const hasPlace: (d: MarkDatum & {
|
|
74
|
+
x?: number | null;
|
|
75
|
+
y?: number | null;
|
|
76
|
+
}) => boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Turn the shots at the left end of a surface a half turn so that every one is at the right end, in the numbers of the frame they are in.
|
|
79
|
+
* Rounded to hundredths of the frame's unit, so that a shot at the right end comes back as it was given.
|
|
80
|
+
*/
|
|
81
|
+
export declare function foldToRight<T extends {
|
|
82
|
+
x: number | null;
|
|
83
|
+
y: number | null;
|
|
84
|
+
}>(rows: readonly T[], frame: Frame, o: SurfaceOptions): T[];
|
|
85
|
+
/** The marks' places on the surface, in feet, in the same frame and ends as a layer will draw them. */
|
|
86
|
+
export declare function placed(o: SurfaceOptions, layer: LayerBase, rows: readonly {
|
|
87
|
+
x?: number | null;
|
|
88
|
+
y?: number | null;
|
|
89
|
+
}[]): ([number, number] | undefined)[];
|
|
90
|
+
/**
|
|
91
|
+
* A shot chart for the NBA from stats.nba.com's shot data: each shot as a mark (filled when it went in, an open x when it missed), and the
|
|
92
|
+
* six shot zones coloured by their field-goal percentage, with the percentage and made over taken written on each, a legend and a table. Give
|
|
93
|
+
* `league` (the league's percentage) and the zones are coloured by how far they are above or below it, on a diverging ramp.
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { nbaShotChart } from "@johnmorrisdotca/kyogijo-stadiums/charts";
|
|
97
|
+
* const chart = nbaShotChart(
|
|
98
|
+
* [
|
|
99
|
+
* { x: 0, y: 12, made: true },
|
|
100
|
+
* { x: -231, y: 18, made: false },
|
|
101
|
+
* { x: 120, y: 140, made: true },
|
|
102
|
+
* { x: -30, y: 70, made: true },
|
|
103
|
+
* ],
|
|
104
|
+
* { league: 46, minShots: 1 },
|
|
105
|
+
* );
|
|
106
|
+
* console.log(chart.totals.shots, chart.totals.made, chart.layers.map((layer) => layer.kind).join(" "));
|
|
107
|
+
* console.log(chart.draw().startsWith("<svg"), chart.table().includes("Restricted area"));
|
|
108
|
+
* // 4 3 zones points points text
|
|
109
|
+
* // true true
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function nbaShotChart(shots: readonly NbaShot[], options?: NbaShotOptions): Chart<NbaTotals> & {
|
|
113
|
+
key: KeyItem[];
|
|
114
|
+
};
|
|
115
|
+
/** Per-team totals of shots, goals and expected goals. */
|
|
116
|
+
export declare function teamTotals(rows: readonly {
|
|
117
|
+
x?: number | null;
|
|
118
|
+
y?: number | null;
|
|
119
|
+
outcome?: string;
|
|
120
|
+
team?: string;
|
|
121
|
+
value?: number | null;
|
|
122
|
+
}[]): Record<string, {
|
|
123
|
+
shots: number;
|
|
124
|
+
goals: number;
|
|
125
|
+
xg: number | null;
|
|
126
|
+
}>;
|
|
127
|
+
/**
|
|
128
|
+
* A shot map for the NHL from play-by-play shots: each shot a mark (a goal a star, a save an open circle, a miss an open x, a block an open
|
|
129
|
+
* triangle) sized by its chance of scoring, with an optional count of the shots into the slot zones or hexagons, a legend, a key and a table.
|
|
130
|
+
* With `fold` the shots at the left end are turned a half turn, so that every shot is at the right end.
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { nhlShotMap } from "@johnmorrisdotca/kyogijo-stadiums/charts";
|
|
134
|
+
* const chart = nhlShotMap(
|
|
135
|
+
* [
|
|
136
|
+
* { x: 75, y: 4, outcome: "goal", xg: 0.31 },
|
|
137
|
+
* { x: -60, y: -12, outcome: "saved", xg: 0.07 },
|
|
138
|
+
* { x: 40, y: 20, outcome: "missed", xg: 0.03 },
|
|
139
|
+
* ],
|
|
140
|
+
* { fold: true, zones: true },
|
|
141
|
+
* );
|
|
142
|
+
* console.log(chart.totals.shots, chart.totals.goals, chart.totals.xg);
|
|
143
|
+
* console.log(chart.layers.map((layer) => layer.id).join(" "));
|
|
144
|
+
* // 3 1 0.41
|
|
145
|
+
* // zones shots key-marks key-text
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export declare function nhlShotMap(shots: readonly NhlShot[], options?: NhlShotOptions): Chart<NhlTotals> & {
|
|
149
|
+
key: KeyItem[];
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* A pass chart for the NFL from tracking data: where each pass came down as a mark (a completion filled, an incompletion open, a touchdown a
|
|
153
|
+
* star, an interception an x), the line of scrimmage, and the pass grid (short and deep by left, middle and right) coloured by completion
|
|
154
|
+
* percentage with the air yards of each pass in its table. The offence goes to the right whichever way `direction` says it went in the data.
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* import { nflPassChart } from "@johnmorrisdotca/kyogijo-stadiums/charts";
|
|
158
|
+
* const chart = nflPassChart(
|
|
159
|
+
* [
|
|
160
|
+
* { x: 72, y: 26, outcome: "complete" },
|
|
161
|
+
* { x: 88, y: 14, outcome: "incomplete" },
|
|
162
|
+
* { x: 104, y: 30, outcome: "touchdown" },
|
|
163
|
+
* ],
|
|
164
|
+
* { los: 65, minPasses: 1 },
|
|
165
|
+
* );
|
|
166
|
+
* console.log(chart.totals.passes, chart.totals.complete, chart.totals.airYards);
|
|
167
|
+
* // 3 2 23
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export declare function nflPassChart(passes: readonly NflPass[], options?: NflPassOptions): Chart<NflTotals> & {
|
|
171
|
+
key: KeyItem[];
|
|
172
|
+
};
|