Spaces:
Running
Running
| <div class="d3-equation-editor"></div> | |
| <style> | |
| .d3-equation-editor { | |
| position: relative; | |
| font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', Arial, sans-serif; | |
| } | |
| .d3-equation-editor .chart-card { | |
| background: var(--surface-bg); | |
| border: 1px solid var(--border-color); | |
| border-radius: 10px; | |
| padding: 8px; | |
| } | |
| .d3-equation-editor .chart-header { | |
| display: flex; | |
| align-items: flex-start; | |
| justify-content: flex-start; | |
| gap: 24px; | |
| margin: 16px 0 0 0; | |
| flex-wrap: wrap; | |
| } | |
| .d3-equation-editor .controls { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 24px; | |
| align-items: flex-start; | |
| justify-content: flex-start; | |
| width: 100%; | |
| } | |
| .d3-equation-editor .controls .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 8px; | |
| } | |
| .d3-equation-editor .controls .control-group.equation-group { | |
| width: 100%; | |
| } | |
| .d3-equation-editor .controls .input-row { | |
| display: flex; | |
| gap: 24px; | |
| align-items: flex-start; | |
| justify-content: flex-start; | |
| flex-wrap: wrap; | |
| width: 100%; | |
| } | |
| .d3-equation-editor .controls .input-row .control-group.equation-group { | |
| flex: 1; | |
| min-width: 300px; | |
| } | |
| .d3-equation-editor .controls .control-group.domain-group { | |
| flex: 0 0 240px; | |
| } | |
| .d3-equation-editor .controls label { | |
| font-size: 13px; | |
| color: var(--text-color); | |
| font-weight: 600; | |
| letter-spacing: -0.01em; | |
| } | |
| .d3-equation-editor .controls input[type="text"] { | |
| font-size: 17px; | |
| font-weight: 400; | |
| padding: 14px 18px; | |
| border: 1.5px solid var(--primary-color); | |
| border-radius: var(--button-radius); | |
| background-color: var(--surface-bg); | |
| color: var(--text-color); | |
| cursor: text; | |
| transition: all .2s cubic-bezier(0.25, 0.46, 0.45, 0.94); | |
| width: 100%; | |
| font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace; | |
| line-height: 1.2; | |
| } | |
| [data-theme="dark"] .d3-equation-editor .controls input[type="text"] { | |
| border: 1.5px solid var(--primary-color); | |
| } | |
| .d3-equation-editor .controls input[type="text"]:hover { | |
| border-color: rgba(0, 123, 255, 0.3); | |
| } | |
| .d3-equation-editor .controls input[type="text"]:focus { | |
| border-color: rgba(0, 123, 255, 0.6); | |
| box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); | |
| outline: none; | |
| } | |
| [data-theme="dark"] .d3-equation-editor .controls input[type="text"]:hover { | |
| border-color: rgba(10, 132, 255, 0.4); | |
| } | |
| [data-theme="dark"] .d3-equation-editor .controls input[type="text"]:focus { | |
| border-color: rgba(10, 132, 255, 0.7); | |
| box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.15); | |
| } | |
| .d3-equation-editor .controls input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: var(--border-color); | |
| outline: none; | |
| cursor: pointer; | |
| width: 100%; | |
| transition: background 0.2s ease; | |
| } | |
| .d3-equation-editor .controls input[type="range"]:hover { | |
| background: var(--muted-color); | |
| } | |
| .d3-equation-editor .controls input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: var(--primary-color); | |
| cursor: pointer; | |
| border: 2px solid var(--page-bg); | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| transition: all 0.2s ease; | |
| } | |
| .d3-equation-editor .controls input[type="range"]::-webkit-slider-thumb:hover { | |
| background: var(--primary-color-hover); | |
| transform: scale(1.1); | |
| } | |
| .d3-equation-editor .controls input[type="range"]::-moz-range-thumb { | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: var(--primary-color); | |
| cursor: pointer; | |
| border: 2px solid var(--page-bg); | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
| transition: all 0.2s ease; | |
| } | |
| .d3-equation-editor .controls input[type="range"]::-moz-range-thumb:hover { | |
| background: var(--primary-color-hover); | |
| transform: scale(1.1); | |
| } | |
| .d3-equation-editor .legend-bottom { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 6px; | |
| font-size: 12px; | |
| color: var(--text-color); | |
| } | |
| .d3-equation-editor .legend-bottom .legend-title { | |
| font-size: 12px; | |
| font-weight: 700; | |
| color: var(--text-color); | |
| } | |
| .d3-equation-editor .legend-bottom .items { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px 14px; | |
| } | |
| .d3-equation-editor .legend-bottom .item { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| white-space: nowrap; | |
| } | |
| .d3-equation-editor .legend-bottom .swatch { | |
| width: 14px; | |
| height: 14px; | |
| border-radius: 3px; | |
| border: 1px solid var(--border-color); | |
| display: inline-block; | |
| } | |
| .d3-equation-editor .axis-label { | |
| fill: var(--text-color); | |
| font-size: 12px; | |
| font-weight: 700; | |
| } | |
| .d3-equation-editor .axes path, | |
| .d3-equation-editor .axes line { | |
| stroke: var(--axis-color); | |
| } | |
| .d3-equation-editor .axes text { | |
| fill: var(--tick-color); | |
| } | |
| .d3-equation-editor .grid line { | |
| stroke: var(--grid-color); | |
| stroke-width: 1; | |
| shape-rendering: crispEdges; | |
| } | |
| .d3-equation-editor .function-curve { | |
| fill: none; | |
| stroke-width: 2.5; | |
| stroke-linejoin: round; | |
| stroke-linecap: round; | |
| } | |
| .d3-equation-editor .d3-tooltip { | |
| z-index: var(--z-tooltip); | |
| backdrop-filter: saturate(1.12) blur(8px); | |
| } | |
| .d3-equation-editor .error-message { | |
| color: var(--danger, #b00020); | |
| font-size: 11px; | |
| margin-top: 4px; | |
| font-style: italic; | |
| } | |
| .d3-equation-editor .examples { | |
| width: 100%; | |
| } | |
| .d3-equation-editor .examples .button { | |
| margin: 0 10px 10px 0; | |
| } | |
| </style> | |
| <script> | |
| (() => { | |
| const ensureD3 = (cb) => { | |
| if (window.d3 && typeof window.d3.select === 'function') return cb(); | |
| let s = document.getElementById('d3-cdn-script'); | |
| if (!s) { | |
| s = document.createElement('script'); | |
| s.id = 'd3-cdn-script'; | |
| s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; | |
| document.head.appendChild(s); | |
| } | |
| const onReady = () => { | |
| if (window.d3 && typeof window.d3.select === 'function') cb(); | |
| }; | |
| s.addEventListener('load', onReady, { once: true }); | |
| if (window.d3) onReady(); | |
| }; | |
| const bootstrap = () => { | |
| const scriptEl = document.currentScript; | |
| let container = scriptEl ? scriptEl.previousElementSibling : null; | |
| if (!(container && container.classList && container.classList.contains('d3-equation-editor'))) { | |
| const candidates = Array.from(document.querySelectorAll('.d3-equation-editor')) | |
| .filter(el => !(el.dataset && el.dataset.mounted === 'true')); | |
| container = candidates[candidates.length - 1] || null; | |
| } | |
| if (!container) return; | |
| if (container.dataset) { | |
| if (container.dataset.mounted === 'true') return; | |
| container.dataset.mounted = 'true'; | |
| } | |
| // Controls | |
| const controls = document.createElement('div'); | |
| controls.className = 'controls'; | |
| // Input row (equation + domain) | |
| const inputRow = document.createElement('div'); | |
| inputRow.className = 'input-row'; | |
| // Equation input | |
| const groupEquation = document.createElement('div'); | |
| groupEquation.className = 'control-group equation-group'; | |
| const labelEquation = document.createElement('label'); | |
| labelEquation.textContent = 'Equation f(x) ='; | |
| const inputEquation = document.createElement('input'); | |
| inputEquation.type = 'text'; | |
| inputEquation.value = 'sin(x) * exp(-x^2/8) + 0.3*sin(3*x)'; | |
| inputEquation.placeholder = 'e.g., sin(x)*exp(-x^2/8), x^3 - 3*x, sin(x) + cos(2*x)'; | |
| groupEquation.appendChild(labelEquation); | |
| groupEquation.appendChild(inputEquation); | |
| // Domain range | |
| const groupRange = document.createElement('div'); | |
| groupRange.className = 'control-group domain-group'; | |
| const labelRange = document.createElement('label'); | |
| labelRange.textContent = 'Domain'; | |
| const inputRange = document.createElement('input'); | |
| inputRange.type = 'range'; | |
| inputRange.min = '1'; | |
| inputRange.max = '10'; | |
| inputRange.step = '0.5'; | |
| inputRange.value = '4'; | |
| const rangeValue = document.createElement('span'); | |
| rangeValue.style.fontSize = '13px'; | |
| rangeValue.style.color = 'var(--muted-color)'; | |
| rangeValue.style.fontWeight = '500'; | |
| rangeValue.style.marginTop = '4px'; | |
| rangeValue.textContent = '[-4π, 4π]'; | |
| groupRange.appendChild(labelRange); | |
| groupRange.appendChild(inputRange); | |
| groupRange.appendChild(rangeValue); | |
| inputRow.appendChild(groupEquation); | |
| inputRow.appendChild(groupRange); | |
| // Examples (more focused and pertinent) | |
| const examples = document.createElement('div'); | |
| examples.className = 'examples'; | |
| const exampleFunctions = [ | |
| 'sin(x) * exp(-x^2/8)', | |
| 'sin(x) + 0.5*cos(2*x)', | |
| 'x^3 - 3*x', | |
| 'sin(x) * exp(-x^2/8) + 0.3*sin(3*x)', | |
| 'exp(-x^2/2) * cos(4*x)', | |
| 'sin(x) + sin(3*x)/3 + sin(5*x)/5' | |
| ]; | |
| exampleFunctions.forEach(func => { | |
| const btn = document.createElement('button'); | |
| btn.className = 'button button--ghost'; | |
| btn.textContent = func; | |
| btn.addEventListener('click', () => { | |
| inputEquation.value = func; | |
| updatePlot(); | |
| }); | |
| examples.appendChild(btn); | |
| }); | |
| controls.appendChild(inputRow); | |
| controls.appendChild(examples); | |
| // Error message | |
| const errorMsg = document.createElement('div'); | |
| errorMsg.className = 'error-message'; | |
| errorMsg.style.display = 'none'; | |
| // Header (controls only) to be placed after chart | |
| const header = document.createElement('div'); | |
| header.className = 'chart-header'; | |
| header.appendChild(controls); | |
| // SVG scaffolding inside a card wrapper | |
| const card = document.createElement('div'); | |
| card.className = 'chart-card'; | |
| container.appendChild(card); | |
| container.appendChild(header); | |
| container.appendChild(errorMsg); | |
| const svg = d3.select(card).append('svg').attr('width', '100%').style('display', 'block'); | |
| const gRoot = svg.append('g'); | |
| const gGrid = gRoot.append('g').attr('class', 'grid'); | |
| const gAxes = gRoot.append('g').attr('class', 'axes'); | |
| const gCurve = gRoot.append('g').attr('class', 'curve'); | |
| // Tooltip | |
| container.style.position = container.style.position || 'relative'; | |
| let tip = container.querySelector('.d3-tooltip'); | |
| let tipInner; | |
| if (!tip) { | |
| tip = document.createElement('div'); | |
| tip.className = 'd3-tooltip'; | |
| Object.assign(tip.style, { | |
| position: 'absolute', | |
| top: '0px', | |
| left: '0px', | |
| transform: 'translate(-9999px, -9999px)', | |
| pointerEvents: 'none', | |
| padding: '8px 10px', | |
| borderRadius: 'var(--button-radius)', | |
| fontSize: '12px', | |
| lineHeight: '1.35', | |
| border: '1px solid var(--border-color)', | |
| background: 'var(--surface-bg)', | |
| color: 'var(--text-color)', | |
| boxShadow: '0 4px 24px rgba(0,0,0,.18)', | |
| opacity: '0', | |
| transition: 'opacity .12s ease' | |
| }); | |
| tipInner = document.createElement('div'); | |
| tipInner.className = 'd3-tooltip__inner'; | |
| tipInner.style.textAlign = 'left'; | |
| tip.appendChild(tipInner); | |
| container.appendChild(tip); | |
| } else { | |
| tipInner = tip.querySelector('.d3-tooltip__inner') || tip; | |
| } | |
| // State | |
| let width = 800, height = 480; | |
| const margin = { top: 16, right: 32, bottom: 44, left: 56 }; | |
| const xScale = d3.scaleLinear(); | |
| const yScale = d3.scaleLinear(); | |
| const line = d3.line() | |
| .x(d => xScale(d.x)) | |
| .y(d => yScale(d.y)) | |
| .curve(d3.curveCardinal); | |
| // Math parser - improved to handle complex expressions and exponents correctly | |
| function safeEval(expr, x) { | |
| try { | |
| // First, replace x with the actual value in parentheses for safety | |
| let cleanExpr = expr.replace(/\bx\b/g, `(${x})`); | |
| // Replace math functions and constants | |
| cleanExpr = cleanExpr | |
| .replace(/\bsin\b/g, 'Math.sin') | |
| .replace(/\bcos\b/g, 'Math.cos') | |
| .replace(/\btan\b/g, 'Math.tan') | |
| .replace(/\bexp\b/g, 'Math.exp') | |
| .replace(/\blog\b/g, 'Math.log') | |
| .replace(/\babs\b/g, 'Math.abs') | |
| .replace(/\bsqrt\b/g, 'Math.sqrt') | |
| .replace(/\bpi\b/g, 'Math.PI') | |
| .replace(/\be\b/g, 'Math.E'); | |
| // Handle exponents more carefully - need to preserve operator precedence | |
| // Convert x^n to Math.pow(x, n) for proper precedence | |
| cleanExpr = cleanExpr.replace(/([^*+\-\/\s]+)\^([^*+\-\/\s]+)/g, 'Math.pow($1, $2)'); | |
| // Handle remaining ^ operators (fallback to **) | |
| cleanExpr = cleanExpr.replace(/\^/g, '**'); | |
| // Handle implicit multiplication (e.g., 2x -> 2*x, sin(x)cos(x) -> sin(x)*cos(x)) | |
| cleanExpr = cleanExpr | |
| .replace(/(\d)(\()/g, '$1*$2') // 2( -> 2*( | |
| .replace(/(\))(\()/g, '$1*$2') // )( -> )*( | |
| .replace(/(\))(\d)/g, '$1*$2') // )2 -> )*2 | |
| .replace(/(\d)([a-zA-Z])/g, '$1*$2'); // 2x -> 2*x | |
| // Security check: only allow safe mathematical operations | |
| const safePattern = /^[0-9+\-*/.()Math\w\s,]*$/; | |
| const withoutMath = cleanExpr.replace(/Math\.\w+/g, ''); | |
| if (!safePattern.test(withoutMath)) { | |
| throw new Error('Invalid expression'); | |
| } | |
| const result = eval(cleanExpr); | |
| return isFinite(result) ? result : NaN; | |
| } catch (e) { | |
| return NaN; | |
| } | |
| } | |
| function getColor() { | |
| try { | |
| if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') { | |
| return window.ColorPalettes.getColors('categorical', 1)[0]; | |
| } | |
| } catch (_) { } | |
| return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#4e79a7'; | |
| } | |
| function updateScales() { | |
| width = container.clientWidth || 800; | |
| height = Math.max(280, Math.round(width / 3)); | |
| svg.attr('width', width).attr('height', height); | |
| const innerWidth = width - margin.left - margin.right; | |
| const innerHeight = height - margin.top - margin.bottom; | |
| gRoot.attr('transform', `translate(${margin.left},${margin.top})`); | |
| const domainSize = parseFloat(inputRange.value); | |
| const xDomain = [-domainSize * Math.PI, domainSize * Math.PI]; | |
| xScale.domain(xDomain).range([0, innerWidth]); | |
| // Calculate y domain based on current function | |
| const equation = inputEquation.value.trim(); | |
| if (equation) { | |
| const testPoints = d3.range(xDomain[0], xDomain[1], (xDomain[1] - xDomain[0]) / 100); | |
| const yValues = testPoints.map(x => safeEval(equation, x)).filter(y => !isNaN(y) && isFinite(y)); | |
| if (yValues.length > 0) { | |
| const yExtent = d3.extent(yValues); | |
| const yPadding = (yExtent[1] - yExtent[0]) * 0.1 || 1; | |
| yScale.domain([yExtent[0] - yPadding, yExtent[1] + yPadding]).range([innerHeight, 0]); | |
| } else { | |
| yScale.domain([-2, 2]).range([innerHeight, 0]); | |
| } | |
| } else { | |
| yScale.domain([-2, 2]).range([innerHeight, 0]); | |
| } | |
| // Grid | |
| gGrid.selectAll('*').remove(); | |
| gGrid.selectAll('line.grid-x').data(xScale.ticks(8)).join('line') | |
| .attr('class', 'grid-x') | |
| .attr('x1', d => xScale(d)).attr('x2', d => xScale(d)) | |
| .attr('y1', 0).attr('y2', innerHeight) | |
| .attr('stroke', 'var(--grid-color)') | |
| .attr('stroke-width', 1) | |
| .attr('shape-rendering', 'crispEdges'); | |
| gGrid.selectAll('line.grid-y').data(yScale.ticks(6)).join('line') | |
| .attr('class', 'grid-y') | |
| .attr('x1', 0).attr('x2', innerWidth) | |
| .attr('y1', d => yScale(d)).attr('y2', d => yScale(d)) | |
| .attr('stroke', 'var(--grid-color)') | |
| .attr('stroke-width', 1) | |
| .attr('shape-rendering', 'crispEdges'); | |
| // Axes | |
| gAxes.selectAll('*').remove(); | |
| gAxes.append('g') | |
| .attr('transform', `translate(0,${innerHeight})`) | |
| .call(d3.axisBottom(xScale).ticks(8).tickFormat(d => { | |
| const val = d / Math.PI; | |
| if (Math.abs(val) < 0.01) return '0'; | |
| if (Math.abs(val - 1) < 0.01) return 'π'; | |
| if (Math.abs(val + 1) < 0.01) return '-π'; | |
| if (Math.abs(val - 0.5) < 0.01) return 'π/2'; | |
| if (Math.abs(val + 0.5) < 0.01) return '-π/2'; | |
| if (Math.abs(val % 1) < 0.01) return `${Math.round(val)}π`; | |
| return d3.format('.1f')(val) + 'π'; | |
| })) | |
| .call(g => { | |
| g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); | |
| g.selectAll('text').attr('fill', 'var(--tick-color)'); | |
| }); | |
| gAxes.append('g') | |
| .call(d3.axisLeft(yScale).ticks(6)) | |
| .call(g => { | |
| g.selectAll('path, line').attr('stroke', 'var(--axis-color)'); | |
| g.selectAll('text').attr('fill', 'var(--tick-color)'); | |
| }); | |
| // Axis labels | |
| gAxes.append('text') | |
| .attr('class', 'axis-label axis-label--x') | |
| .attr('x', innerWidth / 2) | |
| .attr('y', innerHeight + 44) | |
| .attr('text-anchor', 'middle') | |
| .text('x'); | |
| gAxes.append('text') | |
| .attr('class', 'axis-label axis-label--y') | |
| .attr('text-anchor', 'middle') | |
| .attr('transform', `translate(${-44},${innerHeight / 2}) rotate(-90)`) | |
| .text('f(x)'); | |
| return { innerWidth, innerHeight }; | |
| } | |
| function updatePlot() { | |
| errorMsg.style.display = 'none'; | |
| const equation = inputEquation.value.trim(); | |
| if (!equation) { | |
| gCurve.selectAll('*').remove(); | |
| return; | |
| } | |
| updateScales(); | |
| // Generate data points | |
| const domainSize = parseFloat(inputRange.value); | |
| const xDomain = [-domainSize * Math.PI, domainSize * Math.PI]; | |
| const numPoints = Math.max(200, Math.min(1000, Math.round((xDomain[1] - xDomain[0]) * 50))); | |
| const data = []; | |
| let hasValidPoints = false; | |
| let errorCount = 0; | |
| for (let i = 0; i <= numPoints; i++) { | |
| const x = xDomain[0] + (i / numPoints) * (xDomain[1] - xDomain[0]); | |
| const y = safeEval(equation, x); | |
| if (!isNaN(y) && isFinite(y)) { | |
| data.push({ x, y }); | |
| hasValidPoints = true; | |
| } else { | |
| errorCount++; | |
| } | |
| } | |
| if (!hasValidPoints) { | |
| errorMsg.textContent = `Error: unable to evaluate equation "${equation}"`; | |
| errorMsg.style.display = 'block'; | |
| gCurve.selectAll('*').remove(); | |
| return; | |
| } | |
| if (errorCount > numPoints * 0.5) { | |
| errorMsg.textContent = `Warning: ${errorCount} invalid points out of ${numPoints}`; | |
| errorMsg.style.display = 'block'; | |
| } | |
| // Draw the curve | |
| const color = getColor(); | |
| const path = gCurve.selectAll('path.function-curve').data([data]); | |
| path.enter() | |
| .append('path') | |
| .attr('class', 'function-curve') | |
| .attr('stroke', color) | |
| .merge(path) | |
| .transition() | |
| .duration(150) | |
| .attr('d', line) | |
| .attr('stroke', color); | |
| path.exit().remove(); | |
| // Hover interaction | |
| const overlay = gCurve.selectAll('rect.overlay').data([0]); | |
| const { innerWidth, innerHeight } = updateScales(); | |
| overlay.enter() | |
| .append('rect') | |
| .attr('class', 'overlay') | |
| .attr('fill', 'transparent') | |
| .style('cursor', 'crosshair') | |
| .merge(overlay) | |
| .attr('width', innerWidth) | |
| .attr('height', innerHeight) | |
| .on('mousemove', function (event) { | |
| const [mx] = d3.pointer(event, this); | |
| const x = xScale.invert(mx); | |
| const y = safeEval(equation, x); | |
| if (!isNaN(y) && isFinite(y)) { | |
| tipInner.innerHTML = ` | |
| <div><strong>f(${x.toFixed(3)}) = ${y.toFixed(3)}</strong></div> | |
| <div style="font-size:11px;color:var(--muted-color);margin-top:2px;">${equation}</div> | |
| `; | |
| tip.style.opacity = '1'; | |
| const tx = Math.max(0, Math.min(mx + margin.left + 12, (container.clientWidth || 0) - (tip.offsetWidth + 6))); | |
| const ty = Math.max(0, Math.min(yScale(y) + margin.top + 12, (container.clientHeight || 0) - (tip.offsetHeight + 6))); | |
| tip.style.transform = `translate(${Math.round(tx)}px, ${Math.round(ty)}px)`; | |
| } | |
| }) | |
| .on('mouseleave', function () { | |
| tip.style.opacity = '0'; | |
| tip.style.transform = 'translate(-9999px, -9999px)'; | |
| }); | |
| } | |
| // Event listeners | |
| inputEquation.addEventListener('input', updatePlot); | |
| inputRange.addEventListener('input', () => { | |
| const val = parseFloat(inputRange.value); | |
| rangeValue.textContent = `[-${val}π, ${val}π]`; | |
| updatePlot(); | |
| }); | |
| // Initial setup (already done above) | |
| // Initial render | |
| updatePlot(); | |
| // Resize handling | |
| const rerender = () => updatePlot(); | |
| if (window.ResizeObserver) { | |
| const ro = new ResizeObserver(() => rerender()); | |
| ro.observe(container); | |
| } else { | |
| window.addEventListener('resize', rerender); | |
| } | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); | |
| } else { | |
| ensureD3(bootstrap); | |
| } | |
| })(); | |
| </script> |