|
|
<!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> |