plot = {
let svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.style("display", "block");
const view = svg.append("g")
.attr("class", "view")
.attr("x", 0.5)
.attr("y", 0.5)
.attr("width", width - 1)
.attr("height", height - 1);
const gGrid = svg.append("g")
.attr("clip-path", "url(#clip)");
let x = d3.scaleLinear()
.domain(d3.extent(values, d => d.x))
.range([margin.left, width - margin.right]);
let y = d3.scaleLinear()
.domain(d3.extent(values, d => d.y))
.range([height - margin.bottom, margin.top]);
const pointerover = function(e) {
tooltip.style("display", "block");
d3.select(this)
.attr("r", (pointProps.size + 3) / currentTransform.k)
.style("opacity", 1);
}
const pointerleave = function(e) {
tooltip.style("display", "none");
d3.select(this)
.attr("r", (pointProps.size) / currentTransform.k)
.style("opacity", pointProps.opacity)
}
const pointermove = function(e, d) {
const xpos = d3.pointer(e, svg.node())[0];
const ypos = d3.pointer(e, svg.node())[1];
const row = tableData[d.index];
const values = Object.keys(row).map(k => [k, row[k]]);
const text = tooltip.selectAll("text")
.call(text => text
.selectAll("tspan")
.data(values)
.join("tspan")
.attr("x", 0)
.attr("y", (_, i) => `${i * 1.1}em`)
.attr("font-weight", (_, i) => i ? null : "bold")
.text(d => d[0] + ": " + d[1]));
const padding = 5;
const { y: texty, width: textw, height: texth } = text.node().getBBox();
const tipx = Math.max(padding, Math.min(xpos - textw / 2, width - textw - 2 * padding));
const tipy = Math.max(padding, Math.min(ypos - texth - 15, height - texth - 2 * padding));
tooltip.attr("transform", `translate(${tipx},${tipy})`);
text.attr("transform", `translate(${0},${-texty})`);
tooltip.selectAll("rect").attr("x", -padding)
.attr("y", -padding)
.attr("width", textw + 2 * padding)
.attr("height", texth + 2 * padding);
}
// Axes.
const xAxis = d3.axisBottom(x).ticks(width / 80);
const yAxis = d3.axisLeft(y).ticks(height / 50);
const gX = svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(xAxis)
.call(g => g.append("text")
.attr("x", width)
.attr("y", margin.bottom - 4)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(plotProps.x + " →"));
const gY = svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(yAxis)
.call(g => g.append("text")
.attr("x", -margin.left)
.attr("y", 10)
.attr("fill", "currentColor")
.attr("text-anchor", "start")
.text("↑ " + plotProps.y));
// Append a clip rectangle to prevent drawing off-axis points when zooming/panning.
const clip = svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", margin.left)
.attr("y", margin.top)
.attr("height", height - margin.top - margin.bottom)
.attr("width", width - margin.left - margin.right);
// Append points in the scatterplot.
const dots = svg
.append("g")
.attr("clip-path", "url(#clip)") // Set clip rectangle for all elements in this group.
.selectAll("dot")
.data(values)
.join("circle")
.attr("cx", e => x(e.x))
.attr("cy", e => y(e.y))
.attr("r", pointProps.size)
.style("fill", "steelblue")
.style("opacity", pointProps.opacity)
// .append('title')
// .text((d) => `Sales were ${d.x} in ${d.y}`)
.on("pointerenter", pointerover)
.on("pointermove", pointermove)
.on("pointerleave", pointerleave)
.on("touchstart", event => event.preventDefault());
// Tooltip handling.
const tooltip = svg.append("g")
.style("pointer-events", "none")
.style("display", "none");
tooltip.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("rx", 6)
.attr("ry", 6)
.attr("height", 100)
.attr("width", 100)
.attr("fill", "white")
.attr("fill-opacity", 0.75)
.style("stroke", "gray")
tooltip.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("fill", "black")
.attr("text-anchor", "start")
.style("font-size", "14px");
let currentTransform = { k: 1, x: 0, y: 0 };
const zoomed = function({ transform }) {
const zx = transform.rescaleX(x).interpolate(d3.interpolateRound);
const zy = transform.rescaleY(y).interpolate(d3.interpolateRound);
currentTransform = transform;
view.attr("transform", transform);
//tooltip.attr("transform", transform);
dots
.attr("transform", transform)
.attr("stroke-width", 1 / transform.k)
.attr("r", pointProps.size / transform.k);
gX.call(xAxis.scale(transform.rescaleX(x)));
gY.call(yAxis.scale(transform.rescaleY(y)));
gGrid.call(grid, zx, zy);
}
// prevent scrolling then apply the default filter
const filter = function(event) {
event.preventDefault();
return (!event.ctrlKey || event.type === 'wheel') && !event.button;
}
const zoom = d3.zoom()
.scaleExtent([0.5, 32])
.filter(filter)
.on("zoom", zoomed);
const reset = function() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
}
// Call zoom with identity transformation to initialize grid.
svg.call(zoom).call(zoom.transform, d3.zoomIdentity);
return Object.assign(svg.call(zoom).node(), {reset});
}