The pages are in English; this contents, the search and the buttons are in Japanese.
Gallery: data on a surface
Every kind of data layer, every heat layer, the ready-made charts, the team-shape layers and a frame of a replay, on made-up data that the code under each picture makes. The marks are in the surface's own frame, or a provider's. The pictures share one stylesheet, which is left out of each one's SVG here; drawKyogijo puts it in every drawing unless style: "none" says not to.
Data layers#
layers in the options of drawKyogijo: points, lines, arrows and text.
Points: the seven shapes
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const shapes = ["circle", "square", "triangle", "diamond", "cross", "x", "star"];
const svg = drawKyogijo({
sport: "nhl",
layers: [{ kind: "points", name: "shapes", data: shapes.map((shape, i) => ({ x: 40 + i * 20, y: 42.5, shape, title: shape })), style: { size: 8 } }],
});Points: outcomes, size and team
A miss is open, a goal a star, and the size follows each mark's `value`.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const shots = Array.from({ length: 36 }, (_, i) => ({ x: 20 + ((i * 53) % 160), y: 8 + ((i * 29) % 70), team: i % 2 ? "away" : "home", outcome: i % 5 === 0 ? "goal" : "shot", value: 0.05 + (i % 7) / 20 }));
const svg = drawKyogijo({
sport: "nhl",
layers: [{ kind: "points", name: "shots", data: shots, style: { size: { range: [1.6, 4.6] }, outcomes: { shot: { hollow: true, shape: "x" } } } }],
});Lines: straight, curved and wavy
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const svg = drawKyogijo({
sport: "nfl",
layers: [
{ kind: "lines", name: "straight", data: [{ x: 40, y: 12, x2: 80, y2: 12 }], style: { width: 1.4 } },
{ kind: "lines", name: "curved", data: [{ x: 40, y: 26, x2: 80, y2: 26, curve: 0.25 }], style: { width: 1.4 } },
{ kind: "lines", name: "wavy", data: [{ x: 40, y: 40, x2: 80, y2: 40 }], style: { width: 1.4, wavy: true } },
],
});Arrows: heads, dashes and a comet
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const svg = drawKyogijo({
sport: "nfl",
layers: [
{ kind: "arrows", name: "complete", data: [{ x: 40, y: 12, x2: 80, y2: 16, outcome: "complete" }], style: { width: 1.4 } },
{ kind: "arrows", name: "incomplete", data: [{ x: 40, y: 26, x2: 80, y2: 30, outcome: "incomplete" }], style: { width: 1.4 } },
{ kind: "arrows", name: "comet", data: [{ x: 40, y: 40, x2: 100, y2: 44 }], style: { width: 1.4, comet: { taper: true } } },
],
});Text: words on the surface
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const svg = drawKyogijo({
sport: "nba",
layers: [{ kind: "text", name: "notes", data: [{ x: 47, y: 25, text: "Centre" }, { x: 12, y: 25, text: "Key" }, { x: 82, y: 25, text: "Key" }], style: { size: 3 } }],
});Heat layers#
Counts in cells, hexagons, named zones or a density, in a colour ramp.
Heat map
A count in each cell of a grid, in a ramp, cut to the boards.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const shots = Array.from({ length: 360 }, (_, i) => { const t = i * 2.399963; const r = 4 + 54 * Math.sqrt((i % 120) / 120); return { x: 189 - r * Math.abs(Math.cos(t)) * 1.1, y: 42.5 + r * Math.sin(t) * 0.7 }; });
const svg = drawKyogijo({
sport: "nhl",
layers: [{ kind: "heatmap", name: "shots", data: shots, bins: [50, 21], legend: { title: "Shots in each 4 by 4 feet" } }],
});Hexagons
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const shots = Array.from({ length: 360 }, (_, i) => { const t = i * 2.399963; const r = 4 + 54 * Math.sqrt((i % 120) / 120); return { x: 189 - r * Math.abs(Math.cos(t)) * 1.1, y: 42.5 + r * Math.sin(t) * 0.7 }; });
const svg = drawKyogijo({ sport: "nhl", layers: [{ kind: "hexbin", name: "shots", data: shots, size: 5, legend: true }] });Zones: the slot
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const shots = Array.from({ length: 360 }, (_, i) => { const t = i * 2.399963; const r = 4 + 54 * Math.sqrt((i % 120) / 120); return { x: 189 - r * Math.abs(Math.cos(t)) * 1.1, y: 42.5 + r * Math.sin(t) * 0.7 }; });
const svg = drawKyogijo({ sport: "nhl", layers: [{ kind: "zones", name: "shots", zones: "nhl-slot", data: shots, labels: true, legend: true }] });Zones: shot zones
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const shots = Array.from({ length: 160 }, (_, i) => ({ x: ((i * 97) % 480) - 240, y: (i * 61) % 300, outcome: i % 3 === 0 ? "made" : "missed" }));
const svg = drawKyogijo({
sport: "nba",
layers: [{ kind: "zones", name: "shots", zones: "nba-shot", from: "nba-stats", data: shots, statistic: "rate", labels: true, legend: true }],
});Zones: positional play
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const touches = Array.from({ length: 220 }, (_, i) => ({ x: 6 + ((i * 41) % 93), y: 4 + ((i * 29) % 92) }));
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "zones", name: "touches", zones: "soccer-positional", from: "opta", data: touches, statistic: "percent", legend: true }] });Zones: passes
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const passes = Array.from({ length: 90 }, (_, i) => ({ x: 62 + ((i * 17) % 45), y: 6 + ((i * 23) % 42) }));
const svg = drawKyogijo({ sport: "nfl", layers: [{ kind: "zones", name: "passes", zones: "football-pass", from: "nfl-tracking", los: 62, data: passes, labels: true, legend: true }] });Contours: a density
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const shots = Array.from({ length: 360 }, (_, i) => { const t = i * 2.399963; const r = 4 + 54 * Math.sqrt((i % 120) / 120); return { x: 189 - r * Math.abs(Math.cos(t)) * 1.1, y: 42.5 + r * Math.sin(t) * 0.7 }; });
const svg = drawKyogijo({ sport: "nhl", layers: [{ kind: "contour", name: "shots", data: shots, levels: 6, filled: true, legend: true }] });A legend of its own
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const shots = Array.from({ length: 360 }, (_, i) => { const t = i * 2.399963; const r = 4 + 54 * Math.sqrt((i % 120) / 120); return { x: 189 - r * Math.abs(Math.cos(t)) * 1.1, y: 42.5 + r * Math.sin(t) * 0.7 }; });
const svg = drawKyogijo({
sport: "nhl",
layers: [
{ kind: "heatmap", id: "heat", name: "shots", data: shots, bins: [40, 17] },
{ kind: "legend", for: "heat", title: "Shots", position: "bottom" },
],
});Ready-made charts#
One call for a good chart: @johnmorrisdotca/kyogijo-stadiums/charts.
NBA shot chart
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { nbaShotChart } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const shots = Array.from({ length: 140 }, (_, i) => ({ x: ((i * 97) % 480) - 240, y: (i * 61) % 300, made: i % 3 === 0 }));
const svg = nbaShotChart(shots, { league: 46, minShots: 2 }).draw();NHL shot map
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { nhlShotMap } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const shots = Array.from({ length: 40 }, (_, i) => ({ x: (i % 2 ? -1 : 1) * (50 + ((i * 11) % 38)), y: ((i * 23) % 60) - 30, outcome: ["goal", "saved", "missed", "blocked"][i % 4], xg: 0.03 + (i % 9) / 30, team: i % 2 ? "away" : "home" }));
const svg = nhlShotMap(shots, { fold: true, zones: true, statistic: "xg" }).draw();MLB spray chart
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { mlbSprayChart } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const types = ["single", "field_out", "double", "field_out", "home_run", "triple"];
const balls = Array.from({ length: 60 }, (_, i) => ({ x: 125.4 + (((i * 37) % 120) - 60), y: 198.27 - (30 + ((i * 53) % 110)), type: types[i % types.length] }));
const svg = mlbSprayChart(balls, { park: "fenway-park", zones: true }).draw();NFL pass chart
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { nflPassChart } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const outcomes = ["complete", "complete", "incomplete", "touchdown", "interception"];
const passes = Array.from({ length: 40 }, (_, i) => ({ x: 66 + ((i * 13) % 44), y: 8 + ((i * 17) % 40), outcome: outcomes[i % outcomes.length], fromX: 60, fromY: 26 }));
const svg = nflPassChart(passes, { los: 62, arrows: true }).draw();Soccer shot map
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { soccerShotMap } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const outcomes = ["goal", "saved", "missed", "blocked"];
const shots = Array.from({ length: 24 }, (_, i) => ({ x: 84 + ((i * 7) % 30), y: 18 + ((i * 11) % 44), outcome: outcomes[i % 4], xg: 0.04 + (i % 8) / 20 }));
const svg = soccerShotMap(shots, { provider: "statsbomb", half: true }).draw();Road to the final, card
1080 by 1350. The winner of each match is brighter; the final's loser is quiet.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { roadToFinal } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const flag = (a, b, c) => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 20"><rect width="30" height="20" fill="' + a + '"/><path d="M15 2L27 10L15 18L3 10Z" fill="' + b + '"/><circle cx="15" cy="10" r="3.4" fill="' + c + '"/></svg>';
const stripes = (a, b, c) => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 20"><rect width="10.1" height="20" fill="' + a + '"/><rect x="10" width="10.1" height="20" fill="' + b + '"/><rect x="20" width="10" height="20" fill="' + c + '"/></svg>';
const aurelia = {
name: "Aurelia", abbr: "AUR", color: "#0f7a7a", logo: flag("#0f7a7a", "#f4f0e4", "#e07a1f"),
group: [
{ opponent: { name: "Isenmark", abbr: "ISE", color: "#3a2d6b", logo: stripes("#3a2d6b", "#f4f0e4", "#3a2d6b") }, score: [2, 0] },
{ opponent: { name: "Lumeria", abbr: "LUM", color: "#14233f", logo: flag("#14233f", "#f2c230", "#14233f") }, score: [1, 1] },
{ opponent: { name: "Rovana", abbr: "ROV", color: "#1f6b3c", logo: stripes("#f2c230", "#1f6b3c", "#f2c230") }, score: [3, 0] },
],
path: [
{ round: "round-of-16", opponent: { name: "Fenlund", abbr: "FEN", color: "#1f6b3c", logo: flag("#1f6b3c", "#f2c230", "#1f6b3c") }, score: [2, 1] },
{ round: "quarter-final", opponent: { name: "Garrovia", abbr: "GAR", color: "#14233f", logo: stripes("#14233f", "#f2a900", "#14233f") }, score: [1, 1], decidedBy: "pens", pens: [5, 4] },
{ round: "semi-final", opponent: { name: "Norvane", abbr: "NRV", color: "#1f6b3c", logo: flag("#f4f0e4", "#1f6b3c", "#f4f0e4") }, score: [2, 1], decidedBy: "aet" },
],
};
const calvara = {
name: "Calvara", abbr: "CAL", color: "#6b2d5c", logo: stripes("#6b2d5c", "#f2c230", "#14233f"),
group: [
{ opponent: { name: "Estria", abbr: "EST", color: "#3a2d6b", logo: flag("#e8e2cf", "#3a2d6b", "#e8e2cf") }, score: [4, 1] },
{ opponent: { name: "Borea", abbr: "BOR", color: "#8a2d4b", logo: stripes("#8a2d4b", "#e8e2cf", "#2f7a67") }, score: [0, 1] },
{ opponent: { name: "Kestrel Isles", abbr: "KES", color: "#14233f", logo: flag("#14233f", "#e8e2cf", "#c2410c") }, score: [2, 2] },
],
path: [
{ round: "round-of-16", opponent: { name: "Dornmark", abbr: "DOR", color: "#c2410c", logo: flag("#c2410c", "#14233f", "#c2410c") }, score: [1, 0] },
{ round: "quarter-final", opponent: { name: "Helmara", abbr: "HEL", color: "#7a2a8c", logo: stripes("#f2c230", "#7a2a8c", "#f2c230") }, score: [2, 2], decidedBy: "pens", pens: [3, 2] },
{ round: "semi-final", opponent: { name: "Valdor", abbr: "VAL", color: "#c2410c", logo: stripes("#c2410c", "#f4f0e4", "#c2410c") }, score: [3, 1] },
],
};
const svg = roadToFinal([aurelia, calvara], { title: "Northern Cup 2030", final: { score: [2, 2], decidedBy: "pens", pens: [4, 3] }, endnote: "Made-up teams, flags and results" }).draw();Road to the final, wide
1200 by 675, before the final is played: the middle says v.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { roadToFinal } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const flag = (a, b, c) => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 20"><rect width="30" height="20" fill="' + a + '"/><path d="M15 2L27 10L15 18L3 10Z" fill="' + b + '"/><circle cx="15" cy="10" r="3.4" fill="' + c + '"/></svg>';
const stripes = (a, b, c) => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 20"><rect width="10.1" height="20" fill="' + a + '"/><rect x="10" width="10.1" height="20" fill="' + b + '"/><rect x="20" width="10" height="20" fill="' + c + '"/></svg>';
const aurelia = {
name: "Aurelia", abbr: "AUR", color: "#0f7a7a", logo: flag("#0f7a7a", "#f4f0e4", "#e07a1f"),
group: [
{ opponent: { name: "Isenmark", abbr: "ISE", color: "#3a2d6b", logo: stripes("#3a2d6b", "#f4f0e4", "#3a2d6b") }, score: [2, 0] },
{ opponent: { name: "Lumeria", abbr: "LUM", color: "#14233f", logo: flag("#14233f", "#f2c230", "#14233f") }, score: [1, 1] },
{ opponent: { name: "Rovana", abbr: "ROV", color: "#1f6b3c", logo: stripes("#f2c230", "#1f6b3c", "#f2c230") }, score: [3, 0] },
],
path: [
{ round: "round-of-16", opponent: { name: "Fenlund", abbr: "FEN", color: "#1f6b3c", logo: flag("#1f6b3c", "#f2c230", "#1f6b3c") }, score: [2, 1] },
{ round: "quarter-final", opponent: { name: "Garrovia", abbr: "GAR", color: "#14233f", logo: stripes("#14233f", "#f2a900", "#14233f") }, score: [1, 1], decidedBy: "pens", pens: [5, 4] },
{ round: "semi-final", opponent: { name: "Norvane", abbr: "NRV", color: "#1f6b3c", logo: flag("#f4f0e4", "#1f6b3c", "#f4f0e4") }, score: [2, 1], decidedBy: "aet" },
],
};
const calvara = {
name: "Calvara", abbr: "CAL", color: "#6b2d5c", logo: stripes("#6b2d5c", "#f2c230", "#14233f"),
group: [
{ opponent: { name: "Estria", abbr: "EST", color: "#3a2d6b", logo: flag("#e8e2cf", "#3a2d6b", "#e8e2cf") }, score: [4, 1] },
{ opponent: { name: "Borea", abbr: "BOR", color: "#8a2d4b", logo: stripes("#8a2d4b", "#e8e2cf", "#2f7a67") }, score: [0, 1] },
{ opponent: { name: "Kestrel Isles", abbr: "KES", color: "#14233f", logo: flag("#14233f", "#e8e2cf", "#c2410c") }, score: [2, 2] },
],
path: [
{ round: "round-of-16", opponent: { name: "Dornmark", abbr: "DOR", color: "#c2410c", logo: flag("#c2410c", "#14233f", "#c2410c") }, score: [1, 0] },
{ round: "quarter-final", opponent: { name: "Helmara", abbr: "HEL", color: "#7a2a8c", logo: stripes("#f2c230", "#7a2a8c", "#f2c230") }, score: [2, 2], decidedBy: "pens", pens: [3, 2] },
{ round: "semi-final", opponent: { name: "Valdor", abbr: "VAL", color: "#c2410c", logo: stripes("#c2410c", "#f4f0e4", "#c2410c") }, score: [3, 1] },
],
};
const svg = roadToFinal([aurelia, calvara], { title: "Northern Cup 2030", size: "wide" }).draw();Road to the final, phone, dark
390 wide and as tall as the rounds need, with each round's name over its row.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { roadToFinal } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const flag = (a, b, c) => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 20"><rect width="30" height="20" fill="' + a + '"/><path d="M15 2L27 10L15 18L3 10Z" fill="' + b + '"/><circle cx="15" cy="10" r="3.4" fill="' + c + '"/></svg>';
const stripes = (a, b, c) => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 20"><rect width="10.1" height="20" fill="' + a + '"/><rect x="10" width="10.1" height="20" fill="' + b + '"/><rect x="20" width="10" height="20" fill="' + c + '"/></svg>';
const aurelia = {
name: "Aurelia", abbr: "AUR", color: "#0f7a7a", logo: flag("#0f7a7a", "#f4f0e4", "#e07a1f"),
group: [
{ opponent: { name: "Isenmark", abbr: "ISE", color: "#3a2d6b", logo: stripes("#3a2d6b", "#f4f0e4", "#3a2d6b") }, score: [2, 0] },
{ opponent: { name: "Lumeria", abbr: "LUM", color: "#14233f", logo: flag("#14233f", "#f2c230", "#14233f") }, score: [1, 1] },
{ opponent: { name: "Rovana", abbr: "ROV", color: "#1f6b3c", logo: stripes("#f2c230", "#1f6b3c", "#f2c230") }, score: [3, 0] },
],
path: [
{ round: "round-of-16", opponent: { name: "Fenlund", abbr: "FEN", color: "#1f6b3c", logo: flag("#1f6b3c", "#f2c230", "#1f6b3c") }, score: [2, 1] },
{ round: "quarter-final", opponent: { name: "Garrovia", abbr: "GAR", color: "#14233f", logo: stripes("#14233f", "#f2a900", "#14233f") }, score: [1, 1], decidedBy: "pens", pens: [5, 4] },
{ round: "semi-final", opponent: { name: "Norvane", abbr: "NRV", color: "#1f6b3c", logo: flag("#f4f0e4", "#1f6b3c", "#f4f0e4") }, score: [2, 1], decidedBy: "aet" },
],
};
const calvara = {
name: "Calvara", abbr: "CAL", color: "#6b2d5c", logo: stripes("#6b2d5c", "#f2c230", "#14233f"),
group: [
{ opponent: { name: "Estria", abbr: "EST", color: "#3a2d6b", logo: flag("#e8e2cf", "#3a2d6b", "#e8e2cf") }, score: [4, 1] },
{ opponent: { name: "Borea", abbr: "BOR", color: "#8a2d4b", logo: stripes("#8a2d4b", "#e8e2cf", "#2f7a67") }, score: [0, 1] },
{ opponent: { name: "Kestrel Isles", abbr: "KES", color: "#14233f", logo: flag("#14233f", "#e8e2cf", "#c2410c") }, score: [2, 2] },
],
path: [
{ round: "round-of-16", opponent: { name: "Dornmark", abbr: "DOR", color: "#c2410c", logo: flag("#c2410c", "#14233f", "#c2410c") }, score: [1, 0] },
{ round: "quarter-final", opponent: { name: "Helmara", abbr: "HEL", color: "#7a2a8c", logo: stripes("#f2c230", "#7a2a8c", "#f2c230") }, score: [2, 2], decidedBy: "pens", pens: [3, 2] },
{ round: "semi-final", opponent: { name: "Valdor", abbr: "VAL", color: "#c2410c", logo: stripes("#c2410c", "#f4f0e4", "#c2410c") }, score: [3, 1] },
],
};
const svg = roadToFinal([aurelia, calvara], { title: "Northern Cup 2030", size: "phone", theme: "dark" }).draw();Road to the final: a series, with no flags
A team with no flag or logo is its initials on its colour; the rounds are named by the caller and the scores are games won.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { roadToFinal } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const owls = {
name: "Northern Owls", abbr: "NOW", color: "#3a2d6b",
path: [
{ round: "first-round", opponent: { name: "Coastal Pines", abbr: "CPN", color: "#1f6b3c" }, score: "4-0", note: "Game 4" },
{ round: "second-round", opponent: { name: "Lakeshore Anchors", abbr: "LSA", color: "#0f7a7a" }, score: "4-2", note: "Game 6" },
{ round: "conference-final", opponent: { name: "Prairie Ridge Bison", abbr: "PRB", color: "#a8452c" }, score: "4-3", note: "Game 7" },
],
};
const gulls = {
name: "Harbour City Gulls", abbr: "HCG", color: "#1d4f91",
path: [
{ round: "first-round", opponent: { name: "Metro Bolts", abbr: "MTB", color: "#c2410c" }, score: "4-1", note: "Game 5" },
{ round: "second-round", opponent: { name: "Summit Peaks", abbr: "SMT", color: "#14233f" }, score: "4-3", note: "Game 7" },
{ round: "conference-final", opponent: { name: "Valley Stars", abbr: "VAL", color: "#c8102e" }, score: "4-2", note: "Game 6" },
],
};
const labels = { "first-round": "First round", "second-round": "Second round", "conference-final": "Conference final", final: "Cup final" };
const svg = roadToFinal([owls, gulls], { title: "Cup play-offs", labels, size: "square" }).draw();Team shape#
Pass networks, formations, space control, triangulations, hulls, flow fields and sonars, as on the Team shape page. Made-up players and passes from a fixed formula.
Pass network
A node at the average place of each player's passes, sized by his touches; a link for each pair, wider the more passes between them.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const passes = Array.from({ length: 240 }, (_, i) => { const a = (i * 7) % 11; const b = (a + 1 + ((i * 3) % 4)) % 11; return { from: a + 1, to: b + 1, x: 14 + a * 6 + (i % 5), y: 12 + ((a * 17) % 76) + (i % 7), x2: 14 + b * 6, y2: 12 + ((b * 17) % 76), team: "home", fromNumber: a + 1, toNumber: b + 1 }; });
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "network", from: "opta", data: passes, minPasses: 3 }] });Pass network, directed
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const passes = Array.from({ length: 240 }, (_, i) => { const a = (i * 7) % 11; const b = (a + 1 + ((i * 3) % 4)) % 11; return { from: a + 1, to: b + 1, x: 14 + a * 6 + (i % 5), y: 12 + ((a * 17) % 76) + (i % 7), x2: 14 + b * 6, y2: 12 + ((b * 17) % 76), team: "home", fromNumber: a + 1, toNumber: b + 1 }; });
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "network", from: "opta", data: passes, minPasses: 6, directed: true, labels: "id" }] });Pass network chart
With the key and the totals; `table()` lists the players and the links.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { passNetwork } from "@johnmorrisdotca/kyogijo-stadiums/charts";
const passes = Array.from({ length: 240 }, (_, i) => { const a = (i * 7) % 11; const b = (a + 1 + ((i * 3) % 4)) % 11; return { from: a + 1, to: b + 1, x: 14 + a * 6 + (i % 5), y: 12 + ((a * 17) % 76) + (i % 7), x2: 14 + b * 6, y2: 12 + ((b * 17) % 76), team: "home", fromNumber: a + 1, toNumber: b + 1 }; });
const svg = passNetwork(passes, { provider: "opta", minPasses: 4 }).draw();Formations: 4-4-2 against 4-3-3
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const svg = drawKyogijo({
sport: "soccer",
layers: [
{ kind: "formation", formation: "4-4-2", team: "home" },
{ kind: "formation", formation: "4-3-3", team: "away", attacking: "left" },
],
});A formation with its lineup
A position nobody stands on is hollow.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const lineup = [["GK", 1, "Okafor"], ["RB", 2, "Haas"], ["LB", 3, "Ito"], ["ST", 9, "Silva"]].map(([position, number, name]) => ({ position, number, name }));
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "formation", formation: "4-2-3-1", team: "home", data: lineup, names: true }] });Formations: a rink
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const svg = drawKyogijo({
sport: "nhl",
layers: [
{ kind: "formation", formation: "hockey-5v5", team: "home" },
{ kind: "formation", formation: "hockey-1-3-1", team: "away", attacking: "left" },
],
});Formations: a court
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const svg = drawKyogijo({ sport: "nba", layers: [{ kind: "formation", formation: "basketball-high-post", team: "home" }] });Formations: offence and defence
The ball is on the 30-yard line (40 yards from the back of the left end zone).
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const svg = drawKyogijo({
sport: "nfl",
layers: [
{ kind: "formation", formation: "football-shotgun", team: "home", los: 40 },
{ kind: "formation", formation: "football-4-3", team: "away", los: 40 },
],
});Formations: a ballpark and a shift
An infield shift against a left-handed pull hitter. Not allowed in MLB since 2023.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const svg = drawKyogijo({ sport: "mlb", layers: [{ kind: "formation", formation: "baseball-shift", team: "away" }] });Lineups: 4-2-3-1 against 4-4-2
The home team has no formation: the best fit is chosen. The visitors attack the other way.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const home = ["Okafor", "Silva", "Mbeki", "Rossi", "Ito", "Nakamura", "Costa", "Dubois", "Haddad", "Kowalski", "Eze"];
const codes = ["GK", "RB", "CB", "CB", "LB", "DM", "DM", "RW", "AM", "LW", "ST"];
const players = home.map((name, i) => ({ pos: codes[i], name, number: i + 1, captain: i === 2 }));
const svg = drawKyogijo({
sport: "soccer",
layers: [
{ kind: "lineup", side: "home", players },
{ kind: "lineup", side: "away", formation: "4-4-2", shape: "jersey", players: [{ pos: "GK", name: "Meyer", number: 1 }, { pos: "ST", name: "Moreau", number: 9 }, { pos: "ST", name: "Eze", number: 10 }] },
],
});Lineups: a rink with a second line
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const svg = drawKyogijo({
sport: "nhl",
layers: [
{
kind: "lineup",
side: "home",
players: [
{ pos: "G", name: "Hellebuyck", number: 37 }, { pos: "D", name: "Josi", number: 59, captain: true }, { pos: "D", name: "Makar", number: 8 },
{ pos: "LW", name: "Marner", number: 16 }, { pos: "C", name: "McDavid", number: 97 }, { pos: "RW", name: "Rantanen", number: 96 },
{ pos: "LW", name: "Hyman", number: 18, line: 2 }, { pos: "C", name: "Draisaitl", number: 29, line: 2 }, { pos: "RW", name: "Kane", number: 93, line: 2 },
],
},
],
});Lineups: a ballpark with the designated hitter
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const codes = ["P", "C", "1B", "2B", "SS", "3B", "LF", "CF", "RF", "DH"];
const names = ["Cole", "Rutschman", "Alonso", "Albies", "Witt", "Ramirez", "Soto", "Trout", "Judge", "Ohtani"];
const players = codes.map((pos, i) => ({ pos, name: names[i], number: i + 1 }));
const svg = drawKyogijo({ sport: "mlb", layers: [{ kind: "lineup", shape: "jersey", players }] });Lineups: two rugby teams
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const fifteen = () => Array.from({ length: 15 }, (_, i) => ({ pos: String(i + 1), number: i + 1, name: "Player " + (i + 1) }));
const svg = drawKyogijo({ sport: "rugby", layers: [{ kind: "lineup", side: "home", shape: "jersey", players: fifteen() }, { kind: "lineup", side: "away", shape: "jersey", players: fifteen() }] });Lineups: an offence and a defence
A football line stands shoulder to shoulder, so the picture is zoomed to the middle of the field.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const offence = ["QB", "HB", "FB", "TE", "WR", "WR", "C", "LG", "RG", "LT", "RT"];
const defence = ["DE", "DT", "DT", "DE", "LB", "LB", "LB", "CB", "CB", "S", "S"];
const of = (codes) => codes.map((pos, i) => ({ pos, number: i + 1 }));
const svg = drawKyogijo({
sport: "nfl",
range: "between-the-20s",
layers: [{ kind: "lineup", side: "home", players: of(offence) }, { kind: "lineup", side: "away", players: of(defence) }],
});Lineups: a fielding preset
For a right-handed batter with the striker at the right end: the off side is up the page.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const codes = ["BWL", "WK", "SL1", "SL2", "SL3", "GU", "PT", "CV", "MO", "MN", "SQ"];
const svg = drawKyogijo({ sport: "cricket", layers: [{ kind: "lineup", formation: "cricket-test-attacking", players: codes.map((pos, i) => ({ pos, number: i + 1 })) }] });Lineups: a volleyball rotation
The home team is on its second rotation: each player has moved one place clockwise.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const six = ["P1", "P2", "P3", "P4", "P5", "P6"].map((pos, i) => ({ pos, number: i + 1 }));
const svg = drawKyogijo({ sport: "volleyball", layers: [{ kind: "lineup", side: "home", rotation: 2, players: six }, { kind: "lineup", side: "away", players: six }] });Voronoi: space control
Each player's cell is the part of the pitch nearer him than any other player; the description gives each team's share.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const players = Array.from({ length: 22 }, (_, i) => ({ x: 8 + ((i * 37 + (i < 11 ? 3 : 17)) % 88), y: 8 + ((i * 53 + (i < 11 ? 5 : 11)) % 84), team: i < 11 ? "home" : "away", number: (i % 11) + 1 }));
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "voronoi", from: "opta", data: players }] });Delaunay triangles
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const players = Array.from({ length: 22 }, (_, i) => ({ x: 8 + ((i * 37 + (i < 11 ? 3 : 17)) % 88), y: 8 + ((i * 53 + (i < 11 ? 5 : 11)) % 84), team: i < 11 ? "home" : "away", number: (i % 11) + 1 }));
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "delaunay", from: "opta", data: players, triangles: true }] });Convex hulls
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const players = Array.from({ length: 22 }, (_, i) => ({ x: 8 + ((i * 37 + (i < 11 ? 3 : 17)) % 88), y: 8 + ((i * 53 + (i < 11 ? 5 : 11)) % 84), team: i < 11 ? "home" : "away", number: (i % 11) + 1 }));
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "hull", from: "opta", data: players }] });Space and shape, tall and dark
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const players = Array.from({ length: 22 }, (_, i) => ({ x: 8 + ((i * 37 + (i < 11 ? 3 : 17)) % 88), y: 8 + ((i * 53 + (i < 11 ? 5 : 11)) % 84), team: i < 11 ? "home" : "away", number: (i % 11) + 1 }));
const svg = drawKyogijo({
sport: "soccer",
orientation: "portrait",
theme: "dark",
layers: [{ kind: "voronoi", from: "opta", data: players }, { kind: "hull", from: "opta", data: players }],
});Flow field
The mean direction of the passes that start in each cell.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const moves = Array.from({ length: 400 }, (_, i) => { const x = 5 + ((i * 13) % 90); const y = 5 + ((i * 29) % 90); return { x, y, x2: x + 8 + (i % 5) + (x > 50 ? 4 : 0), y2: y + ((i % 7) - 3) * 2.2, id: (i % 5) + 1 }; });
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "flow", from: "opta", data: moves, bins: 12 }] });Sonars: a rose for each player
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const moves = Array.from({ length: 400 }, (_, i) => { const x = 5 + ((i * 13) % 90); const y = 5 + ((i * 29) % 90); return { x, y, x2: x + 8 + (i % 5) + (x > 50 ? 4 : 0), y2: y + ((i % 7) - 3) * 2.2, id: (i % 5) + 1 }; });
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "sonar", from: "opta", data: moves, by: "player", angles: 10 }] });Sonars: a rose for each cell of a rink
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const moves = Array.from({ length: 400 }, (_, i) => { const x = 5 + ((i * 13) % 90); const y = 5 + ((i * 29) % 90); return { x, y, x2: x + 8 + (i % 5) + (x > 50 ? 4 : 0), y2: y + ((i % 7) - 3) * 2.2, id: (i % 5) + 1 }; }).map((m) => ({ x: m.x * 2 - 100, y: m.y * 0.85 - 42.5, x2: m.x2 * 2 - 100, y2: m.y2 * 0.85 - 42.5 }));
const svg = drawKyogijo({ sport: "nhl", layers: [{ kind: "sonar", from: "nhl-api", data: moves, by: "grid", bins: 8, angles: 8 }] });Sonars: a rose for each zone
The bars are the mean length of the passes in each direction.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawKyogijo } from "@johnmorrisdotca/kyogijo-stadiums";
const moves = Array.from({ length: 400 }, (_, i) => { const x = 5 + ((i * 13) % 90); const y = 5 + ((i * 29) % 90); return { x, y, x2: x + 8 + (i % 5) + (x > 50 ? 4 : 0), y2: y + ((i % 7) - 3) * 2.2, id: (i % 5) + 1 }; });
const svg = drawKyogijo({ sport: "soccer", layers: [{ kind: "sonar", from: "opta", data: moves, by: "zone", zones: "soccer-thirds", angles: 8, statistic: "mean" }] });Replay#
A frame of tracking data on a surface: drawFrame from @johnmorrisdotca/kyogijo-stadiums/replay. The same frame, played, is mountKyogijo(...).replay(...).
A frame of a replay: the rink
Tracking data in the NHL API's frame, played on the rink: the players with their numbers, the puck with its trail, and the events that are on the surface at 7.8 seconds (the pass has gone, the shot is on its way). Played, with controls, it is mountKyogijo(...).replay(...).
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawFrame } from "@johnmorrisdotca/kyogijo-stadiums/replay";
const skater = (id, team, number, x, y, t) => ({ id, team, number, x: x + 6 * Math.sin(t / 1.4 + number), y: y + 4 * Math.cos(t / 1.1 + number) });
const frames = Array.from({ length: 51 }, (_, i) => {
const t = i / 5;
const objects = [skater("h9", "home", 9, 30, 0, t), skater("h17", "home", 17, 48, 18, t), skater("h28", "home", 28, 48, -18, t), skater("a4", "away", 4, 62, 8, t), skater("a8", "away", 8, 62, -8, t), skater("a31", "away", 31, 89, 0, 0)];
const puck = objects[Math.min(2, Math.floor(t / 3.5))];
return { t, objects: [...objects, { id: "puck", team: "puck", x: puck.x, y: puck.y }] };
});
const events = [
{ t: 3.5, kind: "pass", team: "home", by: "h9", to: "h17", until: 4 },
{ t: 7, kind: "shot", team: "home", by: "h28", x2: 89, y2: 2, until: 7.6 },
];
const svg = drawFrame({ sport: "nhl" }, { from: "nhl-api", frames, events }, 7.8);A frame of a replay: the pitch
The same on the pitch, in Opta's frame, with a trail of three seconds and the goal marked by a star.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawFrame } from "@johnmorrisdotca/kyogijo-stadiums/replay";
const spot = (id, team, number, x, y, t) => ({ id, team, number, x: x + 4 * Math.sin(t / 1.7 + number), y: y + 5 * Math.cos(t / 1.3 + number) });
const frames = Array.from({ length: 41 }, (_, i) => {
const t = i / 5;
const objects = [spot("h1", "home", 1, 4, 50, t), spot("h4", "home", 4, 30, 35, t), spot("h8", "home", 8, 50, 55, t), spot("h9", "home", 9, 72, 45, t), spot("a5", "away", 5, 66, 60, t), spot("a1", "away", 1, 96, 50, 0)];
const ball = objects[Math.min(3, Math.floor(t / 2))];
return { t, possession: ball.id, objects: [...objects, { id: "ball", team: "ball", x: ball.x, y: ball.y }] };
});
const events = [{ t: 2, kind: "pass", team: "home", by: "h4", to: "h8", until: 2.4 }, { t: 6, kind: "goal", team: "home", x: 100, y: 48 }];
const svg = drawFrame({ sport: "soccer" }, { from: "opta", frames, events }, 6.4, { trail: 3 });Goals, the strike zone and the stumps#
A target seen end-on, with where each shot, pitch or delivery crossed it: @johnmorrisdotca/kyogijo-stadiums/target.
A hockey net with its zones
The net is the NHL's 6 by 4 feet, seen from the front with the shooter's left on the left. A post and a crossbar are on the frame, a shot wide or high is outside it; the top shelf and the five-hole are hockey's names for the middle cells of the top and bottom rows.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawGoalMouth } from "@johnmorrisdotca/kyogijo-stadiums/target";
const shots = [
{ x: -2.4, z: 3.6, outcome: "goal" },
{ x: 0, z: 0.4, outcome: "saved" },
{ x: 1.5, z: 1.2, outcome: "goal" },
{ x: -3, z: 2.5, outcome: "post" },
{ x: 1, z: 4, outcome: "crossbar" },
{ x: 3.4, z: 2, outcome: "wide" },
{ x: 1.2, z: 4.6, outcome: "high" },
];
const svg = drawGoalMouth(shots, { sport: "nhl", zones: "labels" }).svg;A soccer goal, dark
The Laws' 7.32 by 2.44 metres; the marks are sized by expected goals.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawGoalMouth } from "@johnmorrisdotca/kyogijo-stadiums/target";
const shots = [
{ x: -3.3, z: 2.2, outcome: "goal", value: 0.4 },
{ x: 1, z: 0.5, outcome: "saved", value: 0.1 },
{ x: 3.66, z: 1.2, outcome: "post", value: 0.05 },
{ x: 4.4, z: 1, outcome: "wide", value: 0.03 },
{ x: 0, z: 2.9, outcome: "high", value: 0.02 },
];
const svg = drawGoalMouth(shots, { sport: "soccer", theme: "dark", zones: true, sizeBy: "value" }).svg;A lacrosse goal: 6 by 6 feet
World Lacrosse's 1.83 m and the NCAA's and the PLL's 6 feet are the same goal; the box floor's (`league: "nll"`) is 4 feet 9 inches by 4.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawGoalMouth } from "@johnmorrisdotca/kyogijo-stadiums/target";
const shots = [{ x: -2, z: 5, outcome: "goal" }, { x: 2.5, z: 1, outcome: "saved" }, { x: 3.4, z: 3, outcome: "wide" }];
const svg = drawGoalMouth(shots, { sport: "lacrosse", zones: true }).svg;A water polo goal
3.0 metres between the posts, the crossbar's underside 0.9 m over the water.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawGoalMouth } from "@johnmorrisdotca/kyogijo-stadiums/target";
const svg = drawGoalMouth([{ x: -1.2, z: 0.7, outcome: "goal" }, { x: 0.4, z: 0.2, outcome: "saved" }, { x: 1.6, z: 0.5, outcome: "wide" }], { sport: "waterpolo", zones: true }).svg;The strike zone: the catcher's view
Plate 17 inches across, a zone from the batter's knees to his belt, the nine cells and the four chase zones round them, the pitches numbered in order, coloured by result and shaped by what became of them.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawStrikeZone } from "@johnmorrisdotca/kyogijo-stadiums/target";
const pitches = [
{ x: 0.1, z: 2.6, outcome: "called-strike", type: "FF" },
{ x: 1.3, z: 1.1, outcome: "ball", type: "SL" },
{ x: -0.4, z: 2.0, outcome: "swinging-strike", type: "SL" },
{ x: 0.9, z: 3.9, outcome: "foul", type: "CH" },
{ x: -0.2, z: 2.4, outcome: "in-play", type: "FF" },
];
const svg = drawStrikeZone(pitches, { bats: "R", top: 3.5, bottom: 1.5 }).svg;The strike zone, left-handed, by pitch type, dark
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawStrikeZone } from "@johnmorrisdotca/kyogijo-stadiums/target";
const pitches = [
{ x: 0.3, z: 2.9, outcome: "called-strike", type: "FF" },
{ x: -1.1, z: 1.4, outcome: "ball", type: "CH" },
{ x: 0.5, z: 1.8, outcome: "swinging-strike", type: "SL" },
];
const svg = drawStrikeZone(pitches, { bats: "L", colorBy: "type", theme: "dark", top: 3.6, bottom: 1.6 }).svg;The stumps from the bowler's end
Three stumps across 9 inches and 28 inches high, with the bails; the line zones are the stumps' own width either side. The length of a delivery (where it pitched) names its length zone.
Open in a page loads the published package from jsDelivr, which serves only released versions.
Show the code
import { drawStumps } from "@johnmorrisdotca/kyogijo-stadiums/target";
const svg = drawStumps([
{ x: 0.02, z: 0.4, outcome: "wicket", length: 5.1 },
{ x: 0.3, z: 0.5, outcome: "dot", length: 7.4 },
{ x: -0.5, z: 0.2, outcome: "runs", length: 2 },
]).svg;