From aff2d4f4ccbe281134bebd2a0b0984c45fc6ce1c Mon Sep 17 00:00:00 2001 From: Jonathan Berrisch Date: Sun, 18 May 2025 01:48:42 +0200 Subject: [PATCH] Add nice transitions --- app.qmd | 313 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 168 insertions(+), 145 deletions(-) diff --git a/app.qmd b/app.qmd index 9fe3004..d7def95 100644 --- a/app.qmd +++ b/app.qmd @@ -26,154 +26,177 @@ maxKnots = Math.max(...knotValues) muValues = Array.from(new Set(bsplineData.map(d => d.mu))).sort((a, b) => a - b) minMu = Math.min(...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} -// D3-based visualization for B-spline basis functions chart = { - // Create chart dimensions - const width = 800; - const height = 400; - const margin = {top: 40, right: 20, bottom: 40, left: 40}; - const innerWidth = width - margin.left - margin.right; - const innerHeight = height - margin.top - margin.bottom; - - // Create scales - const x = d3.scaleLinear() - .domain([0, 1]) - .range([0, innerWidth]); - - const y = d3.scaleLinear() - .domain([0, 0.7]) - .range([innerHeight, 0]); - - // Create a color scale for the basis functions - const color = d3.scaleOrdinal(d3.schemeCategory10); - - // Create SVG - const svg = d3.create("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [0, 0, width, height]) - .attr("style", "max-width: 100%; height: auto;"); - - // Add chart title - svg.append("text") - .attr("class", "chart-title") - .attr("x", width / 2) - .attr("y", 20) - .attr("text-anchor", "middle") - .attr("font-size", "16px") - .attr("font-weight", "bold"); - - // Create the chart group - const g = svg.append("g") - .attr("transform", `translate(${margin.left},${margin.top})`); - - // Add axes - const xAxis = g.append("g") - .attr("transform", `translate(0,${innerHeight})`) - .call(d3.axisBottom(x).ticks(10)); - - const yAxis = g.append("g") - .call(d3.axisLeft(y).ticks(5)); - - // Add axis labels - g.append("text") - .attr("x", innerWidth / 2) - .attr("y", innerHeight + 35) - .attr("text-anchor", "middle") - .text("x"); - - g.append("text") - .attr("transform", "rotate(-90)") - .attr("x", -innerHeight / 2) - .attr("y", -30) - .attr("text-anchor", "middle") - .text("y"); - - // Add a horizontal line at y = 0 - g.append("line") - .attr("x1", 0) - .attr("x2", innerWidth) - .attr("y1", y(0)) - .attr("y2", y(0)) - .attr("stroke", "#000") - .attr("stroke-opacity", 0.2); - - // Add gridlines - g.append("g") - .attr("class", "grid-lines") - .selectAll("line") - .data(y.ticks(5)) - .join("line") - .attr("x1", 0) - .attr("x2", innerWidth) - .attr("y1", d => y(d)) - .attr("y2", d => y(d)) - .attr("stroke", "#ccc") - .attr("stroke-opacity", 0.5); - - // Create a line generator - const line = d3.line() - .x(d => x(d.x)) - .y(d => y(d.y)); - - // Group to contain the basis function lines - const linesGroup = g.append("g") - .attr("class", "basis-functions"); - - // Function to update the chart with new data - function updateChart(data) { - // Group data by basis function - const groupedData = d3.group(data, d => d.b); - - // Update the chart title - svg.select(".chart-title") - .text(`B-spline Basis Functions (${selectedKnots} knots, μ = ${selectedMu})`); - - // Update or create paths - const paths = linesGroup.selectAll("path") - .data(Array.from(groupedData.values())); - - // Remove paths that are no longer needed - paths.exit().remove(); - - // Add new paths - paths.enter() - .append("path") - .attr("fill", "none") - .attr("stroke-width", 3) - .attr("stroke", (_, i) => color(i)) - .merge(paths) // Merge with existing paths for transition - .transition() - .duration(750) - .attr("d", line); - } - - // Store the update function - svg.node().update = updateChart; - - // Initial render - updateChart(filteredBspline); - - return svg.node(); + // State variables for selected parameters + let selectedKnots = minKnots; + let selectedMu = 0.5; + const filteredData = () => bsplineData.filter(d => selectedKnots == d.knots && Math.abs(selectedMu - d.mu) < 0.001); + const container = d3.create("div").style("margin-bottom", "20px"); + // Knots slider control + const knotsControl = container.append("div").style("display","flex").style("align-items","center").style("gap","10px"); + knotsControl.append("label").text("Knots:"); + knotsControl.append("input") + .attr("type","range").attr("min",minKnots).attr("max",maxKnots).attr("step",1) + .property("value",selectedKnots) + .on("input", function() { selectedKnots = +this.value; knotsControl.select("span").text(selectedKnots); updateChart(filteredData()); }); + knotsControl.append("span").text(selectedKnots); + // μ slider control + const muControl = container.append("div").style("display","flex").style("align-items","center").style("gap","10px"); + muControl.append("label").text("μ:"); + muControl.append("input") + .attr("type","range").attr("min",minMu).attr("max",maxMu).attr("step",0.1) + .property("value",selectedMu) + .on("input", function() { selectedMu = +this.value; muControl.select("span").text(selectedMu); updateChart(filteredData()); }); + muControl.append("span").text(selectedMu); + // Build SVG + const width = 800; + const height = 400; + const margin = {top: 40, right: 20, bottom: 40, left: 40}; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + // Create scales + const x = d3.scaleLinear() + .domain([0, 1]) + .range([0, innerWidth]); + + const y = d3.scaleLinear() + .domain([0, 0.7]) + .range([innerHeight, 0]); + + // Create a color scale for the basis functions + const color = d3.scaleOrdinal(d3.schemeCategory10); + + // Create SVG + const svg = d3.create("svg") + .attr("width", width) + .attr("height", height) + .attr("viewBox", [0, 0, width, height]) + .attr("style", "max-width: 100%; height: auto;"); + + // Add chart title + svg.append("text") + .attr("class", "chart-title") + .attr("x", width / 2) + .attr("y", 20) + .attr("text-anchor", "middle") + .attr("font-size", "16px") + .attr("font-weight", "bold"); + + // Create the chart group + const g = svg.append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // Add axes + const xAxis = g.append("g") + .attr("transform", `translate(0,${innerHeight})`) + .attr("class", "x-axis") + .call(d3.axisBottom(x).ticks(10)); + + const yAxis = g.append("g") + .attr("class", "y-axis") + .call(d3.axisLeft(y).ticks(5)); + + // Add axis labels + g.append("text") + .attr("x", innerWidth / 2) + .attr("y", innerHeight + 35) + .attr("text-anchor", "middle") + .text("x"); + + g.append("text") + .attr("transform", "rotate(-90)") + .attr("x", -innerHeight / 2) + .attr("y", -30) + .attr("text-anchor", "middle") + .text("y"); + + // Add a horizontal line at y = 0 + g.append("line") + .attr("x1", 0) + .attr("x2", innerWidth) + .attr("y1", y(0)) + .attr("y2", y(0)) + .attr("stroke", "#000") + .attr("stroke-opacity", 0.2); + + // Add gridlines + g.append("g") + .attr("class", "grid-lines") + .selectAll("line") + .data(y.ticks(5)) + .join("line") + .attr("x1", 0) + .attr("x2", innerWidth) + .attr("y1", d => y(d)) + .attr("y2", d => y(d)) + .attr("stroke", "#ccc") + .attr("stroke-opacity", 0.5); + + // Create a line generator + const line = d3.line() + .x(d => x(d.x)) + .y(d => y(d.y)) + .curve(d3.curveBasis); + + // Group to contain the basis function lines + const linesGroup = g.append("g") + .attr("class", "basis-functions"); + + // Store the current basis functions for transition + let currentBasisFunctions = new Map(); + + // Function to update the chart with new data + function updateChart(data) { + // Update axes with transitions + x.domain([0, d3.max(data, d => d.x)]); + g.select(".x-axis") + .transition().duration(750) + .call(d3.axisBottom(x).ticks(10)); + 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(); } ```