--- title: "Data Science Methods for Forecasting in Energy and Economics" date: 2025-07-10 format: revealjs: embed-resources: true footer: "" execute: daemon: false highlight-style: github --- ## B-spline Basis Functions ```{ojs} d3 = require("d3@7") ``` ```{ojs} bsplineData = FileAttachment("basis_functions.csv").csv({ typed: true }) muValues = Array.from(new Set(bsplineData.map(d => d.mu))).sort((a, b) => a - b) minMu = Math.min(...muValues) maxMu = Math.max(...muValues) ``` ```{ojs} chart = { // State variables for selected parameters let selectedMu = 0.5; let selectedSig = 1; let selectedNonc = 0; let selectedTailw = 1; const filteredData = () => bsplineData.filter(d => Math.abs(selectedMu - d.mu) < 0.001 && d.sig === selectedSig && d.nonc === selectedNonc && d.tailw === selectedTailw ); const container = d3.create("div").style("margin-bottom", "20px"); const controlsContainer = container.append("div") .style("display", "flex") .style("gap", "20px"); // μ slider control const muControl = controlsContainer.append("div").style("display","flex").style("align-items","center").style("gap","10px"); muControl.append("label").text("μ:").style("font-size","16px"); muControl.append("input") .attr("type","range").attr("min",minMu).attr("max",maxMu).attr("step",0.2) .property("value",selectedMu) .on("input", function() { selectedMu = +this.value; muControl.select("span").text(selectedMu); updateChart(filteredData()); }); muControl.append("span").text(selectedMu).style("font-size","16px"); // sigma control const sigControl = controlsContainer.append("div").style("display","flex").style("align-items","center").style("gap","10px"); sigControl.append("label").text("Sigma:").style("font-size","16px"); sigControl.append("input") .attr("type","range").attr("min", -2).attr("max", 2).attr("step", 1) .property("value", Math.log2(selectedSig)) .on("input", function() { selectedSig = 2 ** (+this.value); sigControl.select("span").text(selectedSig); updateChart(filteredData()); }); sigControl.append("span").text(selectedSig).style("font-size","16px"); // nonc control const noncControl = controlsContainer.append("div").style("display","flex").style("align-items","center").style("gap","10px"); noncControl.append("label").text("Non-centrality:").style("font-size","16px"); noncControl.append("input") .attr("type","range").attr("min", -4).attr("max", 4).attr("step", 2) .property("value", selectedNonc) .on("input", function() { selectedNonc = +this.value; noncControl.select("span").text(selectedNonc); updateChart(filteredData()); }); noncControl.append("span").text(selectedNonc).style("font-size","16px"); // tail weight control const tailwControl = controlsContainer.append("div").style("display","flex").style("align-items","center").style("gap","10px"); tailwControl.append("label").text("Tail weight:").style("font-size","16px"); tailwControl.append("input") .attr("type","range").attr("min", -2).attr("max", 2).attr("step", 1) .property("value", Math.log2(selectedTailw)) .on("input", function() { selectedTailw = 2 ** (+this.value); tailwControl.select("span").text(selectedTailw); updateChart(filteredData()); }); tailwControl.append("span").text(selectedTailw).style("font-size","16px"); // 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)) .style("font-size", "16px"); const yAxis = g.append("g") .attr("class", "y-axis") .call(d3.axisLeft(y).ticks(5)) .style("font-size", "16px"); // 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(1500) .call(d3.axisBottom(x).ticks(10)); y.domain([0, d3.max(data, d => d.y)]); g.select(".y-axis") .transition().duration(1500) .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(1000).style("opacity", 0).remove() ) .transition().duration(1000) .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(); } ```