Download index.html from alterzick/dca: direct link, hf CLI and curl.
- Browser
- Download file 57 kB
-
https://huggingface.co/spaces/alterzick/dca/resolve/main/index.html
- Command line
-
hf download hf://spaces/alterzick/dca/index.html
-
curl -L -o index.html https://huggingface.co/spaces/alterzick/dca/resolve/main/index.html
57 kB
| <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; | |
| } | |
| /* Custom scrollbar */ | |
| .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 button animation */ | |
| .add-btn { | |
| transition: all 0.2s; | |
| } | |
| .add-btn:hover { | |
| transform: scale(1.05); | |
| background-color: #1e40af ; | |
| } | |
| /* Delete button styling */ | |
| .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; | |
| } | |
| /* Flash animation */ | |
| @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> | |
| <!-- Manual Input Section for Current Month and Production --> | |
| <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> | |
| <!-- Manual Production Data Input Section --> | |
| <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"> | |
| <!-- Manual data entries will be added here --> | |
| </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> | |
| <!-- Upload Section with Advanced Controls --> | |
| <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> | |
| <!-- Analysis Configuration Section --> | |
| <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> | |
| <!-- Upload Section --> | |
| <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> | |
| <!-- Results Section --> | |
| <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> | |
| <!-- Key Metrics Row --> | |
| <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> | |
| <!-- Current Performance Section --> | |
| <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> | |
| <!-- Chart --> | |
| <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> | |
| <!-- Data Table --> | |
| <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"> | |
| <!-- Data will be inserted here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- Export button --> | |
| <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> | |
| // Global variables | |
| let productionData = []; | |
| let manualData = []; | |
| let filteredData = []; | |
| let productionChart = null; | |
| let currentAnalysis = null; | |
| let dataSource = null; // 'manual', 'upload', or null | |
| // DOM Elements | |
| 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'); | |
| // Manual data input elements | |
| 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'); | |
| // Current well information input elements | |
| const currentMonthInput = document.getElementById('currentMonthInput'); | |
| const currentProductionInput = document.getElementById('currentProductionInput'); | |
| // Current performance elements | |
| const currentMonthEl = document.getElementById('currentMonth'); | |
| const currentProductionEl = document.getElementById('currentProduction'); | |
| const cumulativeProductionEl = document.getElementById('cumulativeProduction'); | |
| const productionLifeEl = document.getElementById('productionLife'); | |
| // Model descriptions | |
| 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.' | |
| }; | |
| // Event Listeners | |
| 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); | |
| // Model selection | |
| declineModelSelect.addEventListener('change', function() { | |
| modelDescription.textContent = modelDescriptions[this.value]; | |
| if (currentAnalysis) { | |
| updateAnalysisParameters(); | |
| updateChart(); | |
| } | |
| }); | |
| // Date filters | |
| startDateFilter.addEventListener('change', function() { | |
| applyDateFilter(); | |
| updateDateFilterStatus(); | |
| }); | |
| endDateFilter.addEventListener('change', function() { | |
| applyDateFilter(); | |
| updateDateFilterStatus(); | |
| }); | |
| // Update the current performance display when manual inputs change | |
| currentMonthInput.addEventListener('change', updateManualCurrentPerformance); | |
| currentProductionInput.addEventListener('input', updateManualCurrentPerformance); | |
| // Manual data input | |
| addDataBtn.addEventListener('click', addManualDataPoint); | |
| clearAllBtn.addEventListener('click', clearAllManualData); | |
| processManualDataBtn.addEventListener('click', processManualData); | |
| // Initialize with current date as default | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const now = new Date(); | |
| const dateString = now.toLocaleDateString('default', { month: 'short', year: 'numeric' }); | |
| currentMonthEl.textContent = dateString; | |
| // Set default model description | |
| modelDescription.textContent = modelDescriptions[declineModelSelect.value]; | |
| // Set today as default for date input | |
| const today = new Date().toISOString().split('T')[0]; | |
| productionDateInput.value = today; | |
| // Set initial data count | |
| updateDataCount(); | |
| }); | |
| // Update current performance display with manual inputs | |
| function updateManualCurrentPerformance() { | |
| // Update current month | |
| 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; | |
| // Update current production | |
| 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]]; | |
| // Convert to JSON | |
| const jsonData = XLSX.utils.sheet_to_json(firstSheet); | |
| // Parse and validate data | |
| productionData = parseProductionData(jsonData); | |
| if (productionData.length === 0) { | |
| alert('No valid production data found. Please check your Excel file format.'); | |
| return; | |
| } | |
| // Sort by date | |
| productionData.sort((a, b) => a.timeMonths - b.timeMonths); | |
| // Set date filter min/max values based on data | |
| setFilterDateBounds(); | |
| // Apply any existing filters | |
| applyDateFilter(); | |
| // Make sure current performance info is updated | |
| if (currentMonthInput.value || currentProductionInput.value) { | |
| updateManualCurrentPerformance(); | |
| } | |
| // Store source and display results | |
| dataSource = 'upload'; | |
| displayResults(); | |
| // Analyze decline curve with selected model | |
| currentAnalysis = performDeclineAnalysis(filteredData); | |
| // Update analysis info | |
| updateAnalysisInfo(currentAnalysis); | |
| // Update current performance - this will prioritize data from Excel if available | |
| updateCurrentPerformance(); | |
| // Create chart | |
| createChart(filteredData, currentAnalysis); | |
| // Show results section | |
| resultsSection.classList.remove('hidden'); | |
| // Show active filters if any | |
| updateActiveFiltersDisplay(); | |
| // Scroll to results | |
| 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; | |
| // Create a new data point | |
| const dataPoint = { | |
| id: Date.now(), // Use timestamp as unique ID | |
| date: date, | |
| rate: rate, | |
| cumulative: cumulative | |
| }; | |
| // Add to manual data array | |
| manualData.push(dataPoint); | |
| // Sort data by date | |
| manualData.sort((a, b) => a.date - b.date); | |
| // Recalculate time months based on the first data point | |
| 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)); | |
| }); | |
| } | |
| // Update the data table | |
| updateManualDataTable(); | |
| // Reset input fields | |
| productionRateInput.value = ''; | |
| cumulativeInput.value = ''; | |
| // Set focus back to rate input for quick data entry | |
| productionRateInput.focus(); | |
| // Update data count | |
| updateDataCount(); | |
| // Flash feedback | |
| addDataBtn.classList.add('flash'); | |
| setTimeout(() => { | |
| addDataBtn.classList.remove('flash'); | |
| }, 500); | |
| } | |
| function updateManualDataTable() { | |
| // Clear the table body | |
| manualDataTableBody.innerHTML = ''; | |
| // Add each data point to the table | |
| manualData.forEach(point => { | |
| const row = document.createElement('tr'); | |
| row.setAttribute('data-id', point.id); | |
| // Format the date | |
| const formattedDate = point.date.toLocaleDateString(); | |
| // Create the row content | |
| 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); | |
| }); | |
| // Add event listeners to delete buttons | |
| document.querySelectorAll('.delete-btn').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| const id = parseInt(this.getAttribute('data-id')); | |
| deleteDataPoint(id); | |
| }); | |
| }); | |
| } | |
| function deleteDataPoint(id) { | |
| // Remove the data point with the given ID | |
| manualData = manualData.filter(point => point.id !== id); | |
| // Recalculate time months based on the new first data point | |
| 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)); | |
| }); | |
| } | |
| // Update the table | |
| updateManualDataTable(); | |
| // Update data count | |
| updateDataCount(); | |
| // Flash feedback | |
| 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(); | |
| // Flash feedback | |
| clearAllBtn.classList.add('flash'); | |
| setTimeout(() => { | |
| clearAllBtn.classList.remove('flash'); | |
| }, 500); | |
| } | |
| } | |
| function updateDataCount() { | |
| dataCount.textContent = `${manualData.length} entries`; | |
| // Change color based on count | |
| 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; | |
| } | |
| // Use the manual data as production data | |
| productionData = [...manualData]; | |
| // Sort by date | |
| productionData.sort((a, b) => a.timeMonths - b.timeMonths); | |
| // Set date filter min/max values based on data | |
| setFilterDateBounds(); | |
| // Apply any existing filters | |
| applyDateFilter(); | |
| // Make sure current performance info is updated | |
| if (currentMonthInput.value || currentProductionInput.value) { | |
| updateManualCurrentPerformance(); | |
| } | |
| // Store source and display results | |
| dataSource = 'manual'; | |
| displayResults(); | |
| // Analyze decline curve with selected model | |
| currentAnalysis = performDeclineAnalysis(filteredData); | |
| // Update analysis info | |
| updateAnalysisInfo(currentAnalysis); | |
| // Update current performance | |
| updateCurrentPerformance(); | |
| // Create chart | |
| createChart(filteredData, currentAnalysis); | |
| // Show results section | |
| resultsSection.classList.remove('hidden'); | |
| // Show active filters if any | |
| updateActiveFiltersDisplay(); | |
| // Scroll to results | |
| resultsSection.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| function setFilterDateBounds() { | |
| if ((dataSource === 'upload' ? productionData : manualData).length === 0) return; | |
| const data = dataSource === 'upload' ? productionData : manualData; | |
| // Set min/max for date filters based on data | |
| 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; | |
| // Apply filters | |
| filteredData = data.filter(point => { | |
| const pointDate = new Date(point.date); | |
| const afterStart = !start || pointDate >= start; | |
| const beforeEnd = !end || pointDate <= end; | |
| return afterStart && beforeEnd; | |
| }); | |
| // Reset time months based on filtered data | |
| 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)); | |
| }); | |
| } | |
| // Update analysis if we have new filtered data | |
| if (currentAnalysis && filteredData.length > 0) { | |
| currentAnalysis = performDeclineAnalysis(filteredData); | |
| updateAnalysisInfo(currentAnalysis); | |
| createChart(filteredData, currentAnalysis); | |
| } | |
| // Update display results | |
| 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; | |
| // Add data source badge | |
| 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); | |
| // Add model filter badge | |
| const modelBadge = document.createElement('div'); | |
| modelBadge.className = 'filter-badge'; | |
| modelBadge.innerHTML = `<i class="fas fa-cogs"></i> ${model}`; | |
| activeFilters.appendChild(modelBadge); | |
| // Add date range badges if filters are active | |
| 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; | |
| // Try to detect columns by common names | |
| 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]); | |
| } | |
| } | |
| // Fallback: assume first three columns are date, rate, cumulative if no headers | |
| 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]); | |
| } | |
| // Validate data | |
| if (date && !isNaN(date.getTime()) && !isNaN(rate) && rate >= 0) { | |
| // Set start date for time calculations | |
| if (!startDate) { | |
| startDate = new Date(date); | |
| } | |
| // Calculate time in months from start | |
| 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() { | |
| // Update b factor based on selected model | |
| switch(declineModelSelect.value) { | |
| case 'exponential': | |
| currentAnalysis.b = 0; | |
| break; | |
| case 'harmonic': | |
| currentAnalysis.b = 1; | |
| break; | |
| case 'hyperbolic': | |
| // Keep calculated value or default to 0.5 | |
| if (currentAnalysis.b === 0 || currentAnalysis.b === 1) { | |
| currentAnalysis.b = 0.5; | |
| } | |
| break; | |
| } | |
| } | |
| function performDeclineAnalysis(data) { | |
| // Simple decline curve analysis using hyperbolic decline | |
| // q(t) = qi / (1 + b * Di * t)^(1/b) | |
| if (data.length === 0) return null; | |
| const qi = data[0].rate; // Initial rate from first point in filtered data | |
| const productionMonths = data.length > 1 ? (data[data.length - 1].timeMonths - data[0].timeMonths) : 0; | |
| // Simple estimation of decline parameters | |
| let Di = 0; // Nominal decline rate | |
| let b = 0.5; // Decline exponent (b=0 exponential, b=1 harmonic, 0<b<1 hyperbolic) | |
| let rSquared = 0; | |
| // Calculate nominal decline rate based on first and last points | |
| if (data.length > 1 && productionMonths > 0) { | |
| const qf = data[data.length - 1].rate; | |
| // For hyperbolic decline, we'll make a simple estimation | |
| const avgDecline = (qi - qf) / qi / (productionMonths/12); // Annual decline rate | |
| Di = Math.abs(avgDecline); | |
| // Adjust b factor based on the shape of the decline | |
| const declineRatio = qf / qi; | |
| if (declineRatio > 0.5) { | |
| b = 0.3; // Steeper decline | |
| } else if (declineRatio > 0.3) { | |
| b = 0.5; | |
| } else { | |
| b = 0.8; // Gentle decline | |
| } | |
| } else { | |
| Di = 0.2; // Default 20% annual decline | |
| b = 0.5; | |
| } | |
| // Apply model-specific constraints | |
| switch(declineModelSelect.value) { | |
| case 'exponential': | |
| b = 0; | |
| break; | |
| case 'harmonic': | |
| b = 1; | |
| break; | |
| } | |
| // Calculate R-squared (very simplified) | |
| 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) { | |
| // Hyperbolic decline model | |
| // q(t) = qi / (1 + b * Di * t)^(1/b) | |
| // Time is in years for standard decline parameters | |
| const timeYears = time / 12; | |
| if (b === 0) { | |
| // Exponential decline | |
| return qi * Math.exp(-Di * timeYears); | |
| } else if (Math.abs(b - 1) < 0.01) { | |
| // Harmonic decline | |
| return qi / (1 + Di * timeYears); | |
| } else { | |
| // Hyperbolic decline | |
| 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; | |
| // Get the latest data point | |
| const latestPoint = data[data.length - 1]; | |
| // Update current month | |
| currentMonthEl.textContent = latestPoint.date.toLocaleDateString('default', { month: 'short', year: 'numeric' }); | |
| // Update current production | |
| currentProductionEl.textContent = `${latestPoint.rate.toFixed(2)} BOPD`; | |
| // Update cumulative production | |
| if (latestPoint.cumulative) { | |
| cumulativeProductionEl.textContent = `${Math.round(latestPoint.cumulative).toLocaleString()} STB`; | |
| } else { | |
| // If cumulative values aren't provided, estimate based on trapezoidal rule | |
| let estimatedCumulative = 0; | |
| for (let i = 1; i < data.length; i++) { | |
| const dt = (data[i].timeMonths - data[i-1].timeMonths) * 30.44; // days | |
| const avgRate = (data[i].rate + data[i-1].rate) / 2; | |
| estimatedCumulative += avgRate * dt; | |
| } | |
| cumulativeProductionEl.textContent = `${Math.round(estimatedCumulative).toLocaleString()} STB (estimated)`; | |
| } | |
| // Calculate and display production life | |
| 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() { | |
| // Determine which data to use (filtered or original) | |
| const dataToUse = filteredData.length > 0 ? filteredData : | |
| (dataSource === 'upload' ? productionData : manualData); | |
| // Populate table | |
| 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'); | |
| // Destroy existing chart if it exists | |
| if (productionChart) { | |
| productionChart.destroy(); | |
| } | |
| const actualTime = data.map(d => d.timeMonths); | |
| const actualRates = data.map(d => d.rate); | |
| // Generate forecast data | |
| const maxTime = Math.max(...actualTime); | |
| const forecastTime = []; | |
| const forecastRates = []; | |
| const forecastPeriod = parseInt(forecastMonths.value); | |
| const b = analysis.b; // Use the b value from analysis which is set by the model selection | |
| // Create data points for the chart | |
| 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() { | |
| // In case the user didn't update the manual inputs, make sure current performance is set | |
| if (!currentProductionEl.textContent || currentProductionEl.textContent === "-") { | |
| updateManualCurrentPerformance(); | |
| } | |
| // Create a worksheet | |
| 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); | |
| // Add analysis results | |
| XLSX.utils.sheet_add_aoa(ws, [ | |
| ['', ''], // blank row | |
| ['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 }); | |
| // Create workbook and download | |
| const wb = XLSX.utils.book_new(); | |
| XLSX.utils.book_append_sheet(wb, ws, 'Production Data'); | |
| // Generate file name | |
| 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> |