Public
Edited
Dec 15
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
beeswarm = {
let el = this

const t = d3.transition().duration(1500)
const circleFill = "#8699AC";
const highlightFill = "#FF590C";
const fontColor = "#f9f9f9";
const fontFamily = "Roboto Medium";
const radius_min = 5;
const radius_max = 30;
const strokeColor = "#505050";
const strokeWidth = radius_max *.05;
const strokeMultiple = .05;
const lineHeight = 17;
const form_check = form.playerNodeStyle === "Text" ? "#005985" : "#ECECEC";
const colorLighten = 15;


//create tooltip placeholder
const tooltip = d3.select("body")
.append("div")
.attr("class", "toolTip")
.style("position", "absolute")
.style("visibility", "hidden")
.text("Placeholder")
.attr("class", "tooltip-box")
.style("background-color", "white")
.style("border", "1px solid black")
.style("border-radius", "5px") // Rounded corners
.style("padding", "2.5px"); // Add padding for margin around text


if (!el || JSON.stringify( previousPlayerList) !== JSON.stringify(form.playersSelected) || stat_select.stat[0] !== previousStatSelect) {

var fadeCircles = true;
if(previousPlayerListLength != 0){
fadeCircles = false;
}
// Update the stored value to the current playerNodeStyle
mutable previousPlayerList = form.playersSelected.slice();
mutable previousStatSelect = stat_select.stat[0];
mutable previousPlayerListLength = form.playersSelected.length;

const radiusScale = d3.scaleLinear()
.domain([d3.min(simulation.nodes(), d => d.poss), d3.max(simulation.nodes(), d => d.poss)])
.range([radius_min, radius_max ]); // Adjust the range based on your desired minimum and maximum radius


const radiusScaleMouseOver = d3.scaleLinear()
.domain([d3.min(simulation.nodes(), d => d.poss), d3.max(simulation.nodes(), d => d.poss)])
.range([radius_min*1.5, radius_max *1.5]); // Adjust the range based on your desired minimum and maximum radius

console.log("Jumping into the if")
el = DOM.svg(width, height)
const svg = d3.select(el)
el.g = svg.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`)
const xAxisGroup = el.g.append("g")
.classed("axis axis--x", true)
.attr("transform", `translate(0, ${height - margin.top - margin.bottom})`)
.call(xAxis);

el.g.append("g")
.classed("circles", true)


// create an empty selection from data binding
el.circles = el.g.select("g.circles")
.selectAll(".node")
.data(simulation.nodes(), d => d.id)
.enter()
.append("g")
.attr('class', 'node')
.attr("transform", function (d, i) {
const x = d.x;
const y = d.y;
return `translate(${x}, ${y})`;
});



// Iterate through each node
el.circles.each(function (d) {
const currentNode = d3.select(this)
const labelText = d.id;
const headshot = d.headshot;
let value = d.value;

//create circle
currentNode.append('circle')
.attr("r", d => radiusScale(d.poss)) // Set the radius based on the scale
.attr("fill", d => lightenColor(d.color, colorLighten))
.attr("stroke-width", d => radiusScale(d.POSS) * strokeMultiple)
.attr("stroke", strokeColor);



//add text
currentNode.append("text")
.style("text-anchor", "middle")
//rescale text based on size
.attr("transform", function() {
const scaleValue = radiusScale(d.poss)*0.7 / getTextRadius(getLines(labelText, lineHeight), lineHeight);
return `translate(0, 0) scale(${scaleValue})`;
})
.selectAll("tspan")
//use custom functions to split text into lines, getLines creates an array of text per line
.data(getLines(labelText, lineHeight))
.enter()
.append("tspan")
.attr("x", 0)
.attr("y", (d, i) => (i - getLines(labelText, lineHeight).length / 2 + 0.8) * lineHeight)
.style("fill", fontColor)
.style("font-family", fontFamily)
.text(m => m.text);

currentNode.append("image")
.attr("class", "headshot")
.attr("height", radiusScale(d.poss) * 2)
.attr("clip-path", d=> "circle(" + radiusScale(d.poss) + "px at " + radiusScale(d.poss) + "px " + radiusScale(d.poss) + "px)" )
.attr("href", headshot)
.attr("x", -radiusScale(d.poss)) // Adjust the image position to center it
.attr("y", -radiusScale(d.poss)*1.04)
.attr("width", radiusScale(d.poss) * 2)
.attr("preserveAspectRatio", "xMidYMid slice") // Maintain aspect ratio and fill the circle
.attr('opacity', 0);

const playersSelected = form.playersSelected;

const playersNotSelected = uniquePlayers.filter(player => !(form.playersSelected).includes(player));


playersSelected.forEach(player => {

el.circles
.filter(x => x.id == player)
.raise()
.select("circle")
.attr("fill", x => x.color)

el.circles
.filter(x => x.id == player)
.raise()
.select("image")
.attr('opacity', 1)

el.circles
.filter(x => x.id == player)
.select("text")
.attr("opacity", 0)
})

/*
if(fadeCircles){
playersNotSelected.forEach(player => {
el.circles
.filter(x => x.id == player)
.select("circle")
.attr("fill", circleFill);

})

}*/

currentNode
.style("cursor", "pointer") // Set the cursor to pointer for the entire group
.on("mouseover", (event, d) => {



mutable hoveredPlayer = d.id;
console.log("Hovered Player is " + hoveredPlayer);

// Increase the radius of the circle to 1.5 times on mouseover
d3.select(this)
.select("circle")
.transition()
.duration(200)
.attr("r", d => radiusScaleMouseOver(d.poss))
.attr("stroke-width", d => radiusScaleMouseOver(d.poss) * strokeMultiple)
.attr("fill", d => d.color);


// Change the text color to red on mouseover
d3.select(this).select("text")
.transition()
.duration(200)
.attr("opacity", 0)

//Make Image Radius Bigger on MouseOver
d3.select(this).select("image")
.transition()
.duration(200)
.attr('opacity', 1)
.attr("height", radiusScaleMouseOver(d.poss) * 2)
.attr("clip-path", d=> "circle(" + radiusScaleMouseOver(d.poss) + "px at " + radiusScaleMouseOver(d.poss) + "px " + radiusScaleMouseOver(d.poss) + "px)" )
.attr("href", headshot)
.attr("x", -radiusScaleMouseOver(d.poss)) // Adjust the image position to center it
.attr("y", -radiusScaleMouseOver(d.poss)*1.04)
.attr("width", radiusScaleMouseOver(d.poss) * 2)
.attr("preserveAspectRatio", "xMidYMid slice"); // Maintain aspect ratio and fill the circle
//raise circle and text element on hover
d3.select(event.currentTarget).raise();




//make tooltip visible, use custom function to return info about player
if(stat_select.stat[0].startsWith("efg")){
tooltip
.style("visibility", "visible")
.html(`<b>${d.id}:</b> ${(d.value * 100).toFixed(2)} eFG% on ${((d.poss).toFixed(1))} FGA / 75`)
}
else{
tooltip
.style("visibility", "visible")
.html(`<b>${d.id}:</b> ${(d.value).toFixed(2)} PPP on ${((d.poss).toFixed(1))} Poss / 75`)
}
})
.on("mouseout", function() {
// Return to the regular size on mouseout
if (!(form.playersSelected).includes(d.id)) {
console.log(form.playersSelected)
console.log(d.id)
mutable hoveredPlayer = null;
d3.select(this)
.select("circle")
.transition()
.duration(200)
.attr("r", d => radiusScale(d.poss))
.attr("stroke-width", d => radiusScale(d.poss) * strokeMultiple)
.attr("fill", d => lightenColor(d.color, colorLighten));

d3.select(this).select("image")
.transition()
.duration(200)
.attr('opacity', 0)
.attr("height", radiusScale(d.poss) * 2)
.attr("clip-path", d=> "circle(" + radiusScale(d.poss) + "px at " + radiusScale(d.poss) + "px " + radiusScale(d.poss) + "px)" )
.attr("href", headshot)
.attr("x", -radiusScale(d.poss)) // Adjust the image position to center it
.attr("y", -radiusScale(d.poss)*1.04)
.attr("width", radiusScale(d.poss) * 2)
.attr("preserveAspectRatio", "xMidYMid slice"); // Maintain aspect ratio and fill the circle
// Change the text color back to the original color on mouseout
d3.select(this).select("text")
.transition()
.duration(200)
.attr("opacity", 1);


}
else{
d3.select(this)
.select("circle")
.transition()
.duration(200)
.attr("r", d => radiusScale(d.poss))
.attr("stroke-width", d => radiusScale(d.poss) * strokeMultiple)
.attr("fill", d => d.color);


d3.select(this).select("image")
.transition()
.duration(200)
.attr("height", radiusScale(d.poss) * 2)
.attr("clip-path", d=> "circle(" + radiusScale(d.poss) + "px at " + radiusScale(d.poss) + "px " + radiusScale(d.poss) + "px)" )
.attr("href", headshot)
.attr("x", -radiusScale(d.poss)) // Adjust the image position to center it
.attr("y", -radiusScale(d.poss)*1.04)
.attr("width", radiusScale(d.poss) * 2)
.attr("preserveAspectRatio", "xMidYMid slice"); // Maintain aspect ratio and fill the circle
}
//hide tooltip after hovering
tooltip.style("visibility", "hidden");

})
.on('mousemove', (event) => {
tooltip.style("top", (event.pageY - 50) + "px").style("left", (event.pageX + 10) + "px")}
);
})
}
else {

el.g.select("g.axis.axis--x")
.transition(t)
.call(xAxis)
const update = d3.select(el)
.selectAll(".node")
.data(simulation.nodes(), d => d.id)

update
.transition(t)
.duration(1000)
.attr("transform", function (d, i) {
const x = d.x;
const y = d.y;
return `translate(${x}, ${y})`;
});


el.circles = update
}
return el
}
Insert cell
import {multiAutoSelect} from "@john-guerra/multi-auto-select"

Insert cell
circle_radius = 20;

Insert cell
simulationForceColide = circle_radius*0.75
Insert cell
data = observable_off_ball.map(d => ({
value: Number(d[stat_select.stat[0]]),
id: d.PLAYER_NAME, headshot:d.headshot_url,
poss: Number(d[stat_select.stat[1]]),
color:d.primary_color }))
.filter(d => d.poss > min_poss);
Insert cell
// Extract unique ids and sort alphabetically
uniquePlayers = Array.from(new Set(observable_off_ball.map(d => d.PLAYER_NAME))).sort();

Insert cell
mutable previousStatSelect = null;
Insert cell
mutable previousPlayerListLength = 0;
Insert cell
mutable previousPlayerList = [];
Insert cell
mutable hoveredPlayer = null;
Insert cell
mutable previousPlayerNodeStyle = null;
Insert cell
margin = ({ top: 10, right: 20, bottom: 30, left: 20 })
Insert cell
xAxis = {
const xAxisTickFormat = stat_select.stat[0].startsWith("efg")
? d3.format(".0%") // Use percentage format if true
: d3.format(".2f"); // Use fixed-point notation with two decimal places if false

const xAxis = d3.axisBottom(xScale)
.ticks(20)
.tickFormat(xAxisTickFormat);

return xAxis; // Output the xAxis
}
Insert cell
xMin = d3.min(data, d => d.value);
Insert cell
xMax = d3.max(data, d => d.value);
Insert cell
xScale = d3.scaleLinear()
.range([0, width - margin.left - margin.right])
.domain([xMin - 0.1*(xMax - xMin), xMax + 0.1*(xMax - xMin)])
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
function getKeyByValue(map, searchValue) {
for (let [key, value] of map.entries()) {
if (value === searchValue) {
return key;
}
}
// Return undefined if the value is not found
return undefined;
}



Insert cell
mutable min_poss = stat_select.stat[0].startsWith("efg") ? 1 : 0.5
Insert cell
mutable volume_type = stat_select.stat[0].startsWith("efg") ? "FGA/75" : "Playtype Poss/75"
Insert cell
mutable efficiency_type = stat_select.stat[0].startsWith("efg") ? "eFG%" : "PPP"
Insert cell
stat_select_map = new Map([
["Teammate-Created Shots (Touch Time <2 Seconds)", ['efg_not_self_created', 'FGA_not_self_created']],
["Self-Created Shots (Touch Time 2+ Seconds)", ['efg_self_created', 'FGA_self_created']],
["Cuts", ['PPP_Cut', 'POSS_Cut']]
]);
Insert cell
height = 720
Insert cell
simulation = {
const radiusScale = d3.scaleLinear()
.domain([d3.min(data, d => d.poss), d3.max(data, d => d.poss)])
.range([5, 30]);


const sim = d3.forceSimulation(data)
.force("x", d3.forceX(d => xScale(d.value)).strength(0.3))
.force("y", d3.forceY(height/2).strength(0.3))
.force("collide", d3.forceCollide(d => radiusScale(d.poss) + 2.5))

.stop()
for (let i = 0; i < 120; ++i) {
sim.tick();
}
return sim
}
Insert cell
import {getTextRadius, getLines} from "@tashapiro/tv-shows-cast-connections"
Insert cell
function lightenColor(hexColor, amount) {
// Parse hex color
let r = parseInt(hexColor.substring(1, 3), 16);
let g = parseInt(hexColor.substring(3, 5), 16);
let b = parseInt(hexColor.substring(5, 7), 16);

// Calculate blend with white
const blendFactor = amount / 100;
r = Math.round((1 - blendFactor) * 255 + blendFactor * r);
g = Math.round((1 - blendFactor) * 255 + blendFactor * g);
b = Math.round((1 - blendFactor) * 255 + blendFactor * b);

// Convert RGB to hex
return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, '0')}`;
}
Insert cell
observable_off_ball
Insert cell
observable_off_ball@1.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more