azdindrissi/bode-buddy-visual-control-wizard
0
1document.addEventListener('DOMContentLoaded', function() {2 // Initialize charts3 const magnitudeCtx = document.getElementById('magnitudePlot').getContext('2d');4 const phaseCtx = document.getElementById('phasePlot').getContext('2d');5 6 let magnitudeChart = new Chart(magnitudeCtx, {7 type: 'line',8 data: {9 labels: [],10 datasets: [{11 label: 'Magnitude (dB)',12 data: [],13 borderColor: '#4f46e5',14 backgroundColor: 'rgba(79, 70, 229, 0.1)',15 borderWidth: 2,16 fill: true,17 tension: 0.418 }]19 },20 options: getChartOptions('Magnitude (dB)')21 });22 23 let phaseChart = new Chart(phaseCtx, {24 type: 'line',25 data: {26 labels: [],27 datasets: [{28 label: 'Phase (degrees)',29 data: [],30 borderColor: '#10b981',31 backgroundColor: 'rgba(16, 185, 129, 0.1)',32 borderWidth: 2,33 fill: true,34 tension: 0.435 }]36 },37 options: getChartOptions('Phase (degrees)')38 });39 40 // System type change handler41 document.getElementById('systemType').addEventListener('change', function() {42 const systemType = this.value;43 document.getElementById('dampingControl').classList.toggle('hidden', systemType === '1st');44 document.getElementById('naturalFreqControl').classList.toggle('hidden', systemType === '1st');45 updateSystemInfo();46 });47 48 // Slider value displays49 document.getElementById('gain').addEventListener('input', function() {50 document.getElementById('gainValue').textContent = this.value;51 });52 53 document.getElementById('timeConstant').addEventListener('input', function() {54 document.getElementById('timeConstantValue').textContent = this.value + ' s';55 });56 57 document.getElementById('dampingRatio').addEventListener('input', function() {58 document.getElementById('dampingRatioValue').textContent = this.value;59 });60 61 document.getElementById('naturalFreq').addEventListener('input', function() {62 document.getElementById('naturalFreqValue').textContent = this.value + ' rad/s';63 });64 65 // Update button click handler66 document.getElementById('updateBtn').addEventListener('click', updatePlots);67 68 // Initial update69 updatePlots();70 71 function updatePlots() {72 const systemType = document.getElementById('systemType').value;73 const K = parseFloat(document.getElementById('gain').value);74 const tau = parseFloat(document.getElementById('timeConstant').value);75 76 let omega = generateLogSpace(0.01, 100, 100);77 let magnitude, phase;78 79 if (systemType === '1st') {80 // 1st order system calculations81 magnitude = omega.map(w => 20 * Math.log10(K / Math.sqrt(1 + Math.pow(w * tau, 2))));82 phase = omega.map(w => -Math.atan(w * tau) * 180 / Math.PI);83 } else {84 // 2nd order system calculations85 const zeta = parseFloat(document.getElementById('dampingRatio').value);86 const wn = parseFloat(document.getElementById('naturalFreq').value);87 88 magnitude = omega.map(w => {89 const ratio = w / wn;90 return 20 * Math.log10(K / Math.sqrt(91 Math.pow(1 - Math.pow(ratio, 2), 2) + 92 Math.pow(2 * zeta * ratio, 2)93 ));94 });95 96 phase = omega.map(w => {97 const ratio = w / wn;98 return -Math.atan2(2 * zeta * ratio, 1 - Math.pow(ratio, 2)) * 180 / Math.PI;99 });100 }101 102 // Update charts103 magnitudeChart.data.labels = omega;104 magnitudeChart.data.datasets[0].data = magnitude;105 magnitudeChart.update();106 107 phaseChart.data.labels = omega;108 phaseChart.data.datasets[0].data = phase;109 phaseChart.update();110 111 updateSystemInfo();112 }113 114 function updateSystemInfo() {115 const systemType = document.getElementById('systemType').value;116 const K = parseFloat(document.getElementById('gain').value);117 const tau = parseFloat(document.getElementById('timeConstant').value);118 119 if (systemType === '1st') {120 document.getElementById('transferFunction').textContent = 121 `H(s) = ${K} / (1 + ${tau}s)`;122 document.getElementById('cutoffFreq').textContent = 123 `${(1/tau).toFixed(2)} rad/s`;124 document.getElementById('phaseAtCutoff').textContent = 125 `-45°`;126 } else {127 const zeta = parseFloat(document.getElementById('dampingRatio').value);128 const wn = parseFloat(document.getElementById('naturalFreq').value);129 130 document.getElementById('transferFunction').textContent = 131 `H(s) = ${K} / (s² + ${(2*zeta*wn).toFixed(2)}s + ${Math.pow(wn, 2).toFixed(2)})`;132 document.getElementById('cutoffFreq').textContent = 133 `${wn.toFixed(2)} rad/s`;134 document.getElementById('phaseAtCutoff').textContent = 135 `-${(zeta < 0.707 ? 180 : 90)}°`;136 }137 }138 139 function generateLogSpace(start, end, count) {140 let result = [];141 const logStart = Math.log10(start);142 const logEnd = Math.log10(end);143 const delta = (logEnd - logStart) / (count - 1);144 145 for (let i = 0; i < count; i++) {146 result.push(Math.pow(10, logStart + i * delta));147 }148 149 return result;150 }151 152 function getChartOptions(title) {153 return {154 responsive: true,155 maintainAspectRatio: false,156 scales: {157 x: {158 type: 'logarithmic',159 title: {160 display: true,161 text: 'Frequency (rad/s)',162 font: {163 weight: 'bold'164 }165 },166 ticks: {167 callback: function(value) {168 return value.toExponential(1);169 }170 }171 },172 y: {173 title: {174 display: true,175 text: title,176 font: {177 weight: 'bold'178 }179 }180 }181 },182 plugins: {183 legend: {184 display: false185 },186 tooltip: {187 callbacks: {188 label: function(context) {189 return `${title}: ${context.parsed.y.toFixed(2)}`;190 },191 title: function(context) {192 return `ω = ${context[0].parsed.x.toExponential(2)} rad/s`;193 }194 }195 }196 }197 };198 }199});