function scaleViz(scale, v, domainBandRatio = .8) {
const width = 400;
const height = 180;
const domain = scale.domain();
const range = scale.range();
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const margin = 40;
const lineY1 = 50;
const lineY2 = 130;
const xDomain = d3.scaleLinear().domain(domain).range([margin, width - margin]);
const xRange = d3.scaleLinear().domain(range).range([margin, width - margin]);
let domainValue = (domain[0] + domain[1]) / 2;
let rangeValue = scale(domainValue);
const rangeX0 = xRange(range[0]);
const rangeX1 = xRange(range[1]);
const rangeWidth = rangeX1 - rangeX0;
const domainWidth = rangeWidth * domainBandRatio;
const domainCenter = (xDomain(domain[0]) + xDomain(domain[1])) / 2;
const domainX0 = domainCenter - domainWidth / 2;
const domainX1 = domainCenter + domainWidth / 2;
svg.append("path")
.attr("d", `
M${domainX0},${lineY1 + 10}
L${domainX1},${lineY1 + 10}
L${rangeX1},${lineY2}
L${rangeX0},${lineY2}
Z`)
.attr("fill", "peachpuff")
.attr("stroke", "orange")
.attr("stroke-width", 1);
svg.append("rect")
.attr("x", domainX0)
.attr("y", lineY1 - 20)
.attr("width", domainWidth)
.attr("height", 30)
.attr("fill", "lightblue")
.attr("stroke", "#2563eb");
svg.append("rect")
.attr("x", rangeX0)
.attr("y", lineY2 - 15)
.attr("width", rangeWidth)
.attr("height", 30)
.attr("fill", "lightblue")
.attr("stroke", "#2563eb");
svg.append("text").text("Domain")
.attr("x", domainCenter)
.attr("y", lineY1)
.attr("dominant-baseline", "middle")
.attr("text-anchor", "middle")
.style("fill", "#ffffff")
.style("font-weight", "bold");
svg.append("text").text("Range")
.attr("x", (rangeX0 + rangeX1) / 2)
.attr("y", lineY2)
.attr("dominant-baseline", "middle")
.attr("text-anchor", "middle")
.style("fill", "#ffffff")
.style("font-weight", "bold");
const domainLabel = svg.append("text")
.attr("y", lineY1 - 10)
.attr("text-anchor", "middle")
.style("font-size", "12px");
const rangeLabel = svg.append("text")
.attr("y", lineY2 + 15)
.attr("text-anchor", "middle")
.style("font-size", "12px");
const connector = svg.append("line")
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("marker-end", "url(#arrow)");
const arrowLabel = svg.append("text")
.attr("text-anchor", "middle")
.attr("font-size", "10px")
.style("fill", "black");
svg.append("defs").append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 0 10 10")
.attr("refX", 25) // 5
.attr("refY", 5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M 0 0 L 10 5 L 0 10 z")
.attr("fill", "black");
svg.append("text")
.text(domain[0])
.attr("x", xDomain(domain[0]))
.attr("y", lineY1 - 25)
.attr("text-anchor", "start");
svg.append("text")
.text(domain[1])
.attr("x", xDomain(domain[1]))
.attr("y", lineY1 - 25)
.attr("text-anchor", "end");
svg.append("text")
.text(range[0])
.attr("x", rangeX0)
.attr("y", lineY2 + 30)
.attr("text-anchor", "start");
svg.append("text")
.text(range[1])
.attr("x", rangeX1)
.attr("y", lineY2 + 30)
.attr("text-anchor", "end");
function update(v) {
const mapped = scale(v);
const t = (v - domain[0]) / (domain[1] - domain[0]);
const x1 = domainX0 + t * domainWidth;
const tMapped = (mapped - range[0]) / (range[1] - range[0]);
const x2 = rangeX0 + tMapped * rangeWidth;
connector
.attr("x1", x1)
.attr("y1", lineY1 + 10)
.attr("x2", x2)
.attr("y2", lineY2 - 5);
domainLabel.attr("x", x1).text(v);
rangeLabel.attr("x", x2).text(mapped.toFixed(0));
arrowLabel
.attr("x", (x1 + x2) / 2)
.attr("y", (lineY1 + lineY2) / 2)
.text(`${v} → ${mapped.toFixed(0)}`);
}
update(v);
return svg.node();
}