function DotPlot(data, {
x = ([x]) => x,
y = ([, y]) => y,
z = () => 1,
r = 3.5,
xFormat,
marginTop = 30,
marginRight = 30,
marginBottom = 10,
marginLeft = 30,
width = 640,
height,
xType = d3.scaleLinear,
xDomain,
xRange = [marginLeft, width - marginRight],
xLabel,
yDomain,
yRange,
yPadding = 1,
zDomain,
colors,
stroke = "currentColor",
strokeWidth,
strokeLinecap,
strokeOpacity,
duration: initialDuration = 250,
delay: initialDelay = (_, i) => i * 10,
} = {}) {
const X = d3.map(data, x);
const Y = d3.map(data, y);
const Z = d3.map(data, z);
// Compute default domains, and unique them as needed.
if (xDomain === undefined) xDomain = d3.extent(X);
if (yDomain === undefined) yDomain = Y;
if (zDomain === undefined) zDomain = Z;
yDomain = new d3.InternSet(yDomain);
zDomain = new d3.InternSet(zDomain);
// Omit any data not present in the y- and z-domains.
const I = d3.range(X.length).filter(i => yDomain.has(Y[i]) && zDomain.has(Z[i]));
// Compute the default height.
if (height === undefined) height = Math.ceil((yDomain.size + yPadding) * 16) + marginTop + marginBottom;
if (yRange === undefined) yRange = [marginTop, height - marginBottom];
// Chose a default color scheme based on cardinality.
if (colors === undefined) colors = d3.schemeSpectral[zDomain.size];
if (colors === undefined) colors = d3.quantize(d3.interpolateSpectral, zDomain.size);
// Construct scales and axes.
const xScale = xType(xDomain, xRange);
const yScale = d3.scalePoint(yDomain, yRange).round(true).padding(yPadding);
const color = d3.scaleOrdinal(zDomain, colors);
const xAxis = d3.axisTop(xScale).ticks(width / 80, xFormat);
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
svg.append("g")
.attr("transform", `translate(0,${marginTop})`)
.call(xAxis)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("y2", height - marginTop - marginBottom)
.attr("stroke-opacity", 0.1))
.call(g => g.append("text")
.attr("x", width - marginRight)
.attr("y", -22)
.attr("fill", "currentColor")
.attr("text-anchor", "end")
.text(xLabel));
const g = svg.append("g")
.attr("text-anchor", "end")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll()
.data(d3.group(I, i => Y[i]))
.join("g")
.attr("transform", ([y]) => `translate(0,${yScale(y)})`);
g.append("line")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-opacity", strokeOpacity)
.attr("x1", ([, I]) => xScale(d3.min(I, i => X[i])))
.attr("x2", ([, I]) => xScale(d3.max(I, i => X[i])));
g.selectAll("circle")
.data(([, I]) => I)
.join("circle")
.attr("cx", i => xScale(X[i]))
.attr("fill", i => color(Z[i]))
.attr("r", r);
g.append("text")
.attr("dy", "0.35em")
.attr("x", ([, I]) => xScale(d3.min(I, i => X[i])) - 6)
.text(([y]) => y);
return Object.assign(svg.node(), {
color,
update(yDomain, {
duration = initialDuration, // duration of transition
delay = initialDelay, // delay of transition
} = {}) {
yScale.domain(yDomain);
const t = g.transition().duration(duration).delay(delay);
t.attr("transform", ([y]) => `translate(0,${yScale(y)})`);
}
});
}