|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | + <meta charset="UTF-8"> |
| 5 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | + <title>Jacobian Visualizer</title> |
| 7 | + <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> |
| 8 | + <style> |
| 9 | + .container { |
| 10 | + display: flex; |
| 11 | + } |
| 12 | + .plot { |
| 13 | + width: 45%; |
| 14 | + margin: 2%; |
| 15 | + } |
| 16 | + .input-group { |
| 17 | + margin-bottom: 10px; |
| 18 | + } |
| 19 | + </style> |
| 20 | +</head> |
| 21 | +<body> |
| 22 | + |
| 23 | + <H1> Jacobian Visualizer </H1> |
| 24 | + |
| 25 | + <p>Input two functions (in Javascript syntax). Specify an input region in terms of an x and y coordinate and x and y deltas. The input and output regions will be shown. Start with linear transforms. What do you observe. Now consider some nonlinear transforms -- note that for nonlinear transforms, the ratio of the areas can depend on the (x,y) coordinates. Set the delta values to something very small. What do you observe about the output areas. Compare the ratio of the input and output areas to the Jacobian when the delta values are small. </p> |
| 26 | + |
| 27 | +<div> |
| 28 | + <div class="input-group"> |
| 29 | + <label for="g">Function g(x, y): </label> |
| 30 | + <input type="text" id="g" value="x + y"> |
| 31 | + </div> |
| 32 | + <div class="input-group"> |
| 33 | + <label for="h">Function h(x, y): </label> |
| 34 | + <input type="text" id="h" value="x - y"> |
| 35 | + </div> |
| 36 | + <div class="input-group"> |
| 37 | + <label for="x">Location x: </label> |
| 38 | + <input type="number" id="x" value="0"> |
| 39 | + </div> |
| 40 | + <div class="input-group"> |
| 41 | + <label for="y">Location y: </label> |
| 42 | + <input type="number" id="y" value="0"> |
| 43 | + </div> |
| 44 | + <div class="input-group"> |
| 45 | + <label for="deltaX">Offset deltaX: </label> |
| 46 | + <input type="number" id="deltaX" value="1"> |
| 47 | + </div> |
| 48 | + <div class="input-group"> |
| 49 | + <label for="deltaY">Offset deltaY: </label> |
| 50 | + <input type="number" id="deltaY" value="1"> |
| 51 | + </div> |
| 52 | +</div> |
| 53 | + |
| 54 | +<div class="container"> |
| 55 | + <div class="plot" id="original"></div> |
| 56 | + <div class="plot" id="transformed"></div> |
| 57 | +</div> |
| 58 | + |
| 59 | +<div> |
| 60 | + <p>Input area: <span id="input-area"></span></p> |
| 61 | + <p>Estimate of output area: <span id="output-area"></span></p> |
| 62 | + <p></p> |
| 63 | + <p>Output area/Input area: <span id="area-ratio"></span></p> |
| 64 | +</div> |
| 65 | + |
| 66 | +<script> |
| 67 | +document.querySelectorAll('input').forEach(input => { |
| 68 | + input.addEventListener('input', updatePlots); |
| 69 | +}); |
| 70 | + |
| 71 | +function evaluateFunctions(g, h, x, y) { |
| 72 | + try { |
| 73 | + const gx = new Function('x', 'y', `return ${g}`); |
| 74 | + const hy = new Function('x', 'y', `return ${h}`); |
| 75 | + return [gx(x, y), hy(x, y)]; |
| 76 | + } catch (error) { |
| 77 | + console.error('Error evaluating functions:', error); |
| 78 | + return [NaN, NaN]; |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +function sampleEdge(x1, y1, x2, y2, numPoints) { |
| 83 | + const points = []; |
| 84 | + for (let i = 0; i <= numPoints; i++) { |
| 85 | + const t = i / numPoints; |
| 86 | + points.push([x1 + t * (x2 - x1), y1 + t * (y2 - y1)]); |
| 87 | + } |
| 88 | + return points; |
| 89 | +} |
| 90 | + |
| 91 | +function polygonArea(vertices) { |
| 92 | + const n = vertices.length; |
| 93 | + if (n < 3) return 0; // A polygon must have at least 3 vertices |
| 94 | + |
| 95 | + let area = 0; |
| 96 | + |
| 97 | + // Calculate the area using the Shoelace formula |
| 98 | + for (let i = 0; i < n; i++) { |
| 99 | + let j = (i + 1) % n; // Wrap around to the first vertex for the last edge |
| 100 | + area += vertices[i][0] * vertices[j][1]; |
| 101 | + area -= vertices[j][0] * vertices[i][1]; |
| 102 | + } |
| 103 | + |
| 104 | + return Math.abs(area) / 2; |
| 105 | +} |
| 106 | + |
| 107 | +function polygonAreaEdges(edges) { |
| 108 | + var vertices = []; |
| 109 | + for (i=0; i < edges.length; i++) { |
| 110 | + vertices = vertices.concat(edges[i]); |
| 111 | + } |
| 112 | + |
| 113 | + const n = vertices.length; |
| 114 | + if (n < 3) return 0; // A polygon must have at least 3 vertices |
| 115 | + |
| 116 | + let area = 0; |
| 117 | + |
| 118 | + // Calculate the area using the Shoelace formula |
| 119 | + for (let i = 0; i < n; i++) { |
| 120 | + let j = (i + 1) % n; // Wrap around to the first vertex for the last edge |
| 121 | + area += vertices[i][0] * vertices[j][1]; |
| 122 | + area -= vertices[j][0] * vertices[i][1]; |
| 123 | + } |
| 124 | + |
| 125 | + return Math.abs(area) / 2; |
| 126 | + } |
| 127 | + |
| 128 | + |
| 129 | +function updatePlots() { |
| 130 | + const g = document.getElementById('g').value; |
| 131 | + const h = document.getElementById('h').value; |
| 132 | + const x = parseFloat(document.getElementById('x').value); |
| 133 | + const y = parseFloat(document.getElementById('y').value); |
| 134 | + const deltaX = parseFloat(document.getElementById('deltaX').value); |
| 135 | + const deltaY = parseFloat(document.getElementById('deltaY').value); |
| 136 | + const numPoints = 100; // Number of sample points per edge |
| 137 | + |
| 138 | + // Define the rectangle vertices |
| 139 | + const rectEdges = [ |
| 140 | + sampleEdge(x, y, x + deltaX, y, numPoints), |
| 141 | + sampleEdge(x + deltaX, y, x + deltaX, y + deltaY, numPoints), |
| 142 | + sampleEdge(x + deltaX, y + deltaY, x, y + deltaY, numPoints), |
| 143 | + sampleEdge(x, y + deltaY, x, y, numPoints) |
| 144 | + ]; |
| 145 | + |
| 146 | + // Print the areas |
| 147 | + const inputArea = polygonArea([ [x,y], [x,y + deltaY], [x+deltaX, y+deltaY], [x+deltaX, y]]); |
| 148 | + document.getElementById("input-area").textContent = inputArea.toPrecision(3); |
| 149 | + |
| 150 | + // Flatten the array of edges |
| 151 | + const rectPoints = rectEdges.flat(); |
| 152 | + |
| 153 | + // Apply transformation |
| 154 | + const transformedPoints = rectPoints.map(([xi, yi]) => evaluateFunctions(g, h, xi, yi)); |
| 155 | + |
| 156 | + const outputArea = polygonArea(transformedPoints); |
| 157 | + document.getElementById("output-area").textContent = outputArea.toPrecision(3); |
| 158 | + |
| 159 | + document.getElementById("area-ratio").textContent = (outputArea/inputArea).toPrecision(3); |
| 160 | + |
| 161 | + // Define the edges colors |
| 162 | + const colors = ['red', 'green', 'blue', 'orange']; |
| 163 | + |
| 164 | + // Plot the original rectangle |
| 165 | + const originalPlot = { |
| 166 | + data: rectEdges.map((edge, i) => ({ |
| 167 | + x: edge.map(p => p[0]), |
| 168 | + y: edge.map(p => p[1]), |
| 169 | + type: 'scatter', |
| 170 | + mode: 'lines+markers', |
| 171 | + marker: { color: colors[i] }, |
| 172 | + line: { color: colors[i] } |
| 173 | + })), |
| 174 | + layout: { |
| 175 | + title: 'Original Rectangle', |
| 176 | + xaxis: { title: 'x' }, |
| 177 | + yaxis: { title: 'y', scaleanchor: 'x' }, |
| 178 | + hovermode: 'closest' |
| 179 | + } |
| 180 | + }; |
| 181 | + |
| 182 | + // Plot the transformed shape |
| 183 | + const transformedPlot = { |
| 184 | + data: rectEdges.map((edge, i) => { |
| 185 | + const transformedEdge = edge.map(([xi, yi]) => evaluateFunctions(g, h, xi, yi)); |
| 186 | + return { |
| 187 | + x: transformedEdge.map(p => p[0]), |
| 188 | + y: transformedEdge.map(p => p[1]), |
| 189 | + type: 'scatter', |
| 190 | + mode: 'lines+markers', |
| 191 | + marker: { color: colors[i] }, |
| 192 | + line: { color: colors[i] } |
| 193 | + } |
| 194 | + }), |
| 195 | + layout: { |
| 196 | + title: 'Transformed Shape', |
| 197 | + xaxis: { title: 'g(x, y)' }, |
| 198 | + yaxis: { title: 'h(x, y)', scaleanchor: 'x' }, |
| 199 | + hovermode: 'closest' |
| 200 | + } |
| 201 | + }; |
| 202 | + |
| 203 | + Plotly.newPlot('original', originalPlot.data, originalPlot.layout); |
| 204 | + Plotly.newPlot('transformed', transformedPlot.data, transformedPlot.layout); |
| 205 | + |
| 206 | + // Add hover event listeners to link the plots |
| 207 | + const originalPlotElement = document.getElementById('original'); |
| 208 | + const transformedPlotElement = document.getElementById('transformed'); |
| 209 | + |
| 210 | + originalPlotElement.on('plotly_hover', function(data) { |
| 211 | + const pointIndex = data.points[0].pointIndex; |
| 212 | + Plotly.Fx.hover('transformed', [ |
| 213 | + { curveNumber: data.points[0].curveNumber, pointIndex: pointIndex } |
| 214 | + ]); |
| 215 | + }); |
| 216 | + |
| 217 | + transformedPlotElement.on('plotly_hover', function(data) { |
| 218 | + const pointIndex = data.points[0].pointIndex; |
| 219 | + Plotly.Fx.hover('original', [ |
| 220 | + { curveNumber: data.points[0].curveNumber, pointIndex: pointIndex } |
| 221 | + ]); |
| 222 | + }); |
| 223 | + |
| 224 | + originalPlotElement.on('plotly_unhover', function(data) { |
| 225 | + Plotly.Fx.unhover('transformed'); |
| 226 | + }); |
| 227 | + |
| 228 | + transformedPlotElement.on('plotly_unhover', function(data) { |
| 229 | + Plotly.Fx.unhover('original'); |
| 230 | + }); |
| 231 | +} |
| 232 | + |
| 233 | +updatePlots(); // Initial plot |
| 234 | +</script> |
| 235 | + |
| 236 | +</body> |
| 237 | +</html> |
0 commit comments