Public
Edited
May 14, 2024
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
matchup_chart_player = {

const team_select = chart_type ? 'off_team_name' : 'def_team_name';
const x_player_field = player_chart_type ? 'DEF_PLAYER_NAME' : 'OFF_PLAYER_NAME';
const x_player_field_headshot = player_chart_type ? 'DEF_PLAYER_HEADSHOT' : 'OFF_PLAYER_HEADSHOT';

const x_player_display = player_chart_type ? 'DEF_PLAYER_NAME_LAST' : 'OFF_PLAYER_NAME_LAST';

const player_type_select = player_chart_type ? 'OFF_PLAYER_NAME' : 'DEF_PLAYER_NAME';
const x_rect_start = player_chart_type ? 'matchup_min_start' : 'matchup_min_start_def';
const x_rect_end = player_chart_type ? 'matchup_min_end' : 'matchup_min_end_def';
const rect_opacity = player_chart_type ? 'pct_matchup_time_scaled' : 'pct_matchup_time_scaled_def';


// Specify the chart’s dimensions (except for the height).
const width = 928;
const marginTop = 30;
const marginRight = 10;
const marginBottom = 50;
const marginLeft = 150;

const display_cutoff = 0.75;


const selected_matchup_player = matchups.filter(d => selected_matchup_games.includes(d.game_id)
&& d[player_type_select] === selectedPlayer);

const off_team_color = selected_matchup_player[0].off_team_color;
const def_team_color = selected_matchup_player[0].def_team_color;

const team_fill_color = player_chart_type ? def_team_color :off_team_color;
const team_fill_color_lighter = lightenColor(team_fill_color, 25);


// Sort player names by the aggregated maximum MATCHUP_MIN in descending order
const game_list = selected_matchup_player.map(d => d.game_number).sort();


const bar_width_factor = 50;
// Compute the height from the number of stacks.
const height = game_list[0].length * bar_width_factor + marginTop + marginBottom;

// Prepare the scales for positional and color encodings.
const x = d3.scaleLinear()
.domain([0, d3.max(selected_matchup_player, d => d[x_rect_end])])
.range([marginLeft, width - marginRight]);

const y = d3.scaleBand()
.domain(game_list) // Set the domain to the list of player names
.range([marginTop, height - marginBottom]) // Set the visual range of the scale
.padding(.65); // Set padding between bands



// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");



//create tooltip placeholder
const tooltip = d3.select("body")
.append("div")
.attr("class", "toolTip")
.style("position", "absolute")
.style("visibility", "hidden")
.text("Placeholder")
.style("font-family", "Roboto, sans-serif") // Set the font family to Roboto
.style("font-size", "11px") // Set the font size to 10px
.style("font-weight", "500") // Roboto Black
.attr("class", "tooltip-box")
.style("background-color", "white")
.style("border", "1px solid black")
.style("border-radius", "5px") // Rounded corners
.style("padding", "2px"); // Add padding for margin around text

// Append "Minutes" label at the bottom
svg.append("text")
.attr("x", width / 2)
.attr("y", height - marginBottom / 2 + 5)
.attr("text-anchor", "middle")
.attr("fill", "black")
.style("font-family", "'Roboto', sans-serif") // Explicitly setting the font family
.style("font-size", "14px") // Smaller font size
.style("font-weight", "500") // Roboto Black
.text("Minutes");




// Append a group for each series, and a rect for each element in the series
const groups = svg.append("g")
.selectAll("g")
.data(selected_matchup_player)
.join("g")
.attr("transform", d => `translate(${x(d[x_rect_start])}, ${y(d.game_number)})`);

groups.each(function(d) {
const group = d3.select(this)
.on("mouseover", function(event, d) {
// Highlight all rects with the same x_player_field

tooltip
.style("visibility", "visible")
.html(`${d.DEF_PLAYER_NAME} defended ${(d.OFF_PLAYER_NAME)} for <b>${((d.MATCHUP_MIN).toFixed(1))} Minutes</b>. That made up <b>${(d.percentage_total_time_both_on*100).toFixed(1)}%</b> of the time both players were on`)

d3.selectAll('rect')
.attr("fill", '#909090');

d3.selectAll(`.rect-player-${d[x_player_field].replace(/\s+/g, '-')}`)
.attr("fill", team_fill_color)
.attr("opacity", 1); // Set opacity to full on hover

svg.circles
.filter(x => x[x_player_field] !== d[x_player_field])
.select("image")
.attr("filter", "grayscale(0.9)")

svg.circles
.filter(x => x[x_player_field] !== d[x_player_field])
.select('circle')
.attr("stroke", '#404040')

d3.selectAll(`.text-player-${d[x_player_field].replace(/\s+/g, '-')}`)
.attr("fill", 'white'); // Ensuring this selector is correct




d3.selectAll(`.circle-player-playerChart-${d.DEF_PLAYER_NAME.replace(/\s+/g, '-')}`)
.attr("fill", team_fill_color_lighter)
.attr("opacity", 1)
.attr("stroke", team_fill_color)
.attr("r", radius*1.5)
.raise(); // Set opacity to full on hover

d3.selectAll(`.headshot-player-playerChart-${d.DEF_PLAYER_NAME.replace(/\s+/g, '-')}`)
.attr("width", radius*2*1.5)
.attr("height", radius*2*1.5)
.attr("opacity", 1)
.attr("filter", "grayscale(0)")
.attr("x", -radius*1.5) // Adjust the image position to center it
.attr("y", -radius*1.5*1.04)
.attr("clip-path", "circle(" + radius*1.5 + "px at " + radius*1.5 + "px " + radius*1.5 + "px)" )
.raise(); // Set opacity to full on hover
})
.on("mouseout", function(event, d) {
// Reset the fill color and opacity of all rects with the same x_player_field

tooltip
.style("visibility", "hidden")
d3.selectAll('rect')
.attr("fill", team_fill_color)
.attr("opacity", d => d[rect_opacity]); // Reset to default scaled opacity

svg.circles
.select('circle')
.attr("fill", background_fill)
.attr("stroke", team_fill_color)
.attr('opacity', x => x.MATCHUP_MIN > display_cutoff ? 1 : 0)
.attr("r", radius); // Set opacity to full on hover
svg.circles
.select("image")
.attr("width", radius*2)
.attr("height", radius*2)
.attr('opacity', x => x.MATCHUP_MIN > display_cutoff ? 1 : 0)
.attr("filter", "grayscale(0)")
.attr("x", -radius) // Adjust the image position to center it
.attr("y", -radius*1.04)
.attr("clip-path", "circle(" + radius + "px at " + radius + "px " + radius + "px)" ); // Set opacity to full on hover

d3.selectAll(`.text-player-${d.DEF_PLAYER_NAME.replace(/\s+/g, '-')}`)
.attr("fill", d => d[rect_opacity] > 0.3 ? 'white' : 'white'); // Reset text color appropriately
})
.on('mousemove', (event) => {
tooltip.style("top", (event.pageY - 50) + "px").style("left", (event.pageX + 10) + "px")}
);

// Append rectangle
group.append("rect")
.attr("class", `rect-player-${d[x_player_field].replace(/\s+/g, '-')}`) // Assign class for selection
.attr("height", y.bandwidth())
.attr("width", d => x(d.MATCHUP_MIN) - x(0))
.attr("fill", team_fill_color) // Default fill, adjust as needed
.attr("opacity", d => d[rect_opacity]); // Opacity based on scaled matchup time

// Add conditional text only if MATCHUP_MIN > 1
if (d.MATCHUP_MIN > display_cutoff) {
group.append("text")
.attr("class", `text-player-${d[x_player_field].replace(/\s+/g, '-')}`)
.attr("x", d => (x(d.MATCHUP_MIN) - x(0)) / 2)
.attr("y", y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(d[x_player_display]) // Assuming this is a valid data property
.attr("fill", d[rect_opacity] > 0.3 ? 'white' : 'white') // Initial fill based on condition
.style("font-size", "12px")
.style("font-weight", "700")
.style("font-family", "Roboto Slab");

}
});




svg.append("g")
.classed("circles", true)
// create an empty selection from data binding
svg.circles = svg.select("g.circles")
.selectAll(".node")
.data(selected_matchup_player, d => d[x_player_field])
.enter()
.append("g")
.attr("transform",
d => `translate(${x(d[x_rect_start] + d.MATCHUP_MIN/2)},
${y(d.game_number) - y.bandwidth()*.625})`);

const background_fill = "#ECECEC";
const radius = y.bandwidth()/2;

svg.circles.each(function (d) {
const currentNode = d3.select(this)
const labelText = d[x_player_field];
const headshot = d[x_player_field_headshot];

// let value = d.value;

if(d.MATCHUP_MIN > display_cutoff){
//create circle
currentNode.append('circle')
.attr("r", radius)
.attr('opacity', d.MATCHUP_MIN > display_cutoff ? 1 : 0)
.attr("fill", background_fill)
.attr("stroke-width", bar_width_factor*0.025)
.attr("stroke", team_fill_color)
.attr("class", d => `circle-player-playerChart-${d[x_player_field].replace(/\s+/g, '-')}`);
currentNode.append("image")
.attr("class", "headshot")
.attr("class", d => `headshot-player-playerChart-${d[x_player_field].replace(/\s+/g, '-')}`)
.attr("height", radius * 2)
.attr('opacity', d.MATCHUP_MIN > display_cutoff ? 1 : 0)
.attr("clip-path", d=> "circle(" + radius + "px at " + radius + "px " + radius + "px)" )
.attr("href", headshot)
.attr("x", -radius) // Adjust the image position to center it
.attr("y", -radius*1.04)
.attr("width", radius * 2)
.attr("preserveAspectRatio", "xMidYMid slice");

}

});

const xAxis = svg.append("g")
.attr("transform", `translate(0,${height - marginBottom})`)
.call(d3.axisBottom(x).ticks(width / 100, "s")); // Adjust the tick count as needed
// Remove the axis line
xAxis.call(g => g.selectAll(".domain").remove());
// Style the tick labels for the x-axis
xAxis.selectAll(".tick text")
.style("font-family", "'Roboto Mono', monospace") // Set the font family to Roboto Mono
.style("font-size", "12px"); // Set the font size to 12px

// Append the vertical axis.
const yAxis = svg.append("g")
.attr("transform", `translate(${marginLeft},0)`)
.call(d3.axisLeft(y).tickSizeOuter(0));
// Remove the axis line
yAxis.call(g => g.selectAll(".domain").remove());
// Style the tick labels
yAxis.selectAll(".tick text")
.style("font-family", "'Roboto', sans-serif") // Set the font family to Roboto
.style("font-weight", "700") // Set the weight to 900 for Roboto Black
.style("font-size", "12px") // Set the font size to 14px
.style("fill", "#404040"); // Set the font size to 14px

// Return the chart with the color scale as a property (for the legend).
return Object.assign(svg.node());
}
Insert cell
#### Data Import
Insert cell
gamelog = d3.csv("https://raw.githubusercontent.com/Saurabh1975/player_match_up_r/main/Data/gamelog.csv")
Insert cell
Insert cell
Insert cell
viewof selectedTeam = Inputs.select(team_names)

Insert cell
viewof selectedGame = Inputs.select(game_ids)

Insert cell
viewof chart_type = toggleSwitch({
textOn: selectedTeam + ' - Offense',
textOff: selectedTeam + ' - Defense'
})
Insert cell
Insert cell
Insert cell
Insert cell
team_names = Array.from(new Set(gamelog.map(d => d.TEAM_NAME))).sort();
Insert cell
Insert cell
viewof selectedMatchup = Inputs.select(matchup_list)

Insert cell
viewof selectedPlayer = Inputs.select(player_list)

Insert cell
Insert cell
Insert cell
selected_matchup_games = Array.from(new Set(matchups.filter(d => d.MATCHUP === selectedMatchup).map(d => d.game_id)))
Insert cell
Insert cell
matchup_list = Array.from(new Set(gamelog.filter(d => d.TEAM_NAME === selectedTeam).map(d => d.MATCHUP))).sort();
Insert cell
Array.from(new Set(matchups.filter(d => d.MATCHUP === selectedMatchup)))
Insert cell
opp_name = gamelog.find(d => d.matchup_full === selectedGame).OPP_NAME
Insert cell
team_color_title = gamelog.find(d => d.matchup_full === selectedGame).TEAM_COLOR
Insert cell
opp_color_title = gamelog.find(d => d.matchup_full === selectedGame).OPP_COLOR
Insert cell
data = {
const data = await FileAttachment("us-population-state-age.csv").csv({typed: true});
return data.columns.slice(1).flatMap((age) => data.map((d) => ({state: d.name, age, population: d[age]})));
}
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
import { toggleSwitch } from '@chrispahm/toggle-switch-input-button'

Insert cell
import {legend} from "@d3/color-legend"
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