| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
| <title>Decline Curve Analysis - Oil & Gas Well Production</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| } |
| |
| .chart-container { |
| position: relative; |
| margin: auto; |
| width: 100%; |
| max-width: 900px; |
| height: 500px; |
| } |
| |
| .upload-area { |
| border: 2px dashed #cbd5e1; |
| border-radius: 12px; |
| padding: 3rem 2rem; |
| text-align: center; |
| background-color: #f8fafc; |
| transition: all 0.3s ease; |
| cursor: pointer; |
| } |
| |
| .upload-area:hover { |
| border-color: #4c51bf; |
| background-color: #ebf4ff; |
| } |
| |
| .upload-area.highlight { |
| border-color: #2b6cb0; |
| background-color: #e6f0ff; |
| } |
| |
| .info-card { |
| transition: all 0.3s ease; |
| border-left: 4px solid #4c51bf; |
| } |
| |
| .info-card:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
| } |
| |
| .manual-input { |
| transition: all 0.3s ease; |
| border-left: 3px solid #2b6cb0; |
| } |
| |
| .manual-input:hover { |
| transform: translateY(-1px); |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); |
| } |
| |
| .filter-badge { |
| background: linear-gradient(90deg, #3b82f6, #60a5fa); |
| padding: 0.25rem 0.75rem; |
| border-radius: 9999px; |
| color: white; |
| font-size: 0.75rem; |
| font-weight: 600; |
| margin: 0.25rem; |
| display: inline-flex; |
| align-items: center; |
| } |
| |
| .filter-badge i { |
| margin-right: 0.25rem; |
| font-size: 0.625rem; |
| } |
| |
| .table-container { |
| max-height: 400px; |
| overflow-y: auto; |
| } |
| |
| |
| .table-container::-webkit-scrollbar { |
| width: 8px; |
| } |
| |
| .table-container::-webkit-scrollbar-track { |
| background: #f1f1f1; |
| border-radius: 10px; |
| } |
| |
| .table-container::-webkit-scrollbar-thumb { |
| background: #c1c1c1; |
| border-radius: 10px; |
| } |
| |
| .table-container::-webkit-scrollbar-thumb:hover { |
| background: #a1a1a1; |
| } |
| |
| |
| .add-btn { |
| transition: all 0.2s; |
| } |
| |
| .add-btn:hover { |
| transform: scale(1.05); |
| background-color: #1e40af !important; |
| } |
| |
| |
| .delete-btn { |
| background: #fee2e2; |
| padding: 0.25rem; |
| border-radius: 6px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| transition: all 0.2s; |
| } |
| |
| .delete-btn:hover { |
| background: #fecaca; |
| transform: scale(1.1); |
| } |
| |
| .delete-btn i { |
| color: #dc2626; |
| font-size: 0.875rem; |
| } |
| |
| |
| @keyframes flash { |
| 0% { opacity: 1; } |
| 50% { opacity: 0.5; } |
| 100% { opacity: 1; } |
| } |
| |
| .flash { |
| animation: flash 0.5s; |
| } |
| </style> |
| </head> |
| <body class="bg-gradient-to-br from-slate-50 to-slate-100 min-h-screen"> |
|
|
| <div class="container mx-auto px-4 py-10"> |
| <div class="text-center mb-10"> |
| <h1 class="text-4xl font-bold text-gray-800 mb-3">Decline Curve Analysis</h1> |
| <p class="text-lg text-gray-600 max-w-3xl mx-auto"> |
| Enter current well information, manually input production data or upload your production data in Excel format to analyze production decline and forecast future performance. |
| Supports exponential, hyperbolic, and harmonic decline models. |
| </p> |
| </div> |
|
|
| |
| <div class="bg-white rounded-2xl shadow-xl p-8 mb-8 max-w-4xl mx-auto transform hover:shadow-2xl transition-shadow duration-300"> |
| <div class="flex items-center mb-6 text-blue-700"> |
| <i class="fas fa-pen-alt text-2xl mr-3"></i> |
| <h2 class="text-2xl font-semibold">Current Well Information</h2> |
| </div> |
|
|
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8 manual-input bg-sky-50 p-6 rounded-xl border border-sky-200"> |
| <div class="mb-4"> |
| <label for="currentMonthInput" class="block text-sm font-medium text-gray-700 mb-2">Current Month of Production</label> |
| <div class="relative"> |
| <select id="currentMonthInput" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 appearance-none bg-white"> |
| <option value="">-- Select Month --</option> |
| <option value="0">Current (0 months)</option> |
| <option value="1">1 Month Ago</option> |
| <option value="2">2 Months Ago</option> |
| <option value="3">3 Months Ago</option> |
| <option value="6">6 Months Ago</option> |
| <option value="9">9 Months Ago</option> |
| <option value="12">12 Months Ago</option> |
| <option value="18">18 Months Ago</option> |
| <option value="24">2 Years Ago</option> |
| <option value="36">3 Years Ago</option> |
| <option value="48">4 Years Ago</option> |
| <option value="60">5 Years Ago</option> |
| </select> |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700"> |
| <i class="fas fa-chevron-down"></i> |
| </div> |
| </div> |
| <p class="mt-2 text-sm text-gray-500">How many months ago is this current data point?</p> |
| </div> |
|
|
| <div class="mb-4"> |
| <label for="currentProductionInput" class="block text-sm font-medium text-gray-700 mb-2">Current Production Rate (BOPD)</label> |
| <div class="relative"> |
| <input type="number" id="currentProductionInput" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Enter current production rate" min="0" step="0.1"> |
| <div class="absolute inset-y-0 right-0 flex items-center px-3 bg-blue-50 rounded-r-lg"> |
| <span class="text-sm text-blue-700 font-medium">BOPD</span> |
| </div> |
| </div> |
| <p class="mt-2 text-sm text-gray-500">Current oil production rate in barrels of oil per day</p> |
| </div> |
| </div> |
|
|
| <div class="bg-amber-50 p-4 rounded-lg border border-amber-200 mb-6"> |
| <div class="flex"> |
| <div class="flex-shrink-0"> |
| <i class="fas fa-info-circle text-amber-500 text-xl"></i> |
| </div> |
| <div class="ml-3"> |
| <h3 class="text-sm font-medium text-amber-800">Important Note</h3> |
| <p class="mt-1 text-sm text-amber-700"> |
| These values will be used as reference points for your analysis. They will be included in the results section before you upload your Excel file with historical data. |
| </p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="manualInputSection" class="bg-white rounded-2xl shadow-xl p-8 mb-8 max-w-6xl mx-auto transform hover:shadow-2xl transition-shadow duration-300"> |
| <div class="flex items-center mb-6 text-green-700"> |
| <i class="fas fa-keyboard text-2xl mr-3"></i> |
| <h2 class="text-2xl font-semibold">Manual Production Data Input</h2> |
| </div> |
|
|
| <div class="bg-green-50 p-6 rounded-xl border border-green-200 mb-6"> |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4"> |
| <div> |
| <label for="productionDateInput" class="block text-sm font-medium text-gray-700 mb-2">Date</label> |
| <input type="date" id="productionDateInput" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"> |
| </div> |
| <div> |
| <label for="productionRateInput" class="block text-sm font-medium text-gray-700 mb-2">Production Rate (BOPD)</label> |
| <div class="flex"> |
| <input type="number" id="productionRateInput" class="w-full px-4 py-3 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-green-500" placeholder="0.0" min="0" step="0.1"> |
| <div class="bg-green-50 px-4 rounded-r-lg flex items-center"> |
| <span class="text-sm text-green-700 font-medium">BOPD</span> |
| </div> |
| </div> |
| </div> |
| <div> |
| <label for="cumulativeInput" class="block text-sm font-medium text-gray-700 mb-2">Cumulative (STB)</label> |
| <div class="flex"> |
| <input type="number" id="cumulativeInput" class="w-full px-4 py-3 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-green-500" placeholder="0.0" min="0" step="1"> |
| <div class="bg-green-50 px-4 rounded-r-lg flex items-center"> |
| <span class="text-sm text-green-700 font-medium">STB</span> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="flex space-x-3"> |
| <button id="addDataBtn" class="add-btn px-6 py-3 bg-green-600 text-white font-medium rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 transition-colors duration-200 flex items-center justify-center flex-1"> |
| <i class="fas fa-plus mr-2"></i> Add Data Point |
| </button> |
| <button id="clearAllBtn" class="px-6 py-3 bg-red-600 text-white font-medium rounded-lg shadow-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50 transition-colors duration-200 flex items-center"> |
| <i class="fas fa-trash mr-2"></i> Clear All |
| </button> |
| </div> |
| </div> |
|
|
| <div class="bg-gray-50 p-6 rounded-xl border border-gray-200"> |
| <h3 class="text-lg font-semibold text-gray-800 mb-4 flex items-center"> |
| <i class="fas fa-database mr-2"></i> Inputted Production Data |
| <span id="dataCount" class="ml-2 bg-blue-100 text-blue-800 text-xs font-semibold px-2.5 py-0.5 rounded-full">0 entries</span> |
| </h3> |
| |
| <div class="table-container rounded-lg border border-gray-300 bg-white overflow-hidden"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50 sticky top-0"> |
| <tr> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rate (BOPD)</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cumulative (STB)</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time (months)</th> |
| </tr> |
| </thead> |
| <tbody id="manualDataTableBody" class="bg-white divide-y divide-gray-200"> |
| |
| </tbody> |
| </table> |
| </div> |
| |
| <div class="mt-4 text-sm text-gray-500"> |
| <p>Enter your production data manually point by point. Data will be automatically sorted by date.</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-2xl shadow-xl p-8 mb-8 max-w-4xl mx-auto transform hover:shadow-2xl transition-shadow duration-300"> |
| <div class="flex items-center mb-6 text-blue-700"> |
| <i class="fas fa-chart-line text-2xl mr-3"></i> |
| <h2 class="text-2xl font-semibold">Upload & Configure Analysis</h2> |
| </div> |
|
|
| |
| <div class="bg-indigo-50 p-6 rounded-xl border border-indigo-200 mb-8"> |
| <h3 class="text-lg font-semibold text-indigo-800 mb-4 flex items-center"> |
| <i class="fas fa-cogs mr-2"></i> Analysis Configuration |
| </h3> |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| <div> |
| <label for="declineModelSelect" class="block text-sm font-medium text-gray-700 mb-2">Decline Curve Model</label> |
| <div class="relative"> |
| <select id="declineModelSelect" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 appearance-none bg-white"> |
| <option value="hyperbolic" selected>Hyperbolic Decline</option> |
| <option value="exponential">Exponential Decline</option> |
| <option value="harmonic">Harmonic Decline</option> |
| </select> |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700"> |
| <i class="fas fa-chevron-down"></i> |
| </div> |
| </div> |
| <p id="modelDescription" class="mt-2 text-sm text-gray-600"> |
| Hyperbolic decline provides the most flexible modeling of production decline patterns. |
| </p> |
| </div> |
|
|
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Date Range for Analysis</label> |
| <div class="grid grid-cols-2 gap-2"> |
| <div> |
| <label for="startDateFilter" class="block text-xs text-gray-500 mb-1">From</label> |
| <input type="date" id="startDateFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
| </div> |
| <div> |
| <label for="endDateFilter" class="block text-xs text-gray-500 mb-1">To</label> |
| <input type="date" id="endDateFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"> |
| </div> |
| </div> |
| <div id="dateFilterStatus" class="mt-2 text-sm text-gray-500"> |
| Use date filters to analyze specific production periods. |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="dropZone" class="upload-area mb-6"> |
| <i class="fas fa-cloud-upload-alt text-5xl text-gray-400 mb-4"></i> |
| <h3 class="text-xl font-medium text-gray-700 mb-2">Or Drag & Drop Excel File</h3> |
| <p class="text-gray-500 mb-4">Upload your production data file</p> |
| <p class="text-sm text-gray-400">Supported: .xlsx, .xls | Example columns: Date, Rate (BOPD), Cumulative (STB)</p> |
| <input type="file" id="fileInput" accept=".xlsx, .xls" class="hidden"/> |
| </div> |
|
|
| <div class="flex space-x-4 justify-center"> |
| <button id="processManualDataBtn" class="px-6 py-3 bg-green-600 text-white font-medium rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 transition duration-200"> |
| <i class="fas fa-chart-line mr-2"></i>Process Manual Data |
| </button> |
| <button id="uploadBtn" class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition duration-200 disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
| <i class="fas fa-upload mr-2"></i>Process Excel File |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="resultsSection" class="hidden bg-white rounded-2xl shadow-xl p-8 max-w-6xl mx-auto"> |
| <div class="flex items-center justify-between mb-6"> |
| <div class="flex items-center text-green-700"> |
| <i class="fas fa-check-circle text-2xl mr-3"></i> |
| <h2 class="text-2xl font-semibold">Analysis Results</h2> |
| </div> |
| <div id="activeFilters" class="flex flex-wrap"></div> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-8"> |
| <div class="bg-blue-50 p-4 rounded-xl border border-blue-100 info-card"> |
| <p class="text-xs text-blue-600 font-medium">Initial Rate (qi)</p> |
| <p id="qiValue" class="text-2xl font-bold text-blue-800">-</p> |
| </div> |
| <div class="bg-green-50 p-4 rounded-xl border border-green-100 info-card"> |
| <p class="text-xs text-green-600 font-medium">Decline Rate (Di)</p> |
| <p id="diValue" class="text-2xl font-bold text-green-800">-</p> |
| </div> |
| <div class="bg-purple-50 p-4 rounded-xl border border-purple-100 info-card"> |
| <p class="text-xs text-purple-600 font-medium">Decline Exponent (b)</p> |
| <p id="bValue" class="text-2xl font-bold text-purple-800">-</p> |
| </div> |
| <div class="bg-orange-50 p-4 rounded-xl border border-orange-100 info-card"> |
| <p class="text-xs text-orange-600 font-medium">R² Fit</p> |
| <p id="r2Value" class="text-2xl font-bold text-orange-800">-</p> |
| </div> |
| <div class="bg-teal-50 p-4 rounded-xl border border-teal-100 info-card"> |
| <p class="text-xs text-teal-600 font-medium">Production Life</p> |
| <p id="productionLife" class="text-2xl font-bold text-teal-800">-</p> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-xl border border-blue-100 mb-8"> |
| <h3 class="text-lg font-semibold text-blue-800 mb-4 flex items-center"> |
| <i class="fas fa-fire mr-2"></i> Current Performance |
| </h3> |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
| <div class="text-center"> |
| <p class="text-sm text-gray-600 mb-1">Current Month</p> |
| <p id="currentMonth" class="text-2xl font-bold text-gray-800">-</p> |
| </div> |
| <div class="text-center"> |
| <p class="text-sm text-gray-600 mb-1">Current Production</p> |
| <p id="currentProduction" class="text-2xl font-bold text-green-600">-</p> |
| </div> |
| <div class="text-center"> |
| <p class="text-sm text-gray-600 mb-1">Cumulative Production</p> |
| <p id="cumulativeProduction" class="text-2xl font-bold text-purple-600">-</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="mb-6"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="text-xl font-semibold text-gray-800">Production Rate vs Time</h3> |
| <div class="flex items-center space-x-2"> |
| <span class="text-sm text-gray-600">Forecast:</span> |
| <input type="number" id="forecastMonths" class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-sm text-center" value="24" min="1" max="120"> |
| <span class="self-center text-sm text-gray-600">months</span> |
| </div> |
| </div> |
| <div class="chart-container"> |
| <canvas id="productionChart"></canvas> |
| </div> |
| </div> |
|
|
| |
| <div class="overflow-x-auto mt-8"> |
| <table id="dataTable" class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rate (BOPD)</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cumulative (STB)</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time (months)</th> |
| </tr> |
| </thead> |
| <tbody id="tableBody" class="bg-white divide-y divide-gray-200"> |
| |
| </tbody> |
| </table> |
| </div> |
|
|
| |
| <div class="text-right mt-6"> |
| <button id="exportBtn" class="px-4 py-2 bg-gray-600 text-white text-sm font-medium rounded-lg hover:bg-gray-700 transition duration-200"> |
| <i class="fas fa-download mr-2"></i>Export Results |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let productionData = []; |
| let manualData = []; |
| let filteredData = []; |
| let productionChart = null; |
| let currentAnalysis = null; |
| let dataSource = null; |
| |
| |
| const dropZone = document.getElementById('dropZone'); |
| const fileInput = document.getElementById('fileInput'); |
| const uploadBtn = document.getElementById('uploadBtn'); |
| const resultsSection = document.getElementById('resultsSection'); |
| const declineModelSelect = document.getElementById('declineModelSelect'); |
| const forecastMonths = document.getElementById('forecastMonths'); |
| const exportBtn = document.getElementById('exportBtn'); |
| const modelDescription = document.getElementById('modelDescription'); |
| const startDateFilter = document.getElementById('startDateFilter'); |
| const endDateFilter = document.getElementById('endDateFilter'); |
| const dateFilterStatus = document.getElementById('dateFilterStatus'); |
| const activeFilters = document.getElementById('activeFilters'); |
| const dataCount = document.getElementById('dataCount'); |
| |
| |
| const productionDateInput = document.getElementById('productionDateInput'); |
| const productionRateInput = document.getElementById('productionRateInput'); |
| const cumulativeInput = document.getElementById('cumulativeInput'); |
| const addDataBtn = document.getElementById('addDataBtn'); |
| const clearAllBtn = document.getElementById('clearAllBtn'); |
| const manualDataTableBody = document.getElementById('manualDataTableBody'); |
| const processManualDataBtn = document.getElementById('processManualDataBtn'); |
| |
| |
| const currentMonthInput = document.getElementById('currentMonthInput'); |
| const currentProductionInput = document.getElementById('currentProductionInput'); |
| |
| |
| const currentMonthEl = document.getElementById('currentMonth'); |
| const currentProductionEl = document.getElementById('currentProduction'); |
| const cumulativeProductionEl = document.getElementById('cumulativeProduction'); |
| const productionLifeEl = document.getElementById('productionLife'); |
| |
| |
| const modelDescriptions = { |
| 'exponential': 'Exponential decline assumes a constant percentage decline rate over time. Suitable for pressure-dominated reservoirs.', |
| 'hyperbolic': 'Hyperbolic decline provides the most flexibility, combining elements of both exponential and harmonic decline. Often the best fit for actual production data.', |
| 'harmonic': 'Harmonic decline assumes the decline rate decreases proportionally with production rate. Suitable for boundary-dominated flow.' |
| }; |
| |
| |
| dropZone.addEventListener('click', () => fileInput.click()); |
| dropZone.addEventListener('dragover', handleDragOver); |
| dropZone.addEventListener('dragleave', handleDragLeave); |
| dropZone.addEventListener('drop', handleDrop); |
| fileInput.addEventListener('change', handleFileSelect); |
| uploadBtn.addEventListener('click', processFile); |
| forecastMonths.addEventListener('change', updateChart); |
| exportBtn.addEventListener('click', exportResults); |
| |
| |
| declineModelSelect.addEventListener('change', function() { |
| modelDescription.textContent = modelDescriptions[this.value]; |
| if (currentAnalysis) { |
| updateAnalysisParameters(); |
| updateChart(); |
| } |
| }); |
| |
| |
| startDateFilter.addEventListener('change', function() { |
| applyDateFilter(); |
| updateDateFilterStatus(); |
| }); |
| |
| endDateFilter.addEventListener('change', function() { |
| applyDateFilter(); |
| updateDateFilterStatus(); |
| }); |
| |
| |
| currentMonthInput.addEventListener('change', updateManualCurrentPerformance); |
| currentProductionInput.addEventListener('input', updateManualCurrentPerformance); |
| |
| |
| addDataBtn.addEventListener('click', addManualDataPoint); |
| clearAllBtn.addEventListener('click', clearAllManualData); |
| processManualDataBtn.addEventListener('click', processManualData); |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| const now = new Date(); |
| const dateString = now.toLocaleDateString('default', { month: 'short', year: 'numeric' }); |
| currentMonthEl.textContent = dateString; |
| |
| |
| modelDescription.textContent = modelDescriptions[declineModelSelect.value]; |
| |
| |
| const today = new Date().toISOString().split('T')[0]; |
| productionDateInput.value = today; |
| |
| |
| updateDataCount(); |
| }); |
| |
| |
| function updateManualCurrentPerformance() { |
| |
| const monthsAgo = parseInt(currentMonthInput.value) || 0; |
| const now = new Date(); |
| now.setMonth(now.getMonth() - monthsAgo); |
| const dateString = now.toLocaleDateString('default', { month: 'short', year: 'numeric' }); |
| currentMonthEl.textContent = dateString; |
| |
| |
| const production = parseFloat(currentProductionInput.value); |
| if (!isNaN(production) && production >= 0) { |
| currentProductionEl.textContent = `${production.toFixed(2)} BOPD`; |
| } else { |
| currentProductionEl.textContent = "-"; |
| } |
| } |
| |
| function handleDragOver(e) { |
| e.preventDefault(); |
| dropZone.classList.add('highlight'); |
| } |
| |
| function handleDragLeave(e) { |
| e.preventDefault(); |
| dropZone.classList.remove('highlight'); |
| } |
| |
| function handleDrop(e) { |
| e.preventDefault(); |
| dropZone.classList.remove('highlight'); |
| |
| const files = e.dataTransfer.files; |
| if (files.length) { |
| fileInput.files = files; |
| handleFileSelect({ target: fileInput }); |
| } |
| } |
| |
| function handleFileSelect(e) { |
| const file = e.target.files[0]; |
| if (file) { |
| const fileName = file.name; |
| if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) { |
| uploadBtn.disabled = false; |
| dropZone.innerHTML = ` |
| <i class="fas fa-file-excel text-5xl text-green-500 mb-4"></i> |
| <h3 class="text-xl font-medium text-gray-700 mb-2">${file.name}</h3> |
| <p class="text-sm text-gray-500">Ready to process</p> |
| `; |
| } else { |
| alert('Please upload a valid Excel file (.xlsx or .xls)'); |
| uploadBtn.disabled = true; |
| } |
| } |
| } |
| |
| async function processFile() { |
| const file = fileInput.files[0]; |
| if (!file) return; |
| |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| try { |
| const data = new Uint8Array(e.target.result); |
| const workbook = XLSX.read(data, { type: 'array' }); |
| const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; |
| |
| |
| const jsonData = XLSX.utils.sheet_to_json(firstSheet); |
| |
| |
| productionData = parseProductionData(jsonData); |
| |
| if (productionData.length === 0) { |
| alert('No valid production data found. Please check your Excel file format.'); |
| return; |
| } |
| |
| |
| productionData.sort((a, b) => a.timeMonths - b.timeMonths); |
| |
| |
| setFilterDateBounds(); |
| |
| |
| applyDateFilter(); |
| |
| |
| if (currentMonthInput.value || currentProductionInput.value) { |
| updateManualCurrentPerformance(); |
| } |
| |
| |
| dataSource = 'upload'; |
| displayResults(); |
| |
| |
| currentAnalysis = performDeclineAnalysis(filteredData); |
| |
| |
| updateAnalysisInfo(currentAnalysis); |
| |
| |
| updateCurrentPerformance(); |
| |
| |
| createChart(filteredData, currentAnalysis); |
| |
| |
| resultsSection.classList.remove('hidden'); |
| |
| |
| updateActiveFiltersDisplay(); |
| |
| |
| resultsSection.scrollIntoView({ behavior: 'smooth' }); |
| |
| } catch (error) { |
| console.error('Error processing file:', error); |
| alert('Error processing the Excel file. Please make sure it is a valid file.'); |
| } |
| }; |
| |
| reader.readAsArrayBuffer(file); |
| } |
| |
| function addManualDataPoint() { |
| const dateValue = productionDateInput.value; |
| const rateValue = productionRateInput.value; |
| const cumulativeValue = cumulativeInput.value; |
| |
| if (!dateValue) { |
| alert('Please select a date'); |
| productionDateInput.focus(); |
| return; |
| } |
| |
| if (!rateValue || parseFloat(rateValue) < 0) { |
| alert('Please enter a valid production rate'); |
| productionRateInput.focus(); |
| return; |
| } |
| |
| const date = new Date(dateValue); |
| const rate = parseFloat(rateValue); |
| const cumulative = cumulativeValue ? parseFloat(cumulativeValue) : null; |
| |
| |
| const dataPoint = { |
| id: Date.now(), |
| date: date, |
| rate: rate, |
| cumulative: cumulative |
| }; |
| |
| |
| manualData.push(dataPoint); |
| |
| |
| manualData.sort((a, b) => a.date - b.date); |
| |
| |
| if (manualData.length > 0) { |
| const firstDate = manualData[0].date; |
| manualData.forEach(point => { |
| point.timeMonths = (point.date - firstDate) / (1000 * 60 * 60 * 24 * 30.44); |
| point.timeMonths = parseFloat(point.timeMonths.toFixed(2)); |
| }); |
| } |
| |
| |
| updateManualDataTable(); |
| |
| |
| productionRateInput.value = ''; |
| cumulativeInput.value = ''; |
| |
| |
| productionRateInput.focus(); |
| |
| |
| updateDataCount(); |
| |
| |
| addDataBtn.classList.add('flash'); |
| setTimeout(() => { |
| addDataBtn.classList.remove('flash'); |
| }, 500); |
| } |
| |
| function updateManualDataTable() { |
| |
| manualDataTableBody.innerHTML = ''; |
| |
| |
| manualData.forEach(point => { |
| const row = document.createElement('tr'); |
| row.setAttribute('data-id', point.id); |
| |
| |
| const formattedDate = point.date.toLocaleDateString(); |
| |
| |
| row.innerHTML = ` |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <div class="delete-btn" data-id="${point.id}"> |
| <i class="fas fa-times"></i> |
| </div> |
| </td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${formattedDate}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.rate.toFixed(2)}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.cumulative ? point.cumulative.toFixed(0) : '-'}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.timeMonths.toFixed(1)}</td> |
| `; |
| |
| manualDataTableBody.appendChild(row); |
| }); |
| |
| |
| document.querySelectorAll('.delete-btn').forEach(btn => { |
| btn.addEventListener('click', function() { |
| const id = parseInt(this.getAttribute('data-id')); |
| deleteDataPoint(id); |
| }); |
| }); |
| } |
| |
| function deleteDataPoint(id) { |
| |
| manualData = manualData.filter(point => point.id !== id); |
| |
| |
| if (manualData.length > 0) { |
| const firstDate = manualData[0].date; |
| manualData.forEach(point => { |
| point.timeMonths = (point.date - firstDate) / (1000 * 60 * 60 * 24 * 30.44); |
| point.timeMonths = parseFloat(point.timeMonths.toFixed(2)); |
| }); |
| } |
| |
| |
| updateManualDataTable(); |
| |
| |
| updateDataCount(); |
| |
| |
| const row = document.querySelector(`tr[data-id="${id}"]`); |
| if (row) { |
| row.style.backgroundColor = '#fee2e2'; |
| setTimeout(() => { |
| row.style.backgroundColor = ''; |
| }, 500); |
| } |
| } |
| |
| function clearAllManualData() { |
| if (manualData.length === 0) return; |
| |
| if (confirm('Are you sure you want to clear all manual data points?')) { |
| manualData = []; |
| updateManualDataTable(); |
| updateDataCount(); |
| |
| |
| clearAllBtn.classList.add('flash'); |
| setTimeout(() => { |
| clearAllBtn.classList.remove('flash'); |
| }, 500); |
| } |
| } |
| |
| function updateDataCount() { |
| dataCount.textContent = `${manualData.length} entries`; |
| |
| |
| if (manualData.length === 0) { |
| dataCount.className = 'ml-2 bg-red-100 text-red-800 text-xs font-semibold px-2.5 py-0.5 rounded-full'; |
| } else if (manualData.length < 3) { |
| dataCount.className = 'ml-2 bg-yellow-100 text-yellow-800 text-xs font-semibold px-2.5 py-0.5 rounded-full'; |
| } else { |
| dataCount.className = 'ml-2 bg-green-100 text-green-800 text-xs font-semibold px-2.5 py-0.5 rounded-full'; |
| } |
| } |
| |
| function processManualData() { |
| if (manualData.length === 0) { |
| alert('Please add at least one data point before processing'); |
| return; |
| } |
| |
| |
| productionData = [...manualData]; |
| |
| |
| productionData.sort((a, b) => a.timeMonths - b.timeMonths); |
| |
| |
| setFilterDateBounds(); |
| |
| |
| applyDateFilter(); |
| |
| |
| if (currentMonthInput.value || currentProductionInput.value) { |
| updateManualCurrentPerformance(); |
| } |
| |
| |
| dataSource = 'manual'; |
| displayResults(); |
| |
| |
| currentAnalysis = performDeclineAnalysis(filteredData); |
| |
| |
| updateAnalysisInfo(currentAnalysis); |
| |
| |
| updateCurrentPerformance(); |
| |
| |
| createChart(filteredData, currentAnalysis); |
| |
| |
| resultsSection.classList.remove('hidden'); |
| |
| |
| updateActiveFiltersDisplay(); |
| |
| |
| resultsSection.scrollIntoView({ behavior: 'smooth' }); |
| } |
| |
| function setFilterDateBounds() { |
| if ((dataSource === 'upload' ? productionData : manualData).length === 0) return; |
| |
| const data = dataSource === 'upload' ? productionData : manualData; |
| |
| |
| const minDate = new Date(data[0].date); |
| const maxDate = new Date(data[data.length - 1].date); |
| |
| startDateFilter.min = minDate.toISOString().split('T')[0]; |
| startDateFilter.max = maxDate.toISOString().split('T')[0]; |
| startDateFilter.value = minDate.toISOString().split('T')[0]; |
| |
| endDateFilter.min = minDate.toISOString().split('T')[0]; |
| endDateFilter.max = maxDate.toISOString().split('T')[0]; |
| endDateFilter.value = maxDate.toISOString().split('T')[0]; |
| |
| updateDateFilterStatus(); |
| } |
| |
| function applyDateFilter() { |
| if ((dataSource === 'upload' ? productionData : manualData).length === 0) return; |
| |
| const data = dataSource === 'upload' ? productionData : manualData; |
| const start = startDateFilter.value ? new Date(startDateFilter.value) : null; |
| const end = endDateFilter.value ? new Date(endDateFilter.value) : null; |
| |
| |
| filteredData = data.filter(point => { |
| const pointDate = new Date(point.date); |
| const afterStart = !start || pointDate >= start; |
| const beforeEnd = !end || pointDate <= end; |
| return afterStart && beforeEnd; |
| }); |
| |
| |
| if (filteredData.length > 0) { |
| const firstDate = new Date(filteredData[0].date); |
| filteredData.forEach(point => { |
| point.timeMonths = (new Date(point.date) - firstDate) / (1000 * 60 * 60 * 24 * 30.44); |
| point.timeMonths = parseFloat(point.timeMonths.toFixed(2)); |
| }); |
| } |
| |
| |
| if (currentAnalysis && filteredData.length > 0) { |
| currentAnalysis = performDeclineAnalysis(filteredData); |
| updateAnalysisInfo(currentAnalysis); |
| createChart(filteredData, currentAnalysis); |
| } |
| |
| |
| displayResults(); |
| updateActiveFiltersDisplay(); |
| } |
| |
| function updateDateFilterStatus() { |
| const start = startDateFilter.value; |
| const end = endDateFilter.value; |
| |
| if (!start && !end) { |
| dateFilterStatus.textContent = "No date filters applied. Analyzing all available data."; |
| dateFilterStatus.className = "mt-2 text-sm text-gray-500"; |
| } else if (start && end) { |
| dateFilterStatus.textContent = `Analyzing data from ${start} to ${end}.`; |
| dateFilterStatus.className = "mt-2 text-sm text-green-600 font-medium"; |
| } else if (start) { |
| dateFilterStatus.textContent = `Analyzing data from ${start} onwards.`; |
| dateFilterStatus.className = "mt-2 text-sm text-blue-600 font-medium"; |
| } else if (end) { |
| dateFilterStatus.textContent = `Analyzing data up to ${end}.`; |
| dateFilterStatus.className = "mt-2 text-sm text-blue-600 font-medium"; |
| } |
| } |
| |
| function updateActiveFiltersDisplay() { |
| activeFilters.innerHTML = ''; |
| |
| const start = startDateFilter.value; |
| const end = endDateFilter.value; |
| const model = declineModelSelect.options[declineModelSelect.selectedIndex].text; |
| |
| |
| const sourceBadge = document.createElement('div'); |
| sourceBadge.className = 'filter-badge'; |
| let sourceText = dataSource === 'manual' ? 'Manual Input' : dataSource === 'upload' ? 'Excel File' : 'No Data'; |
| let sourceIcon = dataSource === 'manual' ? 'fa-keyboard' : dataSource === 'upload' ? 'fa-file-excel' : 'fa-exclamation-triangle'; |
| sourceBadge.innerHTML = `<i class="fas ${sourceIcon}"></i> ${sourceText}`; |
| activeFilters.appendChild(sourceBadge); |
| |
| |
| const modelBadge = document.createElement('div'); |
| modelBadge.className = 'filter-badge'; |
| modelBadge.innerHTML = `<i class="fas fa-cogs"></i> ${model}`; |
| activeFilters.appendChild(modelBadge); |
| |
| |
| if (start) { |
| const startBadge = document.createElement('div'); |
| startBadge.className = 'filter-badge'; |
| startBadge.innerHTML = `<i class="fas fa-calendar"></i> From: ${new Date(start).toLocaleDateString()}`; |
| activeFilters.appendChild(startBadge); |
| } |
| |
| if (end) { |
| const endBadge = document.createElement('div'); |
| endBadge.className = 'filter-badge'; |
| endBadge.innerHTML = `<i class="fas fa-calendar"></i> To: ${new Date(end).toLocaleDateString()}`; |
| activeFilters.appendChild(endBadge); |
| } |
| } |
| |
| function parseProductionData(jsonData) { |
| const parsedData = []; |
| let startDate = null; |
| |
| for (let row of jsonData) { |
| let date, rate, cumulative; |
| |
| |
| for (let key in row) { |
| const keyLower = key.toLowerCase(); |
| if (date === undefined && (keyLower.includes('date') || keyLower.includes('time'))) { |
| date = new Date(row[key]); |
| } |
| if (rate === undefined && (keyLower.includes('rate') || keyLower.includes('oil') || keyLower.includes('prod'))) { |
| rate = parseFloat(row[key]); |
| } |
| if (cumulative === undefined && (keyLower.includes('cum') || keyLower.includes('total') || keyLower.includes('cumulative'))) { |
| cumulative = parseFloat(row[key]); |
| } |
| } |
| |
| |
| if (parsedData.length === 0 && Object.keys(row).length === 3 && date === undefined) { |
| const values = Object.values(row); |
| date = new Date(values[0]); |
| rate = parseFloat(values[1]); |
| cumulative = parseFloat(values[2]); |
| } |
| |
| |
| if (date && !isNaN(date.getTime()) && !isNaN(rate) && rate >= 0) { |
| |
| if (!startDate) { |
| startDate = new Date(date); |
| } |
| |
| |
| const timeMonths = (date - startDate) / (1000 * 60 * 60 * 24 * 30.44); |
| |
| parsedData.push({ |
| date: new Date(date), |
| rate: rate, |
| cumulative: !isNaN(cumulative) ? cumulative : null, |
| timeMonths: parseFloat(timeMonths.toFixed(2)) |
| }); |
| } |
| } |
| |
| return parsedData; |
| } |
| |
| function updateAnalysisParameters() { |
| |
| switch(declineModelSelect.value) { |
| case 'exponential': |
| currentAnalysis.b = 0; |
| break; |
| case 'harmonic': |
| currentAnalysis.b = 1; |
| break; |
| case 'hyperbolic': |
| |
| if (currentAnalysis.b === 0 || currentAnalysis.b === 1) { |
| currentAnalysis.b = 0.5; |
| } |
| break; |
| } |
| } |
| |
| function performDeclineAnalysis(data) { |
| |
| |
| |
| if (data.length === 0) return null; |
| |
| const qi = data[0].rate; |
| const productionMonths = data.length > 1 ? (data[data.length - 1].timeMonths - data[0].timeMonths) : 0; |
| |
| |
| let Di = 0; |
| let b = 0.5; |
| let rSquared = 0; |
| |
| |
| if (data.length > 1 && productionMonths > 0) { |
| const qf = data[data.length - 1].rate; |
| |
| |
| const avgDecline = (qi - qf) / qi / (productionMonths/12); |
| Di = Math.abs(avgDecline); |
| |
| |
| const declineRatio = qf / qi; |
| if (declineRatio > 0.5) { |
| b = 0.3; |
| } else if (declineRatio > 0.3) { |
| b = 0.5; |
| } else { |
| b = 0.8; |
| } |
| } else { |
| Di = 0.2; |
| b = 0.5; |
| } |
| |
| |
| switch(declineModelSelect.value) { |
| case 'exponential': |
| b = 0; |
| break; |
| case 'harmonic': |
| b = 1; |
| break; |
| } |
| |
| |
| let ssRes = 0; |
| let ssTot = 0; |
| const yMean = data.reduce((sum, point) => sum + point.rate, 0) / data.length; |
| |
| data.forEach(point => { |
| const predicted = predictRate(point.timeMonths, qi, Di, b); |
| ssRes += Math.pow(point.rate - predicted, 2); |
| ssTot += Math.pow(point.rate - yMean, 2); |
| }); |
| |
| rSquared = ssTot > 0 ? 1 - (ssRes / ssTot) : 0; |
| |
| return { qi, Di, b, rSquared }; |
| } |
| |
| function predictRate(time, qi, Di, b) { |
| |
| |
| |
| const timeYears = time / 12; |
| |
| if (b === 0) { |
| |
| return qi * Math.exp(-Di * timeYears); |
| } else if (Math.abs(b - 1) < 0.01) { |
| |
| return qi / (1 + Di * timeYears); |
| } else { |
| |
| const base = 1 + b * Di * timeYears; |
| return base > 0 ? qi / Math.pow(base, 1/b) : 0; |
| } |
| } |
| |
| function updateCurrentPerformance() { |
| if ((dataSource === 'upload' ? productionData : manualData).length === 0) return; |
| |
| const data = dataSource === 'upload' ? productionData : manualData; |
| |
| |
| const latestPoint = data[data.length - 1]; |
| |
| |
| currentMonthEl.textContent = latestPoint.date.toLocaleDateString('default', { month: 'short', year: 'numeric' }); |
| |
| |
| currentProductionEl.textContent = `${latestPoint.rate.toFixed(2)} BOPD`; |
| |
| |
| if (latestPoint.cumulative) { |
| cumulativeProductionEl.textContent = `${Math.round(latestPoint.cumulative).toLocaleString()} STB`; |
| } else { |
| |
| let estimatedCumulative = 0; |
| for (let i = 1; i < data.length; i++) { |
| const dt = (data[i].timeMonths - data[i-1].timeMonths) * 30.44; |
| const avgRate = (data[i].rate + data[i-1].rate) / 2; |
| estimatedCumulative += avgRate * dt; |
| } |
| cumulativeProductionEl.textContent = `${Math.round(estimatedCumulative).toLocaleString()} STB (estimated)`; |
| } |
| |
| |
| const productionLifeInMonths = latestPoint.timeMonths; |
| const years = Math.floor(productionLifeInMonths / 12); |
| const months = Math.round(productionLifeInMonths % 12); |
| |
| if (years > 0) { |
| productionLifeEl.textContent = `${years}y ${months}m`; |
| } else { |
| productionLifeEl.textContent = `${months} months`; |
| } |
| } |
| |
| function displayResults() { |
| |
| const dataToUse = filteredData.length > 0 ? filteredData : |
| (dataSource === 'upload' ? productionData : manualData); |
| |
| |
| const tableBody = document.getElementById('tableBody'); |
| tableBody.innerHTML = ''; |
| |
| if (dataToUse.length === 0) return; |
| |
| dataToUse.forEach(point => { |
| const row = document.createElement('tr'); |
| row.innerHTML = ` |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.date.toLocaleDateString()}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.rate.toFixed(2)}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.cumulative ? point.cumulative.toFixed(0) : '-'}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.timeMonths.toFixed(1)}</td> |
| `; |
| tableBody.appendChild(row); |
| }); |
| } |
| |
| function updateAnalysisInfo(analysis) { |
| if (!analysis) return; |
| |
| document.getElementById('qiValue').textContent = `${analysis.qi.toFixed(2)} BOPD`; |
| document.getElementById('diValue').textContent = `${(analysis.Di * 100).toFixed(1)}%/yr`; |
| document.getElementById('bValue').textContent = analysis.b.toFixed(2); |
| document.getElementById('r2Value').textContent = analysis.rSquared.toFixed(3); |
| } |
| |
| function createChart(data, analysis) { |
| if (!analysis || data.length === 0) return; |
| |
| const ctx = document.getElementById('productionChart').getContext('2d'); |
| |
| |
| if (productionChart) { |
| productionChart.destroy(); |
| } |
| |
| const actualTime = data.map(d => d.timeMonths); |
| const actualRates = data.map(d => d.rate); |
| |
| |
| const maxTime = Math.max(...actualTime); |
| const forecastTime = []; |
| const forecastRates = []; |
| |
| const forecastPeriod = parseInt(forecastMonths.value); |
| const b = analysis.b; |
| |
| |
| for (let t = 0; t <= maxTime + forecastPeriod; t += 0.5) { |
| forecastTime.push(t); |
| forecastRates.push(predictRate(t, analysis.qi, analysis.Di, b)); |
| } |
| |
| productionChart = new Chart(ctx, { |
| type: 'line', |
| data: { |
| labels: forecastTime, |
| datasets: [ |
| { |
| label: 'Historical Production', |
| data: actualRates, |
| borderColor: '#4c1d95', |
| backgroundColor: 'rgba(76, 29, 149, 0.1)', |
| borderWidth: 3, |
| pointBackgroundColor: '#4c1d95', |
| pointRadius: 4, |
| pointHoverRadius: 6, |
| fill: false, |
| tension: 0.2 |
| }, |
| { |
| label: 'Forecasted Decline', |
| data: forecastRates, |
| borderColor: '#059669', |
| borderDash: [10, 5], |
| borderWidth: 3, |
| pointBackgroundColor: '#059669', |
| pointRadius: 0, |
| fill: false, |
| tension: 0.2 |
| } |
| ] |
| }, |
| options: { |
| responsive: true, |
| maintainAspectRatio: false, |
| interaction: { |
| mode: 'index', |
| intersect: false |
| }, |
| plugins: { |
| tooltip: { |
| backgroundColor: 'rgba(0, 0, 0, 0.8)', |
| titleColor: '#ffffff', |
| bodyColor: '#ffffff', |
| borderColor: '#ffffff', |
| borderWidth: 1, |
| cornerRadius: 8, |
| displayColors: true, |
| callbacks: { |
| title: function(tooltipItems) { |
| const timeMonths = tooltipItems[0].label; |
| const years = Math.floor(timeMonths / 12); |
| const months = Math.round(timeMonths % 12); |
| return `Time: ${years}y ${months}m`; |
| }, |
| label: function(context) { |
| return `${context.dataset.label}: ${context.parsed.y.toFixed(2)} BOPD`; |
| } |
| } |
| }, |
| legend: { |
| position: 'top', |
| labels: { |
| usePointStyle: true, |
| padding: 20, |
| font: { |
| size: 14, |
| weight: 500 |
| } |
| } |
| }, |
| title: { |
| display: false |
| } |
| }, |
| scales: { |
| x: { |
| type: 'linear', |
| position: 'bottom', |
| title: { |
| display: true, |
| text: 'Time (months)', |
| font: { |
| size: 14, |
| weight: 500 |
| } |
| }, |
| grid: { |
| color: 'rgba(0, 0, 0, 0.05)' |
| } |
| }, |
| y: { |
| title: { |
| display: true, |
| text: 'Production Rate (BOPD)', |
| font: { |
| size: 14, |
| weight: 500 |
| } |
| }, |
| grid: { |
| color: 'rgba(0, 0, 0, 0.05)' |
| }, |
| beginAtZero: true |
| } |
| } |
| } |
| }); |
| } |
| |
| function updateChart() { |
| if (filteredData.length > 0 ? filteredData : |
| (dataSource === 'upload' ? productionData : manualData).length > 0 && currentAnalysis) { |
| updateAnalysisParameters(); |
| currentAnalysis = performDeclineAnalysis(filteredData.length > 0 ? filteredData : |
| (dataSource === 'upload' ? productionData : manualData)); |
| updateAnalysisInfo(currentAnalysis); |
| createChart(filteredData.length > 0 ? filteredData : |
| (dataSource === 'upload' ? productionData : manualData), currentAnalysis); |
| } |
| } |
| |
| function exportResults() { |
| |
| if (!currentProductionEl.textContent || currentProductionEl.textContent === "-") { |
| updateManualCurrentPerformance(); |
| } |
| |
| |
| const dataToUse = filteredData.length > 0 ? filteredData : |
| (dataSource === 'upload' ? productionData : manualData); |
| |
| const ws_data = [ |
| ['Date', 'Rate (BOPD)', 'Cumulative (STB)', 'Time (months)'], |
| ...dataToUse.map(d => [d.date.toLocaleDateString(), d.rate, d.cumulative, d.timeMonths]) |
| ]; |
| |
| const ws = XLSX.utils.aoa_to_sheet(ws_data); |
| |
| |
| XLSX.utils.sheet_add_aoa(ws, [ |
| ['', ''], |
| ['Analysis Results', ''], |
| ['Initial Rate (qi)', document.getElementById('qiValue').textContent], |
| ['Decline Rate (Di)', document.getElementById('diValue').textContent], |
| ['Decline Exponent (b)', document.getElementById('bValue').textContent], |
| ['R² Fit', document.getElementById('r2Value').textContent], |
| ['Production Life', document.getElementById('productionLife').textContent], |
| ['Current Month', currentMonthEl.textContent], |
| ['Current Production', currentProductionEl.textContent], |
| ['Cumulative Production', cumulativeProductionEl.textContent], |
| ['Data Source', dataSource === 'manual' ? 'Manual Input' : 'Uploaded Excel File'], |
| ['Model Used', declineModelSelect.options[declineModelSelect.selectedIndex].text], |
| ['Date Range', startDateFilter.value ? (endDateFilter.value ? `${startDateFilter.value} to ${endDateFilter.value}` : `From ${startDateFilter.value}`) : (endDateFilter.value ? `To ${endDateFilter.value}` : 'All data')], |
| ['Forecast Period', `${forecastMonths.value} months`] |
| ], { origin: -1 }); |
| |
| |
| const wb = XLSX.utils.book_new(); |
| XLSX.utils.book_append_sheet(wb, ws, 'Production Data'); |
| |
| |
| const dateStr = new Date().toISOString().slice(0, 10); |
| const source = dataSource === 'manual' ? 'Manual' : 'Excel'; |
| XLSX.writeFile(wb, `DeclineCurveAnalysis_${source}_${dateStr}.xlsx`); |
| } |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-qwensite.hf.space/logo.svg" alt="qwensite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-qwensite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >QwenSite</a> - 🧬 <a href="https://enzostvs-qwensite.hf.space?remix=alterzick/dca" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |