function RadialScatterplot(data, {
width = 500,
height = 500,
margin = 0,
marginLeft = margin,
marginTop = margin,
marginRight = margin,
marginBottom = margin,
strokeWidth = 1.5,
strokeOpacity = 1,
fontSize = 10,
theta = (d) => d.theta,
thetaDomain,
thetaRange = [0, 2 * Math.PI],
thetaType = d3.scaleLinear,
thetaLabel,
thetaTicks = 5,
thetaTickFormat = "~s",
r = (d) => d.r,
rDomain,
rRange = [
0,
Math.min(
width - marginLeft - marginRight,
height - marginTop - marginBottom
) / 2
],
rType = d3.scaleLinear,
rLabel,
rTicks = 5,
rTickFormat = "~s",
color = "black",
colorDomain,
colorRange = d3.interpolateBlues,
colorType = d3.scaleSequential,
colorLabel,
colorTicks = 5,
colorTickFormat = "~s",
radius = 2,
radiusDomain,
radiusRange = [1, 10],
radiusType = d3.scaleSqrt,
radiusLabel,
radiusTicks = 5,
radiusTickFormat = "~s",
} = {}) {
const isRadiusFunction = typeof radius === "function";
const isColorFunction = typeof color === "function";
if (thetaDomain === undefined) thetaDomain = d3.extent(data, theta);
if (rDomain === undefined) rDomain = d3.extent(data, r);
if (radiusDomain === undefined && isRadiusFunction) radiusDomain = d3.extent(data, radius);
if (colorDomain === undefined && isColorFunction) colorDomain = d3.extent(data, color);
const thetaScale = thetaType(thetaDomain, thetaRange);
const rScale = rType(rDomain, rRange).nice();
const radiusScale = isRadiusFunction ? radiusType(radiusDomain, radiusRange) : null;
const colorScale = isColorFunction ? colorType(colorDomain, colorRange) : null;
const ctx = DOM.context2d(width, height);
ctx.font = `${fontSize}px sans-serif`;
function drawDots(data) {
ctx.save();
ctx.translate(marginLeft + rRange[1], marginTop + rRange[1]);
ctx.globalAlpha = strokeOpacity;
ctx.lineWidth = strokeWidth;
data.forEach(d => {
ctx.beginPath();
const hyp = rScale(r(d))
const angle = thetaScale(theta(d)) - Math.PI / 2;
const cx = hyp * Math.cos(angle);
const cy = hyp * Math.sin(angle);
const dotRadius = isRadiusFunction ? radiusScale(radius(d)) : radius;
const dotColor = isColorFunction ? colorScale(color(d)) : color;
ctx.arc(cx, cy, dotRadius, 0, 2 * Math.PI);
ctx.strokeStyle = dotColor;
ctx.stroke();
});
ctx.restore();
}
function drawRAxis() {
ctx.save();
ctx.translate(marginLeft + rRange[1], marginTop + rRange[1]);
const rFormat = getFormat(rTickFormat);
const rTickValues = Array.isArray(rTicks) ? rTicks : rScale.ticks(rTicks);
ctx.strokeStyle = "black";
ctx.globalAlpha = 0.5;
ctx.lineWidth = 1;
ctx.beginPath();
rTickValues.slice(1).forEach((v) => {
ctx.moveTo(rScale(v), 0);
ctx.arc(0, 0, rScale(v), 0, 2 * Math.PI);
});
ctx.stroke();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.strokeStyle = 'white';
ctx.fillStyle = 'black';
ctx.globalAlpha = 1;
ctx.lineWidth = 5;
rTickValues.slice(1).forEach((v) => {
const cx = 0;
const cy = -rScale(v);
ctx.strokeText(rFormat(v), cx, cy);
ctx.fillText(rFormat(v), cx, cy);
});
ctx.strokeText(rLabel, 0, -rScale(rTickValues[rTickValues.length - 1]) - fontSize);
ctx.fillText(rLabel, 0, -rScale(rTickValues[rTickValues.length - 1]) - fontSize);
ctx.restore();
}
function drawThetaAxis() {
ctx.save();
ctx.translate(marginLeft + rRange[1], marginTop + rRange[1]);
const thetaFormat = getFormat(thetaTickFormat);
const thetaTickValues = Array.isArray(thetaTicks) ? thetaTicks : thetaScale.ticks(thetaTicks);
ctx.strokeStyle = "black";
ctx.globalAlpha = 1;
ctx.lineWidth = 1;
ctx.beginPath();
thetaTickValues.forEach((v) => {
ctx.save();
const radians = thetaScale(v) - Math.PI / 2;;
ctx.rotate(radians);
ctx.moveTo(rRange[1], 0);
ctx.lineTo(rRange[1] + 6, 0);
ctx.restore();
});
ctx.stroke();
ctx.textAlign = 'start';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.globalAlpha = 1;
thetaTickValues.forEach((v) => {
ctx.save();
ctx.rotate(thetaScale(v) - Math.PI / 2);
ctx.fillText(thetaFormat(v), rRange[1] + 9, 0);
ctx.restore();
});
ctx.restore();
}
function update(data) {
ctx.clearRect(0, 0, width, height);
drawDots(data);
drawRAxis();
drawThetaAxis();
}
update(data);
const div = d3.create("div");
if (isColorFunction) {
const colorFormat = getFormat(colorTickFormat);
const colorTickValues = Array.isArray(colorTicks) ? colorTicks : colorScale.ticks(colorTicks);
const legend = Legend(colorScale, {
tickFormat: colorFormat,
tickValues: colorTickValues,
title: colorLabel
});
d3.select(legend)
.selectAll("text")
.attr("font-size", fontSize);
div.append(() => legend);
}
if (isRadiusFunction) {
const radiusFormat = getFormat(radiusTickFormat);
const radiusTickValues = Array.isArray(radiusTicks) ? radiusTicks : radiusScale.ticks(radiusTicks);
const circleLegend = legendCircle()
.scale(radiusScale)
.tickValues(radiusTickValues)
.tickFormat(radiusFormat)
.tickSize(5);
const svg = d3.create("svg")
.attr("style", "display: block;")
.attr("height", radiusScale.range()[1] * 2 + 40)
.attr("width", radiusScale.range()[1] * 2 + 100);
svg.append("g")
.attr("transform", "translate(0, 20)")
.call(circleLegend);
div.append(() => svg.node());
};
div.append(() => ctx.canvas);
return Object.assign(div.node(), {
update,
scales: {
theta: thetaScale,
r: rScale,
radius: radiusScale,
color: colorScale
}
});
}