A basketball shot chart overlays shooting data on a half-court diagram, plotting each shot attempt as a point colored by outcome (made or missed). The court drawing includes the three-point arc, free-throw line, paint/key area, and basket, providing spatial context for analyzing shooting patterns and efficiency. Essential for basketball analytics and player evaluation.

// anyplot.ai
// scatter-shot-chart: Basketball Shot Chart
// Library: d3 7.9.0 | JavaScript 22.22.3
// Quality: 89/100 | Created: 2026-06-21
//# anyplot-orientation: square
const t = window.ANYPLOT_TOKENS;
const THEME = window.ANYPLOT_THEME;
const { width, height } = window.ANYPLOT_SIZE;
// Imprint palette: made = brand green (pos 1), missed = matte red (semantic anchor for loss/error)
const COLOR_MADE = t.palette[0]; // #009E73
const COLOR_MISSED = t.palette[4]; // #AE3030
// Deterministic LCG — browser has no seeded Math.random
function makeLcg(seed) {
let s = seed >>> 0;
return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; };
}
const rng = makeLcg(42);
function bm() {
const u1 = Math.max(rng(), 1e-9), u2 = rng();
const mag = Math.sqrt(-2 * Math.log(u1));
return [mag * Math.cos(2 * Math.PI * u2), mag * Math.sin(2 * Math.PI * u2)];
}
// Shot clusters [cx, cy, spreadX, spreadY, madeRate, weight]
// x = horizontal from court center, y = distance from baseline (ft); basket at (0, 5.25)
const CLUSTERS = [
[0, 5.5, 1.8, 1.5, 0.62, 0.12], // at-rim
[0, 16.0, 2.0, 1.5, 0.38, 0.07], // mid-range top
[-7, 13.0, 2.5, 2.0, 0.37, 0.07], // left mid
[7, 13.0, 2.5, 2.0, 0.37, 0.07], // right mid
[-21, 6.5, 1.5, 2.5, 0.38, 0.09], // left corner 3
[21, 6.5, 1.5, 2.5, 0.38, 0.09], // right corner 3
[-18, 18.0, 2.5, 2.5, 0.34, 0.10], // left wing 3
[18, 18.0, 2.5, 2.5, 0.34, 0.10], // right wing 3
[0, 28.0, 2.5, 1.5, 0.35, 0.10], // top of 3-point arc
[-12, 23.0, 2.5, 2.0, 0.33, 0.10], // left 3
[12, 23.0, 2.5, 2.0, 0.33, 0.10], // right 3
[0, 9.5, 3.5, 2.5, 0.40, 0.09], // paint
];
const W_SUM = CLUSTERS.reduce((a, c) => a + c[5], 0);
const shots = [];
for (let i = 0; i < 420; i++) {
let pick = rng() * W_SUM, ci = 0, acc = 0;
for (let j = 0; j < CLUSTERS.length; j++) {
acc += CLUSTERS[j][5];
if (pick <= acc) { ci = j; break; }
}
const [cx, cy, spx, spy, madeRate] = CLUSTERS[ci];
const [z0, z1] = bm();
const sx = cx + spx * z0;
const sy = cy + spy * z1;
if (sx < -25 || sx > 25 || sy < 0 || sy > 47) continue;
shots.push({ x: sx, y: sy, made: rng() < madeRate });
}
// Layout
const margin = { top: 80, right: 50, bottom: 30, left: 50 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;
const BKST_X = 0, BKST_Y = 5.25;
const xSc = d3.scaleLinear().domain([-25, 25]).range([0, iw]);
// Crop to [0, 32] — removes dead upper-court area; 3-pt arc peaks at ~29ft
const ySc = d3.scaleLinear().domain([0, 32]).range([ih, 0]);
const svg = d3.select("#container").append("svg")
.attr("width", width).attr("height", height);
// ClipPath keeps court elements within the plot rectangle
svg.append("defs").append("clipPath")
.attr("id", "plotClip")
.append("rect").attr("width", iw).attr("height", ih);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
.attr("clip-path", "url(#plotClip)");
// Court floor tint
const floorColor = THEME === "dark" ? "#3A2B12" : "#D4B483";
g.append("rect")
.attr("x", 0).attr("y", 0).attr("width", iw).attr("height", ih)
.attr("fill", floorColor).attr("fill-opacity", 0.18);
// Arc path helper: math-coord angles (y-axis flip handled by ySc)
function aPath(cx, cy, r, a0, a1, steps = 80) {
return Array.from({ length: steps + 1 }, (_, i) => {
const a = a0 + (a1 - a0) * i / steps;
return `${i ? "L" : "M"}${xSc(cx + r * Math.cos(a)).toFixed(1)},${ySc(cy + r * Math.sin(a)).toFixed(1)}`;
}).join("");
}
const LC = t.inkSoft;
const LW = 2;
function cLine(x1, y1, x2, y2) {
g.append("line")
.attr("x1", xSc(x1)).attr("y1", ySc(y1))
.attr("x2", xSc(x2)).attr("y2", ySc(y2))
.attr("stroke", LC).attr("stroke-width", LW);
}
// Court boundary — sidelines extend to y=47 but are clipped at y=32
cLine(-25, 0, 25, 0);
cLine(-25, 0, -25, 47);
cLine(25, 0, 25, 47);
// Paint / key
cLine(-8, 0, -8, 19);
cLine(8, 0, 8, 19);
cLine(-8, 19, 8, 19);
// Free-throw circle (6 ft radius, center at y=19)
g.append("path").attr("d", aPath(0, 19, 6, Math.PI, 0))
.attr("fill", "none").attr("stroke", LC).attr("stroke-width", LW);
g.append("path").attr("d", aPath(0, 19, 6, 0, -Math.PI))
.attr("fill", "none").attr("stroke", LC).attr("stroke-width", LW)
.attr("stroke-dasharray", "7,5");
// Backboard and basket
cLine(-3, 4, 3, 4);
g.append("circle")
.attr("cx", xSc(BKST_X)).attr("cy", ySc(BKST_Y))
.attr("r", xSc(BKST_X + 0.75) - xSc(BKST_X))
.attr("fill", "none").attr("stroke", LC).attr("stroke-width", 2.5);
// Restricted area arc (4 ft radius)
g.append("path").attr("d", aPath(BKST_X, BKST_Y, 4, Math.PI, 0))
.attr("fill", "none").attr("stroke", LC).attr("stroke-width", LW);
// Three-point line: straight corner segments + arc at 23.75 ft
cLine(-22, 0, -22, 14);
cLine(22, 0, 22, 14);
g.append("path")
.attr("d", aPath(
BKST_X, BKST_Y, 23.75,
Math.atan2(14 - BKST_Y, -22 - BKST_X),
Math.atan2(14 - BKST_Y, 22 - BKST_X),
))
.attr("fill", "none").attr("stroke", LC).attr("stroke-width", LW);
// Zone FG% annotations — rendered before markers so shots sit on top
const ZONE_LABELS = [
{ x: 0, y: 30.5, text: "Top-3 · ~35%", anchor: "middle" },
{ x: 14, y: 14.5, text: "Mid · ~38%", anchor: "start" },
{ x: 0, y: 3.2, text: "At-Rim · ~62%", anchor: "middle" },
];
ZONE_LABELS.forEach(({ x, y, text, anchor }) => {
const tx = xSc(x), ty = ySc(y);
const approxW = text.length * 7.2 + 12;
const bx = anchor === "middle" ? tx - approxW / 2
: anchor === "end" ? tx - approxW - 3
: tx + 3;
g.append("rect")
.attr("x", bx).attr("y", ty - 14).attr("width", approxW).attr("height", 18)
.attr("fill", t.elevatedBg).attr("fill-opacity", 0.72).attr("rx", 3);
g.append("text")
.attr("x", tx).attr("y", ty)
.attr("text-anchor", anchor)
.attr("fill", t.inkSoft).attr("fill-opacity", 0.90)
.style("font-size", "12px").style("font-weight", "600")
.text(text);
});
// CVD accessibility: missed shots rendered as X (cross rotated 45°), made as circles
// Shape + color dual encoding distinguishes outcome for color-vision-deficient viewers
const MARKER_AREA = 180; // sq px; visually balances r=7 circle (≈154 sq px)
const crossPath = d3.symbol().type(d3.symbolCross).size(MARKER_AREA)();
const madeArr = shots.filter(d => d.made);
const missedArr = shots.filter(d => !d.made);
// Missed: X shape
g.selectAll(".miss").data(missedArr).join("path")
.attr("d", crossPath)
.attr("transform", d => `translate(${xSc(d.x)},${ySc(d.y)}) rotate(45)`)
.attr("fill", COLOR_MISSED).attr("fill-opacity", 0.65)
.attr("stroke", t.pageBg).attr("stroke-width", 0.8);
// Made: circle (rendered on top)
g.selectAll(".make").data(madeArr).join("circle")
.attr("cx", d => xSc(d.x)).attr("cy", d => ySc(d.y))
.attr("r", 7)
.attr("fill", COLOR_MADE).attr("fill-opacity", 0.85)
.attr("stroke", t.pageBg).attr("stroke-width", 0.8);
// Legend — legend group is unclipped so append to svg with absolute position
const lgOffX = margin.left + 16, lgOffY = margin.top + 14;
const lgW = 192, lgH = 76;
svg.append("rect")
.attr("x", lgOffX - 10).attr("y", lgOffY - 12)
.attr("width", lgW).attr("height", lgH)
.attr("fill", t.elevatedBg).attr("rx", 6).attr("fill-opacity", 0.92)
.attr("stroke", t.grid).attr("stroke-width", 1);
const lgItems = [
{ label: `Made (${madeArr.length})`, color: COLOR_MADE, shape: "circle" },
{ label: `Missed (${missedArr.length})`, color: COLOR_MISSED, shape: "cross" },
];
lgItems.forEach((d, i) => {
const lx = lgOffX + 6, ly = lgOffY + 8 + i * 30;
if (d.shape === "circle") {
svg.append("circle")
.attr("cx", lx).attr("cy", ly)
.attr("r", 7).attr("fill", d.color)
.attr("stroke", t.pageBg).attr("stroke-width", 1);
} else {
svg.append("path")
.attr("d", crossPath)
.attr("transform", `translate(${lx},${ly}) rotate(45)`)
.attr("fill", d.color).attr("stroke", t.pageBg).attr("stroke-width", 1);
}
svg.append("text")
.attr("x", lgOffX + 22).attr("y", lgOffY + 13 + i * 30)
.attr("fill", t.inkSoft).style("font-size", "16px")
.text(d.label);
});
// Title
svg.append("text")
.attr("x", width / 2).attr("y", 48)
.attr("text-anchor", "middle")
.attr("fill", t.ink)
.style("font-size", "22px").style("font-weight", "600")
.text("scatter-shot-chart · javascript · d3 · anyplot.ai");
Part of Basketball Shot Chart on anyplot.ai.