script.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. // 函数拟合工具 - 主脚本文件
  2. // 页面加载完成后执行初始化
  3. document.addEventListener('DOMContentLoaded', function() {
  4. // 初始化标签页切换功能
  5. initTabs();
  6. // 初始化数据表格
  7. initDataTable();
  8. // 初始化函数绘制功能
  9. initFunctionPlot();
  10. // 初始化表格排序功能
  11. initTableSorting();
  12. // 初始化数据导入导出功能
  13. initDataImportExport();
  14. // 初始化数据拟合功能
  15. // 将在下一部分实现
  16. // 初始化回归分析功能
  17. // 将在后续部分实现
  18. });
  19. /**
  20. * 初始化标签页切换功能
  21. */
  22. function initTabs() {
  23. const tabButtons = document.querySelectorAll('.tab-btn');
  24. const tabPanes = document.querySelectorAll('.tab-pane');
  25. tabButtons.forEach(button => {
  26. button.addEventListener('click', function() {
  27. // 移除所有标签页的激活状态
  28. tabButtons.forEach(btn => btn.classList.remove('active'));
  29. tabPanes.forEach(pane => pane.classList.remove('active'));
  30. // 激活当前点击的标签页
  31. this.classList.add('active');
  32. const tabId = this.getAttribute('data-tab');
  33. document.getElementById(tabId).classList.add('active');
  34. });
  35. });
  36. }
  37. /**
  38. * 初始化数据表格功能
  39. */
  40. function initDataTable() {
  41. // 获取表格元素
  42. const dataTable = document.getElementById('data-table');
  43. const tableBody = dataTable.querySelector('tbody') || document.createElement('tbody');
  44. // 如果表格没有tbody,则添加一个
  45. if (!dataTable.querySelector('tbody')) {
  46. dataTable.appendChild(tableBody);
  47. }
  48. // 绑定添加行按钮事件
  49. const addRowBtn = document.getElementById('add-row');
  50. if (addRowBtn) {
  51. addRowBtn.addEventListener('click', function() {
  52. addTableRow(tableBody);
  53. updateRowNumbers();
  54. });
  55. }
  56. // 使用事件委托绑定删除按钮事件
  57. // 这样可以处理所有现有和将来添加的删除按钮
  58. dataTable.addEventListener('click', function(event) {
  59. const target = event.target;
  60. if (target.classList.contains('remove-row')) {
  61. const row = target.closest('tr');
  62. if (row && tableBody.contains(row)) {
  63. tableBody.removeChild(row);
  64. updateRowNumbers();
  65. }
  66. }
  67. });
  68. }
  69. /**
  70. * 添加表格行
  71. * @param {HTMLElement} tableBody - 表格体元素
  72. */
  73. function addTableRow(tableBody) {
  74. const newRow = document.createElement('tr');
  75. // 创建序号单元格
  76. const indexCell = document.createElement('td');
  77. indexCell.className = 'row-index';
  78. indexCell.textContent = tableBody.children.length + 1;
  79. // 创建X输入单元格
  80. const xCell = document.createElement('td');
  81. const xInput = document.createElement('input');
  82. xInput.type = 'number';
  83. xInput.step = 'any';
  84. xInput.className = 'x-value';
  85. xCell.appendChild(xInput);
  86. // 创建Y输入单元格
  87. const yCell = document.createElement('td');
  88. const yInput = document.createElement('input');
  89. yInput.type = 'number';
  90. yInput.step = 'any';
  91. yInput.className = 'y-value';
  92. yCell.appendChild(yInput);
  93. // 创建操作单元格(删除按钮)
  94. const actionCell = document.createElement('td');
  95. const removeBtn = document.createElement('button');
  96. removeBtn.textContent = '删除';
  97. removeBtn.className = 'remove-row';
  98. removeBtn.addEventListener('click', function() {
  99. tableBody.removeChild(newRow);
  100. updateRowNumbers();
  101. });
  102. actionCell.appendChild(removeBtn);
  103. // 将单元格添加到行
  104. newRow.appendChild(indexCell);
  105. newRow.appendChild(xCell);
  106. newRow.appendChild(yCell);
  107. newRow.appendChild(actionCell);
  108. // 将行添加到表格
  109. tableBody.appendChild(newRow);
  110. }
  111. /**
  112. * 初始化函数绘制功能
  113. */
  114. function initFunctionPlot() {
  115. // 获取绘制按钮和输入元素
  116. const plotButton = document.getElementById('plot-function');
  117. const functionInput = document.getElementById('function-input');
  118. const xMinInput = document.getElementById('x-min');
  119. const xMaxInput = document.getElementById('x-max');
  120. // 添加绘制按钮点击事件
  121. plotButton.addEventListener('click', function() {
  122. const functionExpr = functionInput.value;
  123. const xMin = parseFloat(xMinInput.value);
  124. const xMax = parseFloat(xMaxInput.value);
  125. // 检查输入是否有效
  126. if (!functionExpr) {
  127. alert('请输入有效的函数表达式');
  128. return;
  129. }
  130. if (isNaN(xMin) || isNaN(xMax) || xMin >= xMax) {
  131. alert('请输入有效的X范围,且最小值应小于最大值');
  132. return;
  133. }
  134. // 绘制函数
  135. plotFunction(functionExpr, xMin, xMax);
  136. });
  137. }
  138. /**
  139. * 绘制函数
  140. * @param {string} functionExpr - 函数表达式
  141. * @param {number} xMin - X轴最小值
  142. * @param {number} xMax - X轴最大值
  143. */
  144. function plotFunction(functionExpr, xMin, xMax) {
  145. try {
  146. // 编译函数表达式
  147. const compiledFunction = math.compile(functionExpr);
  148. // 生成X值数组
  149. const step = (xMax - xMin) / 500;
  150. const xValues = [];
  151. const yValues = [];
  152. // 计算对应的Y值
  153. for (let x = xMin; x <= xMax; x += step) {
  154. try {
  155. const y = compiledFunction.evaluate({x: x});
  156. if (!isNaN(y) && isFinite(y)) {
  157. xValues.push(x);
  158. yValues.push(y);
  159. }
  160. } catch (error) {
  161. // 跳过计算错误的点
  162. continue;
  163. }
  164. }
  165. // 创建绘图数据
  166. const trace = {
  167. x: xValues,
  168. y: yValues,
  169. mode: 'lines',
  170. type: 'scatter',
  171. name: functionExpr,
  172. line: {
  173. color: '#3498db',
  174. width: 2
  175. }
  176. };
  177. // 绘图布局
  178. const layout = {
  179. title: '函数图像',
  180. xaxis: {
  181. title: 'X',
  182. zeroline: true,
  183. zerolinecolor: '#999',
  184. gridcolor: '#eee'
  185. },
  186. yaxis: {
  187. title: 'Y',
  188. zeroline: true,
  189. zerolinecolor: '#999',
  190. gridcolor: '#eee'
  191. },
  192. plot_bgcolor: '#fff',
  193. paper_bgcolor: '#fff',
  194. margin: { t: 50, b: 50, l: 50, r: 30 }
  195. };
  196. // 绘制图表
  197. Plotly.newPlot('plot-area', [trace], layout);
  198. // 使用MathJax显示函数表达式
  199. const equationResult = document.getElementById('equation-result');
  200. // 将函数表达式转换为LaTeX格式
  201. const latexExpr = convertToLatex(functionExpr);
  202. equationResult.innerHTML = '$$y = ' + latexExpr + '$$';
  203. // 重新渲染MathJax
  204. if (window.MathJax) {
  205. MathJax.typesetPromise([equationResult]).catch(function (err) {
  206. console.error('MathJax渲染错误:', err);
  207. });
  208. }
  209. } catch (error) {
  210. alert('函数绘制错误: ' + error.message);
  211. }
  212. }
  213. /**
  214. * 更新表格行序号
  215. */
  216. function updateRowNumbers() {
  217. const tableBody = document.querySelector('#data-table tbody');
  218. const rows = tableBody.querySelectorAll('tr');
  219. rows.forEach((row, index) => {
  220. const indexCell = row.querySelector('.row-index');
  221. if (indexCell) {
  222. indexCell.textContent = index + 1;
  223. }
  224. });
  225. }
  226. /**
  227. * 初始化表格排序功能
  228. */
  229. function initTableSorting() {
  230. const sortableHeaders = document.querySelectorAll('.professional-table th.sortable');
  231. sortableHeaders.forEach(header => {
  232. header.addEventListener('click', function() {
  233. const table = this.closest('table');
  234. const tbody = table.querySelector('tbody');
  235. const rows = Array.from(tbody.querySelectorAll('tr'));
  236. const columnIndex = Array.from(this.parentNode.children).indexOf(this);
  237. const isAsc = !this.classList.contains('sorted-asc');
  238. // 移除所有排序标记
  239. sortableHeaders.forEach(h => {
  240. h.classList.remove('sorted-asc', 'sorted-desc');
  241. });
  242. // 添加当前排序标记
  243. this.classList.add(isAsc ? 'sorted-asc' : 'sorted-desc');
  244. // 排序行
  245. rows.sort((a, b) => {
  246. let aValue, bValue;
  247. if (columnIndex === 0) { // 序号列
  248. aValue = parseInt(a.cells[columnIndex].textContent);
  249. bValue = parseInt(b.cells[columnIndex].textContent);
  250. } else if (columnIndex === 1 || columnIndex === 2) { // X或Y列
  251. const aInput = a.cells[columnIndex].querySelector('input');
  252. const bInput = b.cells[columnIndex].querySelector('input');
  253. aValue = aInput ? parseFloat(aInput.value) : 0;
  254. bValue = bInput ? parseFloat(bInput.value) : 0;
  255. if (isNaN(aValue)) aValue = 0;
  256. if (isNaN(bValue)) bValue = 0;
  257. }
  258. return isAsc ? aValue - bValue : bValue - aValue;
  259. });
  260. // 重新添加排序后的行
  261. rows.forEach(row => tbody.appendChild(row));
  262. // 更新行序号
  263. updateRowNumbers();
  264. });
  265. });
  266. }
  267. /**
  268. * 初始化数据导入导出功能
  269. */
  270. function initDataImportExport() {
  271. const importBtn = document.getElementById('import-data');
  272. const exportBtn = document.getElementById('export-data');
  273. if (importBtn) {
  274. importBtn.addEventListener('click', function() {
  275. const input = document.createElement('input');
  276. input.type = 'file';
  277. input.accept = '.csv,.txt';
  278. input.addEventListener('change', function(e) {
  279. const file = e.target.files[0];
  280. if (!file) return;
  281. const reader = new FileReader();
  282. reader.onload = function(e) {
  283. const content = e.target.result;
  284. importDataFromCSV(content);
  285. };
  286. reader.readAsText(file);
  287. });
  288. input.click();
  289. });
  290. }
  291. if (exportBtn) {
  292. exportBtn.addEventListener('click', function() {
  293. exportDataToCSV();
  294. });
  295. }
  296. }
  297. /**
  298. * 从CSV导入数据
  299. */
  300. function importDataFromCSV(csvContent) {
  301. const lines = csvContent.split('\n');
  302. const dataPoints = [];
  303. // 跳过可能的标题行
  304. for (let i = 0; i < lines.length; i++) {
  305. const line = lines[i].trim();
  306. if (!line) continue;
  307. const values = line.split(',');
  308. if (values.length >= 2) {
  309. const x = parseFloat(values[0]);
  310. const y = parseFloat(values[1]);
  311. if (!isNaN(x) && !isNaN(y)) {
  312. dataPoints.push({ x, y });
  313. }
  314. }
  315. }
  316. if (dataPoints.length === 0) {
  317. alert('没有找到有效的数据点');
  318. return;
  319. }
  320. // 清空现有表格
  321. const tableBody = document.querySelector('#data-table tbody');
  322. tableBody.innerHTML = '';
  323. // 添加导入的数据点
  324. dataPoints.forEach(point => {
  325. const newRow = document.createElement('tr');
  326. // 序号单元格
  327. const indexCell = document.createElement('td');
  328. indexCell.className = 'row-index';
  329. indexCell.textContent = tableBody.children.length + 1;
  330. // X值单元格
  331. const xCell = document.createElement('td');
  332. const xInput = document.createElement('input');
  333. xInput.type = 'number';
  334. xInput.step = 'any';
  335. xInput.className = 'x-value';
  336. xInput.value = point.x;
  337. xCell.appendChild(xInput);
  338. // Y值单元格
  339. const yCell = document.createElement('td');
  340. const yInput = document.createElement('input');
  341. yInput.type = 'number';
  342. yInput.step = 'any';
  343. yInput.className = 'y-value';
  344. yInput.value = point.y;
  345. yCell.appendChild(yInput);
  346. // 操作单元格
  347. const actionCell = document.createElement('td');
  348. const removeBtn = document.createElement('button');
  349. removeBtn.textContent = '删除';
  350. removeBtn.className = 'remove-row';
  351. removeBtn.addEventListener('click', function() {
  352. tableBody.removeChild(newRow);
  353. updateRowNumbers();
  354. });
  355. actionCell.appendChild(removeBtn);
  356. // 添加单元格到行
  357. newRow.appendChild(indexCell);
  358. newRow.appendChild(xCell);
  359. newRow.appendChild(yCell);
  360. newRow.appendChild(actionCell);
  361. // 添加行到表格
  362. tableBody.appendChild(newRow);
  363. });
  364. alert(`成功导入 ${dataPoints.length} 个数据点`);
  365. }
  366. /**
  367. * 导出数据到CSV
  368. */
  369. function exportDataToCSV() {
  370. const dataPoints = getDataPointsFromTable();
  371. if (dataPoints.length === 0) {
  372. alert('没有数据可导出');
  373. return;
  374. }
  375. let csvContent = 'X,Y\n';
  376. dataPoints.forEach(point => {
  377. csvContent += `${point.x},${point.y}\n`;
  378. });
  379. const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
  380. const url = URL.createObjectURL(blob);
  381. const link = document.createElement('a');
  382. link.href = url;
  383. link.setAttribute('download', '数据点.csv');
  384. link.style.visibility = 'hidden';
  385. document.body.appendChild(link);
  386. link.click();
  387. document.body.removeChild(link);
  388. }
  389. /**
  390. * 将函数表达式转换为LaTeX格式
  391. */
  392. function convertToLatex(expr) {
  393. // 基本替换
  394. let latex = expr
  395. .replace(/\*/g, ' \\cdot ')
  396. .replace(/\//g, ' \\div ')
  397. .replace(/\^(\d+)/g, '^{$1}')
  398. .replace(/\^([a-zA-Z])/g, '^{$1}')
  399. .replace(/sqrt\(([^)]+)\)/g, '\\sqrt{$1}')
  400. .replace(/sin\(([^)]+)\)/g, '\\sin{($1)}')
  401. .replace(/cos\(([^)]+)\)/g, '\\cos{($1)}')
  402. .replace(/tan\(([^)]+)\)/g, '\\tan{($1)}')
  403. .replace(/log\(([^)]+)\)/g, '\\log{($1)}')
  404. .replace(/ln\(([^)]+)\)/g, '\\ln{($1)}')
  405. .replace(/exp\(([^)]+)\)/g, 'e^{$1}');
  406. // 处理分数
  407. latex = latex.replace(/(\d+)\/(\d+)/g, '\\frac{$1}{$2}');
  408. return latex;
  409. }