Upload or Enter Copper Market Data
Provide data with dates and prices. Prices are assumed to be in USD ($).
Or Enter Data Manually:
Current Data Preview:
No data loaded yet.
| Date | Price ($) |
|---|
Copper Market Analysis
Summary Statistics (USD $)
Date Range: -
Number of Data Points: -
Highest Price: - (on -)
Lowest Price: - (on -)
Average Price: -
Net Change: -
Percentage Change: -
No data to display. Please input data in the "Data Input" tab.
'; } this.clearSummaryStatistics(); } }, navigateToTab: function(tabId) { const targetTabLink = document.querySelector(`.cmt-tab-link[onclick*="'${tabId}'"]`); if (targetTabLink) { // Null check targetTabLink.click(); } else { console.error("Target tab link not found for navigation to:", tabId); } }, processDataAndNavigate: function(tabId) { this.processDataFromInputs(); // Process data first if (this.marketData.length > 0) { this.navigateToTab(tabId); // Then navigate } else { // Error message handled by processDataFromInputs or show generic const dataMessageEl = document.getElementById('cmtDataMessage'); if (dataMessageEl && !document.getElementById('cmtCsvError').classList.contains('cmt-hidden') && !document.getElementById('cmtManualDataError').classList.contains('cmt-hidden')) { // Keep existing error messages } else if (dataMessageEl) { dataMessageEl.textContent = "Please upload a valid CSV or enter data manually before proceeding."; dataMessageEl.classList.remove('cmt-hidden'); dataMessageEl.classList.add('cmt-error-message'); } } }, updateNavButtonsVisibility: function() { const prevBtnDataInput = document.querySelector('#cmtDataInputTab .prev-tab-btn'); const nextBtnDataInput = document.querySelector('#cmtDataInputTab .next-tab-btn'); const prevBtnAnalysis = document.querySelector('#cmtAnalysisTab .prev-tab-btn'); const nextBtnAnalysis = document.querySelector('#cmtAnalysisTab .next-tab-btn'); if (!prevBtnDataInput || !nextBtnDataInput || !prevBtnAnalysis || !nextBtnAnalysis) { console.error("Navigation buttons not found."); return; // Guard clause } if (this.currentTab === 'cmtDataInputTab') { prevBtnDataInput.style.visibility = 'hidden'; nextBtnDataInput.style.visibility = 'visible'; } else { prevBtnDataInput.style.visibility = 'visible'; // Should not happen as this is for DataInputTab } if (this.currentTab === 'cmtAnalysisTab') { prevBtnAnalysis.style.visibility = 'visible'; nextBtnAnalysis.style.visibility = 'hidden'; } else { prevBtnAnalysis.style.visibility = 'hidden'; // Should not happen for Analysis tab } }, handleFileUpload: function(event) { const fileInput = event.target; const csvErrorEl = document.getElementById('cmtCsvError'); const dataMessageEl = document.getElementById('cmtDataMessage'); if (!fileInput || !csvErrorEl || !dataMessageEl) { console.error("Required elements for file upload not found."); return; // Guard clause } csvErrorEl.classList.add('cmt-hidden'); csvErrorEl.textContent = ''; if (!fileInput.files || fileInput.files.length === 0) { csvErrorEl.textContent = 'No file selected.'; csvErrorEl.classList.remove('cmt-hidden'); return; } const file = fileInput.files[0]; if (file && file.type === "text/csv") { const reader = new FileReader(); reader.onload = (e) => { try { const text = e.target.result; const parsedData = this.parseCSV(text); if (parsedData.length === 0) { throw new Error("CSV file is empty or contains no valid data rows (expected Date,Price)."); } this.marketData = parsedData; this.sortData(); this.populateDataTable(); dataMessageEl.classList.add('cmt-hidden'); // Clear manual input fields if CSV is successfully loaded document.getElementById('cmtManualDataEntryContainer').innerHTML = ''; this.manualRowCount = 0; this.addManualEntryRow(); // Add a fresh one document.getElementById('cmtManualDataError').classList.add('cmt-hidden'); } catch (error) { this.marketData = []; csvErrorEl.textContent = 'Error parsing CSV: ' + error.message + ". Ensure format is Date,Price (e.g., YYYY-MM-DD,100.50 or MM/DD/YYYY,100.50). Header row is optional."; csvErrorEl.classList.remove('cmt-hidden'); this.clearDataTable(); dataMessageEl.textContent = "Failed to load data from CSV."; dataMessageEl.classList.remove('cmt-hidden'); } }; reader.onerror = () => { csvErrorEl.textContent = 'Error reading file.'; csvErrorEl.classList.remove('cmt-hidden'); this.clearDataTable(); dataMessageEl.textContent = "Failed to load data from CSV."; dataMessageEl.classList.remove('cmt-hidden'); }; reader.readAsText(file); } else { csvErrorEl.textContent = 'Please upload a valid .csv file.'; csvErrorEl.classList.remove('cmt-hidden'); this.clearDataTable(); if (fileInput) fileInput.value = ''; // Reset file input } }, parseCSV: function(csvText) { const lines = csvText.trim().split(/\r\n|\n/); const data = []; // Skip header if it looks like one, or try to parse all lines // A simple check: if the first line's second column isn't a number, assume header. let startLine = 0; if (lines.length > 0) { const firstLineParts = lines[0].split(','); if (firstLineParts.length === 2 && isNaN(parseFloat(firstLineParts[1]))) { startLine = 1; // Skip header } } for (let i = startLine; i < lines.length; i++) { if (lines[i].trim() === "") continue; // Skip empty lines const parts = lines[i].split(','); if (parts.length === 2) { const dateStr = parts[0].trim(); const priceStr = parts[1].trim(); const date = new Date(dateStr); const price = parseFloat(priceStr); if (!isNaN(date.getTime()) && !isNaN(price)) { data.push({ date: date, price: price }); } else { console.warn(`Skipping invalid row: ${lines[i]}. Date or Price is invalid.`); } } else { console.warn(`Skipping malformed row: ${lines[i]}. Expected 2 columns.`); } } if (data.length === 0 && lines.length > 0 && startLine === 0) { // If no data parsed and we didn't skip a header throw new Error("No valid data rows found. Ensure CSV has Date,Price columns and numeric prices."); } return data; }, addManualEntryRow: function() { this.manualRowCount++; const container = document.getElementById('cmtManualDataEntryContainer'); if (!container) { console.error("Manual entry container not found."); return; } const rowDiv = document.createElement('div'); rowDiv.className = 'cmt-manual-entry-row'; rowDiv.id = `cmtManualRow-${this.manualRowCount}`; const dateInput = document.createElement('input'); dateInput.type = 'date'; dateInput.className = 'cmt-manual-date'; dateInput.setAttribute('aria-label', 'Date for manual entry'); const priceInput = document.createElement('input'); priceInput.type = 'number'; priceInput.className = 'cmt-manual-price'; priceInput.placeholder = 'Price ($)'; priceInput.step = '0.01'; priceInput.setAttribute('aria-label', 'Price for manual entry'); const removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.textContent = 'Remove'; removeBtn.className = 'cmt-remove-row-btn'; removeBtn.onclick = () => { const rowToRemove = document.getElementById(rowDiv.id); if (rowToRemove) rowToRemove.remove(); // Prevent removing the last row if it's the only one. if (container.childElementCount === 0) { this.addManualEntryRow(); } }; rowDiv.appendChild(dateInput); rowDiv.appendChild(priceInput); rowDiv.appendChild(removeBtn); container.appendChild(rowDiv); }, processDataFromInputs: function() { // Priority to CSV if data exists from it if (this.marketData.length > 0 && document.getElementById('cmtFileUpload').files.length > 0) { // Data already processed from CSV upload by handleFileUpload // If user uploaded CSV then started manual entry, CSV data takes precedence on "Process" this.sortData(); this.populateDataTable(); return; } // Otherwise, try to process manual entries const manualErrorEl = document.getElementById('cmtManualDataError'); const dataMessageEl = document.getElementById('cmtDataMessage'); if (!manualErrorEl || !dataMessageEl) { console.error("Required elements for manual data processing not found."); return; } manualErrorEl.classList.add('cmt-hidden'); manualErrorEl.textContent = ''; this.marketData = []; const rows = document.querySelectorAll('#cmtManualDataEntryContainer .cmt-manual-entry-row'); let validManualDataFound = false; rows.forEach(row => { const dateInput = row.querySelector('.cmt-manual-date'); const priceInput = row.querySelector('.cmt-manual-price'); if (dateInput && priceInput && dateInput.value && priceInput.value) { // Check for nulls and empty values const date = new Date(dateInput.value); const price = parseFloat(priceInput.value); if (!isNaN(date.getTime()) && !isNaN(price)) { this.marketData.push({ date: date, price: price }); validManualDataFound = true; } } }); if (this.marketData.length > 0) { this.sortData(); this.populateDataTable(); dataMessageEl.classList.add('cmt-hidden'); // Clear CSV input if manual data is used const fileInput = document.getElementById('cmtFileUpload'); if (fileInput) fileInput.value = ''; document.getElementById('cmtCsvError').classList.add('cmt-hidden'); } else if (rows.length > 0 && !validManualDataFound) { // Rows exist but no valid data entered manualErrorEl.textContent = 'Please enter valid dates and prices in the manual fields.'; manualErrorEl.classList.remove('cmt-hidden'); this.clearDataTable(); dataMessageEl.textContent = "No valid manual data entered."; dataMessageEl.classList.remove('cmt-hidden'); } else if (document.getElementById('cmtFileUpload').files.length === 0) { // No CSV, no manual data this.clearDataTable(); dataMessageEl.textContent = "No data provided. Please upload a CSV or enter data manually."; dataMessageEl.classList.remove('cmt-hidden'); dataMessageEl.classList.remove('cmt-error-message'); // Reset to info } }, sortData: function() { this.marketData.sort((a, b) => a.date - b.date); }, populateDataTable: function() { const tbody = document.querySelector('#cmtDataTablePreview tbody'); const tableContainer = document.getElementById('cmtDataTablePreviewContainer'); const dataMessageEl = document.getElementById('cmtDataMessage'); if (!tbody || !tableContainer || !dataMessageEl) { console.error("Table elements not found."); return; } tbody.innerHTML = ''; // Clear previous data if (this.marketData.length === 0) { this.clearDataTable(); return; } this.marketData.forEach(item => { const row = tbody.insertRow(); const dateCell = row.insertCell(); const priceCell = row.insertCell(); dateCell.textContent = item.date.toLocaleDateString(); // Adjust format as needed priceCell.textContent = `$${item.price.toFixed(2)}`; }); tableContainer.classList.remove('cmt-hidden'); dataMessageEl.classList.add('cmt-hidden'); }, clearDataTable: function() { const tbody = document.querySelector('#cmtDataTablePreview tbody'); const tableContainer = document.getElementById('cmtDataTablePreviewContainer'); const dataMessageEl = document.getElementById('cmtDataMessage'); if (tbody) tbody.innerHTML = ''; if (tableContainer) tableContainer.classList.add('cmt-hidden'); if (dataMessageEl) { dataMessageEl.textContent = "No data loaded yet."; dataMessageEl.classList.remove('cmt-hidden'); dataMessageEl.classList.remove('cmt-error-message'); } this.marketData = []; // Clear data array }, renderChart: function() { const chartContainer = document.getElementById('cmtChartContainer'); if (!chartContainer) { console.error("Chart container not found."); return; } // Ensure a canvas element exists or recreate it if necessary let canvas = document.getElementById('copperChart'); if (!canvas) { chartContainer.innerHTML = ''; // Clear previous content (like error messages) canvas = document.createElement('canvas'); canvas.id = 'copperChart'; chartContainer.appendChild(canvas); } const ctx = canvas.getContext('2d'); if (!ctx) { console.error("Canvas context not found."); return; } if (this.chartInstance) { this.chartInstance.destroy(); } if (this.marketData.length === 0) { chartContainer.innerHTML = 'No data to display for chart.
'; return; } if (typeof Chart === 'undefined' || typeof Chart !== 'function') { console.error("Chart.js is not loaded. Cannot render chart."); chartContainer.innerHTML = "Charting library (Chart.js) is missing or failed to load. Chart cannot be displayed.
"; return; } this.chartInstance = new Chart(ctx, { type: 'line', data: { labels: this.marketData.map(item => item.date.toLocaleDateString()), datasets: [{ label: 'Copper Price (USD $)', data: this.marketData.map(item => item.price), borderColor: '#007bff', backgroundColor: 'rgba(0, 123, 255, 0.1)', fill: true, tension: 0.1 }] }, options: { responsive: true, maintainAspectRatio: true, // Maintain aspect ratio scales: { x: { title: { display: true, text: 'Date' }, ticks: { autoSkip: true, maxTicksLimit: 15 } // Adjust for readability }, y: { title: { display: true, text: 'Price (USD $)' }, ticks: { callback: function(value) { return '$' + value.toFixed(2); } } } }, plugins: { tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y); } return label; } } } } } }); }, displaySummaryStatistics: function() { const summaryDateRangeEl = document.getElementById('cmtSummaryDateRange'); const summaryDataPointsEl = document.getElementById('cmtSummaryDataPoints'); const summaryHighestPriceEl = document.getElementById('cmtSummaryHighestPrice'); const summaryHighestPriceDateEl = document.getElementById('cmtSummaryHighestPriceDate'); const summaryLowestPriceEl = document.getElementById('cmtSummaryLowestPrice'); const summaryLowestPriceDateEl = document.getElementById('cmtSummaryLowestPriceDate'); const summaryAveragePriceEl = document.getElementById('cmtSummaryAveragePrice'); const summaryNetChangeEl = document.getElementById('cmtSummaryNetChange'); const summaryPercentageChangeEl = document.getElementById('cmtSummaryPercentageChange'); // Null checks for all elements if (!summaryDateRangeEl || !summaryDataPointsEl || !summaryHighestPriceEl || !summaryHighestPriceDateEl || !summaryLowestPriceEl || !summaryLowestPriceDateEl || !summaryAveragePriceEl || !summaryNetChangeEl || !summaryPercentageChangeEl) { console.error("One or more summary statistic elements not found."); return; } if (this.marketData.length === 0) { this.clearSummaryStatistics(); return; } const firstDataPoint = this.marketData[0]; const lastDataPoint = this.marketData[this.marketData.length - 1]; summaryDateRangeEl.textContent = `${firstDataPoint.date.toLocaleDateString()} - ${lastDataPoint.date.toLocaleDateString()}`; summaryDataPointsEl.textContent = this.marketData.length; let highestPrice = -Infinity; let highestPriceDate = ''; let lowestPrice = Infinity; let lowestPriceDate = ''; let sumPrices = 0; this.marketData.forEach(item => { if (item.price > highestPrice) { highestPrice = item.price; highestPriceDate = item.date.toLocaleDateString(); } if (item.price < lowestPrice) { lowestPrice = item.price; lowestPriceDate = item.date.toLocaleDateString(); } sumPrices += item.price; }); summaryHighestPriceEl.textContent = `$${highestPrice.toFixed(2)}`; summaryHighestPriceDateEl.textContent = highestPriceDate; summaryLowestPriceEl.textContent = `$${lowestPrice.toFixed(2)}`; summaryLowestPriceDateEl.textContent = lowestPriceDate; summaryAveragePriceEl.textContent = `$${(sumPrices / this.marketData.length).toFixed(2)}`; const netChange = lastDataPoint.price - firstDataPoint.price; const percentageChange = (firstDataPoint.price === 0) ? (netChange === 0 ? 0 : Infinity) : (netChange / firstDataPoint.price) * 100; summaryNetChangeEl.textContent = `${netChange >= 0 ? '+' : ''}$${netChange.toFixed(2)}`; summaryPercentageChangeEl.textContent = `${percentageChange.toFixed(2)}%`; }, clearSummaryStatistics: function() { const fields = ['cmtSummaryDateRange', 'cmtSummaryDataPoints', 'cmtSummaryHighestPrice', 'cmtSummaryHighestPriceDate', 'cmtSummaryLowestPrice', 'cmtSummaryLowestPriceDate', 'cmtSummaryAveragePrice', 'cmtSummaryNetChange', 'cmtSummaryPercentageChange']; fields.forEach(id => { const el = document.getElementById(id); if (el) el.textContent = '-'; // Null check before setting }); }, downloadResultsAsPDF: function() { // Prepare UI for printing: ensure Analysis tab is "active" for print styles to catch it // This is a bit of a hack; print styles should ideally make it visible regardless of JS state const analysisTab = document.getElementById('cmtAnalysisTab'); const dataInputTab = document.getElementById('cmtDataInputTab'); let originalDataInputDisplay = ''; if(dataInputTab) { originalDataInputDisplay = dataInputTab.style.display; dataInputTab.style.display = 'none'; // Hide input tab for print } if(analysisTab) analysisTab.style.display = 'block'; // Ensure analysis tab is block for print // Make sure the chart is rendered before printing if (this.marketData.length > 0 && (!this.chartInstance || this.currentTab !== 'cmtAnalysisTab')) { this.renderChart(); // Re-render if not currently visible or instance gone } // Give browser a moment to apply styles and render chart setTimeout(() => { window.print(); // Restore display after print dialog if(dataInputTab) dataInputTab.style.display = originalDataInputDisplay; if(analysisTab && this.currentTab !== 'cmtAnalysisTab') analysisTab.style.display = 'none'; if(analysisTab && this.currentTab === 'cmtAnalysisTab') analysisTab.style.display = 'block'; // if it was the active tab, restore to block }, 500); // Delay may need adjustment } }; CMT_App.init();