/** * Pricify - Stock Screener (Client-side filtering) * Allows real-time filtering by exchange, sector, price range, * change %, and volume. Works with any table that has data attributes. */ (function() { 'use strict'; // ============================================================ // CONFIGURATION // ============================================================ const DEBOUNCE_MS = 200; const SECTOR_MAP = { 'BANKING': 'Banking & Financials', 'IT': 'Information Technology', 'PHARMA': 'Pharma & Healthcare', 'AUTO': 'Automobile', 'FMCG': 'FMCG & Consumer', 'ENERGY': 'Energy & Power', 'METALS': 'Metals & Mining', 'REALTY': 'Real Estate', 'MEDIA': 'Media & Entertainment', 'TELECOM': 'Telecommunications', 'CONSTRUCTION': 'Construction & Infra', 'CONSUMER_DURABLES': 'Consumer Durables', 'OIL_GAS': 'Oil & Gas', 'DIVERSIFIED': 'Diversified', 'ETF': 'ETFs', 'TECHNOLOGY': 'Technology', 'FINANCIAL': 'Financial Services', 'HEALTHCARE': 'Healthcare', 'CONSUMER_CYCLICAL': 'Consumer Cyclical', 'INDUSTRIALS': 'Industrials', 'UTILITIES': 'Utilities' }; const EXCHANGE_MAP = { 'NSE': '🇮🇳 NSE (India)', 'BSE': '🇮🇳 BSE (India)', 'US': '🇺🇸 USA', 'UK': '🇬🇧 UK', 'JP': '🇯🇵 Japan', 'HK': '🇭🇰 Hong Kong', 'EU': '🇪🇺 Europe', 'AU': '🇦🇺 Australia', 'CA': '🇨🇦 Canada' }; // ============================================================ // SCREENER ENGINE // ============================================================ class StockScreener { constructor(options) { this.table = options.table; this.tbody = this.table ? this.table.querySelector('tbody') : null; this.filterContainer = options.filterContainer; this.resultCount = options.resultCount; this.callback = options.onFilter || null; this.filters = { search: '', exchange: 'all', sector: 'all', priceMin: 0, priceMax: Infinity, changeMin: -Infinity, changeMax: Infinity, volumeMin: 0, volumeMax: Infinity }; this.allRows = []; this.filteredRows = []; if (this.table) { this._init(); } } _init() { var self = this; // Gather all rows if (this.tbody) { this.allRows = Array.prototype.slice.call(this.tbody.querySelectorAll('tr')); } // Set up filter inputs if (this.filterContainer) { var inputs = this.filterContainer.querySelectorAll('[data-filter]'); Array.prototype.forEach.call(inputs, function(input) { var filterName = input.getAttribute('data-filter'); var eventType = input.getAttribute('data-event') || 'input'; input.addEventListener(eventType, function() { self._updateFilter(filterName, input); self._debouncedFilter(); }); }); // Reset button var resetBtn = this.filterContainer.querySelector('[data-action="reset"]'); if (resetBtn) { resetBtn.addEventListener('click', function() { self._resetFilters(); self._applyFilters(); }); } } // Initial filter this._applyFilters(); } _updateFilter(name, input) { var val = input.value; var type = input.getAttribute('data-type') || 'text'; switch (type) { case 'select': this.filters[name] = val; break; case 'range': this.filters[name] = parseFloat(val) || 0; break; case 'number': this.filters[name] = parseFloat(val) || 0; break; default: // text this.filters[name] = val.toLowerCase().trim(); } } _resetFilters() { this.filters = { search: '', exchange: 'all', sector: 'all', priceMin: 0, priceMax: Infinity, changeMin: -Infinity, changeMax: Infinity, volumeMin: 0, volumeMax: Infinity }; // Reset UI if (this.filterContainer) { var inputs = this.filterContainer.querySelectorAll('input, select'); Array.prototype.forEach.call(inputs, function(input) { if (input.getAttribute('data-filter-reset') !== 'false') { if (input.tagName === 'SELECT') { input.selectedIndex = 0; } else if (input.type === 'range') { input.value = input.getAttribute('data-default') || input.min || ''; } else { input.value = input.getAttribute('data-default') || ''; } } }); } } _debouncedFilter() { clearTimeout(this._debounceTimer); var self = this; this._debounceTimer = setTimeout(function() { self._applyFilters(); }, DEBOUNCE_MS); } _applyFilters() { var f = this.filters; var self = this; this.filteredRows = this.allRows.filter(function(row) { // Skip hidden/empty rows if (row.style.display === 'none' && !row.dataset._screenerActive) return false; var symbol = (row.getAttribute('data-symbol') || '').toLowerCase(); var name = (row.getAttribute('data-name') || '').toLowerCase(); var exchange = row.getAttribute('data-exchange') || ''; var sector = row.getAttribute('data-sector') || ''; // Numeric values from data attributes or cell content var priceEl = row.querySelector('.td-price'); var changeEl = row.querySelector('.td-change'); var volEl = row.querySelector('.td-vol'); var price = self._parseNumeric(row.getAttribute('data-price') || (priceEl ? priceEl.textContent : '')); var change = self._parseNumeric(row.getAttribute('data-change') || (changeEl ? changeEl.textContent : '')); var volume = self._parseVolume(row.getAttribute('data-volume') || (volEl ? volEl.textContent : '')); // Search filter if (f.search && symbol.indexOf(f.search) === -1 && name.indexOf(f.search) === -1) { return false; } // Exchange filter if (f.exchange !== 'all' && exchange.toUpperCase() !== f.exchange.toUpperCase()) { return false; } // Sector filter if (f.sector !== 'all' && sector.toUpperCase() !== f.sector.toUpperCase()) { return false; } // Price range if (price !== null) { if (f.priceMin > 0 && price < f.priceMin) return false; if (f.priceMax < Infinity && price > f.priceMax) return false; } // Change % range if (change !== null) { if (f.changeMin > -Infinity && change < f.changeMin) return false; if (f.changeMax < Infinity && change > f.changeMax) return false; } // Volume range if (volume !== null) { if (f.volumeMin > 0 && volume < f.volumeMin) return false; if (f.volumeMax < Infinity && volume > f.volumeMax) return false; } return true; }); // Update display this._updateDisplay(); // Callback if (typeof this.callback === 'function') { this.callback(this.filteredRows, this.filters); } } _updateDisplay() { var self = this; var visibleCount = 0; this.allRows.forEach(function(row) { var match = self.filteredRows.indexOf(row) !== -1; if (match) { row.style.display = ''; row.dataset._screenerActive = 'true'; visibleCount++; } else { row.style.display = 'none'; row.dataset._screenerActive = 'false'; } }); // Update count display if (this.resultCount) { this.resultCount.textContent = visibleCount + ' / ' + this.allRows.length + ' stocks'; } } _parseNumeric(str) { if (!str) return null; var cleaned = str.replace(/[^0-9.\-]/g, ''); var val = parseFloat(cleaned); return isNaN(val) ? null : val; } _parseVolume(str) { if (!str) return null; var cleaned = str.replace(/[^0-9.]/g, ''); var num = parseFloat(cleaned); if (isNaN(num)) return null; // Handle abbreviations var upper = str.toUpperCase(); if (upper.indexOf('T') !== -1) return num * 1e12; if (upper.indexOf('B') !== -1) return num * 1e9; if (upper.indexOf('M') !== -1) return num * 1e6; if (upper.indexOf('K') !== -1) return num * 1e3; return num; } // Public API setFilter(name, value) { this.filters[name] = value; this._applyFilters(); } refresh() { if (this.tbody) { this.allRows = Array.prototype.slice.call(this.tbody.querySelectorAll('tr')); } this._applyFilters(); } } // ============================================================ // AUTO-INITIALIZE // ============================================================ function initScreener(id) { var container = document.getElementById(id); if (!container) return null; var table = container.querySelector('.data-table'); var filterContainer = container.querySelector('.screener-filters'); var resultCount = container.querySelector('.screener-count'); if (!table || !filterContainer) return null; return new StockScreener({ table: table, filterContainer: filterContainer, resultCount: resultCount }); } // Auto-init on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { var containers = document.querySelectorAll('[data-screener]'); Array.prototype.forEach.call(containers, function(container) { initScreener(container.id); }); }); } else { var containers = document.querySelectorAll('[data-screener]'); Array.prototype.forEach.call(containers, function(container) { initScreener(container.id); }); } // Export for manual use window.StockScreener = StockScreener; window.initScreener = initScreener; })();