Published
Edited
Sep 26, 2022
Fork of shot scatter
Insert cell
Insert cell
shot = Inputs.button("Shot")
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

let y = 'SHOT_against';
let x = 'SHOT_for';
let keep = [y, x];
let sliced = contrast.map(row => ['team', ...keep].reduce((acc, v) => ({ ...acc, [v]: row[v] }), {}));
const dots = svg.append("g");
let xScale = d3.scaleLinear()
.domain(d3.extent(sliced, d => Number(d[x]))).nice()
.range([margin.left, width - margin.right]);

let yScale = d3.scaleLinear()
.domain(d3.extent(sliced, d => Number(d[y]))).nice()
.range([height - margin.bottom, 0]);

let mainxAxis = svg.append("g").attr("transform", `translate(0,${height - margin.bottom})`);

let mainyAxis = svg.append("g").attr("transform", `translate(${margin.left},0)`);
let meandots = mainyAxis.append('g');

const meanAxis = object => object
.attr("transform", `translate(0,${yScale(25)})`)
.call(d3.axisRight(yScale)
.ticks(2)
.tickSize(width - margin.left - margin.right))
.style('stroke', 'red')
// .tickFormat(formatTick))
.call(g => g.select(".domain")
.remove())
.call(g => g.selectAll(".tick:not(:first-of-type) line")
.attr("stroke-opacity", 1.0)
.attr("stroke-dasharray", "6"))
.call(g => g.selectAll(".tick text")
.text("the mean line")
.attr("x", 4)
.attr("dy", -4));

let yAxis = d3
.axisLeft(yScale)
.tickFormat(formatTicks)

let xAxis = d3
.axisBottom(xScale)
.tickFormat(formatTicks)
function sety()
{
y = 'PASS_for';
keep = [y, x];
sliced = contrast.map(row => ['team', ...keep].reduce((acc, v) => ({ ...acc, [v]: row[v] }), {}));
console.log(sliced);
update(sliced, y, x);
}

d3.select(shot).on('click', sety);
d3.select(pass).on('click', sety);

const padding = 20;

