Add nice transitions

This commit is contained in:
2025-05-18 01:48:42 +02:00
parent 63691f53b1
commit aff2d4f4cc

313
app.qmd
View File

@@ -26,154 +26,177 @@ maxKnots = Math.max(...knotValues)
muValues = Array.from(new Set(bsplineData.map(d => d.mu))).sort((a, b) => a - b) muValues = Array.from(new Set(bsplineData.map(d => d.mu))).sort((a, b) => a - b)
minMu = Math.min(...muValues) minMu = Math.min(...muValues)
maxMu = Math.max(...muValues) maxMu = Math.max(...muValues)
// Create a more compact layout for controls
viewof controls = Inputs.form({
knots: Inputs.range([minKnots, maxKnots], {value: minKnots, step: 1, label: "Knots:", width: 200}),
mu: Inputs.range([minMu, maxMu], {value: 0.5, step: 0.1, label: "μ:", width: 200})
}, {
submit: false,
layout: 'horizontal',
style: 'display: flex; gap: 20px; align-items: center; margin-bottom: 10px; font-size: 0.9em;'
})
selectedKnots = controls.knots
selectedMu = controls.mu
filteredBspline = bsplineData.filter(function(row) {
return selectedKnots == row.knots && Math.abs(selectedMu - row.mu) < 0.001;
})
``` ```
```{ojs} ```{ojs}
// D3-based visualization for B-spline basis functions
chart = { chart = {
// Create chart dimensions // State variables for selected parameters
const width = 800; let selectedKnots = minKnots;
const height = 400; let selectedMu = 0.5;
const margin = {top: 40, right: 20, bottom: 40, left: 40}; const filteredData = () => bsplineData.filter(d => selectedKnots == d.knots && Math.abs(selectedMu - d.mu) < 0.001);
const innerWidth = width - margin.left - margin.right; const container = d3.create("div").style("margin-bottom", "20px");
const innerHeight = height - margin.top - margin.bottom; // Knots slider control
const knotsControl = container.append("div").style("display","flex").style("align-items","center").style("gap","10px");
// Create scales knotsControl.append("label").text("Knots:");
const x = d3.scaleLinear() knotsControl.append("input")
.domain([0, 1]) .attr("type","range").attr("min",minKnots).attr("max",maxKnots).attr("step",1)
.range([0, innerWidth]); .property("value",selectedKnots)
.on("input", function() { selectedKnots = +this.value; knotsControl.select("span").text(selectedKnots); updateChart(filteredData()); });
const y = d3.scaleLinear() knotsControl.append("span").text(selectedKnots);
.domain([0, 0.7]) // μ slider control
.range([innerHeight, 0]); const muControl = container.append("div").style("display","flex").style("align-items","center").style("gap","10px");
muControl.append("label").text("μ:");
// Create a color scale for the basis functions muControl.append("input")
const color = d3.scaleOrdinal(d3.schemeCategory10); .attr("type","range").attr("min",minMu).attr("max",maxMu).attr("step",0.1)
.property("value",selectedMu)
// Create SVG .on("input", function() { selectedMu = +this.value; muControl.select("span").text(selectedMu); updateChart(filteredData()); });
const svg = d3.create("svg") muControl.append("span").text(selectedMu);
.attr("width", width) // Build SVG
.attr("height", height) const width = 800;
.attr("viewBox", [0, 0, width, height]) const height = 400;
.attr("style", "max-width: 100%; height: auto;"); const margin = {top: 40, right: 20, bottom: 40, left: 40};
const innerWidth = width - margin.left - margin.right;
// Add chart title const innerHeight = height - margin.top - margin.bottom;
svg.append("text")
.attr("class", "chart-title") // Create scales
.attr("x", width / 2) const x = d3.scaleLinear()
.attr("y", 20) .domain([0, 1])
.attr("text-anchor", "middle") .range([0, innerWidth]);
.attr("font-size", "16px")
.attr("font-weight", "bold"); const y = d3.scaleLinear()
.domain([0, 0.7])
// Create the chart group .range([innerHeight, 0]);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`); // Create a color scale for the basis functions
const color = d3.scaleOrdinal(d3.schemeCategory10);
// Add axes
const xAxis = g.append("g") // Create SVG
.attr("transform", `translate(0,${innerHeight})`) const svg = d3.create("svg")
.call(d3.axisBottom(x).ticks(10)); .attr("width", width)
.attr("height", height)
const yAxis = g.append("g") .attr("viewBox", [0, 0, width, height])
.call(d3.axisLeft(y).ticks(5)); .attr("style", "max-width: 100%; height: auto;");
// Add axis labels // Add chart title
g.append("text") svg.append("text")
.attr("x", innerWidth / 2) .attr("class", "chart-title")
.attr("y", innerHeight + 35) .attr("x", width / 2)
.attr("text-anchor", "middle") .attr("y", 20)
.text("x"); .attr("text-anchor", "middle")
.attr("font-size", "16px")
g.append("text") .attr("font-weight", "bold");
.attr("transform", "rotate(-90)")
.attr("x", -innerHeight / 2) // Create the chart group
.attr("y", -30) const g = svg.append("g")
.attr("text-anchor", "middle") .attr("transform", `translate(${margin.left},${margin.top})`);
.text("y");
// Add axes
// Add a horizontal line at y = 0 const xAxis = g.append("g")
g.append("line") .attr("transform", `translate(0,${innerHeight})`)
.attr("x1", 0) .attr("class", "x-axis")
.attr("x2", innerWidth) .call(d3.axisBottom(x).ticks(10));
.attr("y1", y(0))
.attr("y2", y(0)) const yAxis = g.append("g")
.attr("stroke", "#000") .attr("class", "y-axis")
.attr("stroke-opacity", 0.2); .call(d3.axisLeft(y).ticks(5));
// Add gridlines // Add axis labels
g.append("g") g.append("text")
.attr("class", "grid-lines") .attr("x", innerWidth / 2)
.selectAll("line") .attr("y", innerHeight + 35)
.data(y.ticks(5)) .attr("text-anchor", "middle")
.join("line") .text("x");
.attr("x1", 0)
.attr("x2", innerWidth) g.append("text")
.attr("y1", d => y(d)) .attr("transform", "rotate(-90)")
.attr("y2", d => y(d)) .attr("x", -innerHeight / 2)
.attr("stroke", "#ccc") .attr("y", -30)
.attr("stroke-opacity", 0.5); .attr("text-anchor", "middle")
.text("y");
// Create a line generator
const line = d3.line() // Add a horizontal line at y = 0
.x(d => x(d.x)) g.append("line")
.y(d => y(d.y)); .attr("x1", 0)
.attr("x2", innerWidth)
// Group to contain the basis function lines .attr("y1", y(0))
const linesGroup = g.append("g") .attr("y2", y(0))
.attr("class", "basis-functions"); .attr("stroke", "#000")
.attr("stroke-opacity", 0.2);
// Function to update the chart with new data
function updateChart(data) { // Add gridlines
// Group data by basis function g.append("g")
const groupedData = d3.group(data, d => d.b); .attr("class", "grid-lines")
.selectAll("line")
// Update the chart title .data(y.ticks(5))
svg.select(".chart-title") .join("line")
.text(`B-spline Basis Functions (${selectedKnots} knots, μ = ${selectedMu})`); .attr("x1", 0)
.attr("x2", innerWidth)
// Update or create paths .attr("y1", d => y(d))
const paths = linesGroup.selectAll("path") .attr("y2", d => y(d))
.data(Array.from(groupedData.values())); .attr("stroke", "#ccc")
.attr("stroke-opacity", 0.5);
// Remove paths that are no longer needed
paths.exit().remove(); // Create a line generator
const line = d3.line()
// Add new paths .x(d => x(d.x))
paths.enter() .y(d => y(d.y))
.append("path") .curve(d3.curveBasis);
.attr("fill", "none")
.attr("stroke-width", 3) // Group to contain the basis function lines
.attr("stroke", (_, i) => color(i)) const linesGroup = g.append("g")
.merge(paths) // Merge with existing paths for transition .attr("class", "basis-functions");
.transition()
.duration(750) // Store the current basis functions for transition
.attr("d", line); let currentBasisFunctions = new Map();
}
// Function to update the chart with new data
// Store the update function function updateChart(data) {
svg.node().update = updateChart; // Update axes with transitions
x.domain([0, d3.max(data, d => d.x)]);
// Initial render g.select(".x-axis")
updateChart(filteredBspline); .transition().duration(750)
.call(d3.axisBottom(x).ticks(10));
return svg.node(); y.domain([0, d3.max(data, d => d.y)]);
g.select(".y-axis")
.transition().duration(750)
.call(d3.axisLeft(y).ticks(5));
// Group data by basis function
const dataByFunction = Array.from(d3.group(data, d => d.b));
// Update the chart title
svg.select(".chart-title")
.text(`B-spline Basis Functions (${selectedKnots} knots, μ = ${selectedMu})`);
// Create a key function to track basis functions
const keyFn = d => d[0];
// Update basis function lines with proper enter/update/exit pattern
const u = linesGroup.selectAll("path")
.data(dataByFunction, keyFn);
u.join(
enter => enter.append("path")
.attr("fill", "none")
.attr("stroke-width", 3)
.attr("stroke", (_, i) => color(i))
.attr("d", d => line(d[1].map(pt => ({x: pt.x, y: 0}))))
.style("opacity", 0),
update => update,
exit => exit.transition().duration(750).style("opacity", 0).remove()
)
.transition().duration(750)
.attr("d", d => line(d[1]))
.attr("stroke", (_, i) => color(i))
.style("opacity", 1);
}
// Store the update function
svg.node().update = updateChart;
// Initial render
updateChart(filteredData());
container.node().appendChild(svg.node());
return container.node();
} }
``` ```