Public
Edited
Apr 25
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';
const radius_tt_multiplier = 2;


// 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", "16px") // 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()/1.5;

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", "14px"); // 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", "14px") // 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
Insert cell
gamelog = d3.csv(`https://raw.githubusercontent.com/Saurabh1975/player_match_up_r/main/Data/gamelog - ${season_selected}.csv`)
Insert cell
Insert cell
Insert cell
viewof season_selected = Inputs.select(
["2024-25", "2023-24", "2022-23"],
{label: "", value: "2024-25"}
)
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

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more