function update(data, y, x)
{
yScale = d3.scaleLinear()
.domain(d3.extent(data, d => Number(d[y]))).nice()
.range([height - margin.bottom, 0])

xScale = d3.scaleLinear()
.domain(d3.extent(data, d => Number(d[x]))).nice()
.range([margin.left, width - margin.right])
yAxis = d3
.axisLeft(yScale)
.tickFormat(formatTicks)

xAxis = d3
.axisBottom(xScale)
.tickFormat(formatTicks)

const colorScale = d3.scaleSequential(d3.interpolateYlOrRd)
// .range(d3.interpolateViridis)
.domain([d3.min(data.map(d => Number(d[x])*Number(d[y]))),d3.max(data.map(d => Number(d[x])*Number(d[y])))]);

function addLabel(axis, label, xposition)
{
axis
.selectAll('.tick:last-of-type text')
.clone()
.text(label)
.attr('x', xposition)
.style('text-anchor', 'start')
.style('font-weight', 'semi-bold')
.style('fill', 'black')
}
mainxAxis
.call(xAxis)
.call(addLabel, x, 25);

mainyAxis
.call(yAxis)
.call(addLabel, y, 25);;

meandots.call(meanAxis);
dots
// .call(meanAxis)
.selectAll(".scatter")
.data(data, d=> d.team)
// .join("circle")
// .attr('class', 'scatter')
// .attr('hello', d => console.log(d[x]))
// .attr("cx", d => xScale(Number(d[x])))
// .attr("cy", d => yScale(Number(d[y])))
// .attr("r", 5)
// .attr("id", (d, i) => `${i}`)
// .style('stroke', 'dodgerblue')
// .style("fill", (d) => colorScale((Number(d[x])*Number(d[y]))))
// .style("fill-opacity", 0.25)
// .on("mouseenter", showData)
// .on('mouseleave', (event, d) => {
// dots.selectAll('.scatter').style('opacity', 1.0);
// d3.selectAll('.labelGroup').remove();
// });
.join(
(enter) => enter
.append('circle')
.attr('class', 'scatter')
.attr("cx", d => xScale(Number(d[x])))
.attr("cy", d => yScale(Number(d[y])))
.attr("r", 5)
.attr("id", (d, i) => `${i}`)
.style('stroke', 'dodgerblue')
.style("fill", (d) => colorScale((Number(d[x])*Number(d[y]))))
.style("fill-opacity", 0.25),
(update) => update
.attr("cx", d => xScale(Number(d[x])))
.attr("cy", d => yScale(Number(d[y]))),
(exit) => exit.remove())
.on("mouseenter", showData)
.on('mouseleave', (event, d) => {
dots.selectAll('.scatter').style('opacity', 1.0);
d3.selectAll('.labelGroup').remove();
});

function showData(event, d)
{
const current = d3.select(this);
const currentTeam = d.team;
console.log("team", currentTeam)
dots
.selectAll('.scatter')
.style("opacity", function(t) {
if (t.team !== currentTeam)
{
return 0.2;
}
})

let xpos = parseFloat(current.attr('cx'));
let ypos = parseFloat(current.attr('cy'));
console.log("Xpositions {}", xpos);

//const labelgroup = current.join('g').attr('class', 'labelGroup');
const labelgroup = current.selectAll('.labelGroup').join('g').attr('class', 'labelGroup');
//also tried to append this to labelgroup but I am not sure about the relative location
// const label = svg
const label = svg
.append('text')
.attr('class', 'bar-label text-sm labelGroup labelText')
.attr('fill', 'black')
// .attr('x', 0)
// .attr('y', 0)
.attr('x', xpos + padding)
.attr('y', ypos + padding);

const team = label
.append('tspan')
.style('text-anchor', 'start')
.style('font-weight', 'bold')
.style('font-family', 'Arial')
.text(`Team: ${d.team}`);

const textheight = team.node().getBBox().height;
// const textheight = 10;
label
.append('tspan')
.attr('class', 'bar-label text-sm')
.attr('fill', 'black')
.style('text-anchor', 'start')
.style('font-weight', 'bold')
.style('font-family', 'Arial')
.attr('x', xpos + padding)
.attr('dy', textheight)
.text(`Shots conceded: ${Number(d.against)}`);

label
.append('tspan')
.attr('class', 'bar-label text-sm')
.attr('fill', 'black')
.style('text-anchor', 'start')
.style('font-weight', 'bold')
.style('font-family', 'Arial')
.attr('x', xpos + padding)
.attr('dy', textheight)
.text(`Shots attempted: ${Number(d[x])}`)

const labelBBox = label.node().getBBox();

const tooltipWidth = labelBBox.width + padding * 2;
const tooltipHeight = labelBBox.height + padding;

console.log(labelBBox);
console.log(tooltipHeight);
//const tooltipWidth = 200;
//const tooltipHeight = 200;
const tooltip = svg
//.append('rect')
.insert('rect', '.labelText')// Why does this not work?
.attr('id', 'tooltip')
.attr('class', 'labelGroup')
.attr('y', ypos)
.attr('x', xpos)
.attr('rx', 5)
.attr('ry', 5)
// .attr('x', (xpos + tooltipWidth > width) ? xpos - tooltipWidth : xpos)
// .attr('y', (ypos + tooltipHeight > height) ? ypos - tooltipHeight : ypos)
.style('fill', 'black')
.style('opacity', 0.5)
.attr('width', tooltipWidth)
.attr('height', tooltipHeight);
}

}
update(sliced, y, x);
return svg.node();
}
Insert cell
contrast = FileAttachment("contrast.csv").csv()
Insert cell
shotdf = FileAttachment("shotdf.csv").csv()
Insert cell
height = 800
Insert cell
contrast.map(d => d['SHOT_for'])
Insert cell
margin = ({top: 100, right: 120, bottom: 40, left: 80})
Insert cell
require('d3')
Insert cell
// Drawing utilities.
function formatTicks(d) {
return d3
.format('~s')(d)
.replace('M', ' mil')
}
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