function parametric_plot2d(f, [a, b], [c, d], opts = {}) {
let {
Nu = 100,
nu = 10,
Nv = 100,
nv = 10,
width = 800,
height = "auto",
pad = 20,
fill = d3.schemeCategory10[9],
fillOpacity = 1,
xticks = 5,
xtickValues,
xtickFormat,
xtickSize = 6,
yticks = 5,
ytickValues,
ytickFormat,
ytickSize = 6
} = opts;
let Du = (b - a) / Nu;
let Dv = (d - c) / Nv;
let u_cut = Math.floor(Nu / nu);
let v_cut = Math.floor(Nv / nv);
let data = [];
let k = 0;
let quads = [];
let lines = [];
let boundary = [];
for (let i = 0; i <= Nu; i++) {
for (let j = 0; j <= Nv; j++) {
let u = a + i * Du;
let v = c + j * Dv;
data.push(f(u, v));
if (i < Nu && j < Nv) {
quads.push([k, k + 1, k + Nv + 2, k + Nv + 1]);
}
if (j % v_cut == 0 && i < Nu) {
lines.push([k, k + Nv + 1]);
}
if (i % u_cut == 0 && j < Nv) {
lines.push([k, k + 1]);
}
if ((i == 0 || i == Nu) && j < Nv) {
boundary.push([k, k + 1]);
}
if ((j == 0 || j == Nv) && i < Nu) {
boundary.push([k, k + Nv + 1]);
}
k++;
}
}
let [xmin, xmax] = d3.extent(data.map((pt) => pt[0]));
let [ymin, ymax] = d3.extent(data.map((pt) => pt[1]));
if (height == "auto") {
height = ((ymax - ymin) * width) / (xmax - xmin);
}
xmin = xmin - 0.05 * (xmax - xmin);
xmax = xmax + 0.05 * (xmax - xmin);
ymin = ymin - 0.05 * (ymax - ymin);
ymax = ymax + 0.05 * (ymax - ymin);
let x_scale = d3
.scaleLinear()
.domain([xmin, xmax])
.range([pad, width - pad]);
let y_scale = d3
.scaleLinear()
.domain([ymin, ymax])
.range([height - pad, pad]);
let pts_to_path = d3
.line()
.x((d) => x_scale(d[0]))
.y((d) => y_scale(d[1]));
let svg = d3.create("svg").attr("width", width).attr("height", height);
let fill_group = svg.append("g").attr("fill", fill).attr("stroke", fill);
fill_group
.selectAll("polygon.fill")
.data(
quads.map(([i, j, k, l]) => [
x_scale(data[i][0]),
y_scale(data[i][1]),
x_scale(data[j][0]),
y_scale(data[j][1]),
x_scale(data[k][0]),
y_scale(data[k][1]),
x_scale(data[l][0]),
y_scale(data[l][1])
])
)
.join("polygon")
.attr("class", "fill")
.attr("points", (d) => d.toString())
.attr("opacity", fillOpacity);
let mesh = svg
.append("g")
.attr("stroke-width", 1)
.attr("stroke", "black")
.attr("opacity", 0.5);
mesh
.selectAll("line.mesh")
.data(
lines.map(([i, j]) => ({
x1: x_scale(data[i][0]),
x2: x_scale(data[j][0]),
y1: y_scale(data[i][1]),
y2: y_scale(data[j][1])
}))
)
.join("line")
.attr("class", "mesh")
.attr("x1", (d) => d.x1)
.attr("x2", (d) => d.x2)
.attr("y1", (d) => d.y1)
.attr("y2", (d) => d.y2);
let bounds = svg
.append("g")
.attr("stroke", "black")
.attr("stroke-width", 5)
.attr("class", "boundary")
.selectAll("line.boundary")
.data(
boundary.map(([i, j]) => ({
x1: x_scale(data[i][0]),
x2: x_scale(data[j][0]),
y1: y_scale(data[i][1]),
y2: y_scale(data[j][1])
}))
)
.join("line")
.attr("class", "boundary")
.attr("x1", (d) => d.x1)
.attr("x2", (d) => d.x2)
.attr("y1", (d) => d.y1)
.attr("y2", (d) => d.y2);
let xAxisPosition;
if (ymin <= 0 && 0 <= ymax) {
xAxisPosition = y_scale(0);
} else {
xAxisPosition = height - pad;
}
let yAxisPosition;
if (xmin <= 0 && 0 <= xmax) {
yAxisPosition = x_scale(0);
} else {
yAxisPosition = pad;
}
svg
.append("g")
.style("font-size", "16px")
.attr("transform", `translate(0, ${xAxisPosition})`)
.call(
d3
.axisBottom(x_scale)
.ticks(xticks)
.tickValues(xtickValues)
.tickFormat(xtickFormat)
.tickSize(xtickSize)
.tickSizeOuter(0)
);
svg
.append("g")
.attr("id", "y-axis")
.style("font-size", "16px")
.attr("transform", `translate(${yAxisPosition})`)
.call(
d3
.axisLeft(y_scale)
.ticks(yticks)
.tickValues(ytickValues)
.tickFormat(ytickFormat)
.tickSize(ytickSize)
.tickSizeOuter(0)
);
return svg.node();
}