Rental Market Supply & Demand Trend Analyzer
Supply Indicators
Demand Indicators
Market Trend Analysis (Illustrative)
Likely Rental Price Trend:
Likely Occupancy Rate Trend:
Summary:
Indicator Definitions
Rental Market Supply & Demand Trend Analysis
Input Parameters:
Trend Analysis:
Rental Price Trend:
Occupancy Rate Trend:
Summary:
Conceptual Trend Chart:
${definition}
`; container.appendChild(itemDiv); }); } // --- Core Logic & Display --- function getElementValue(id, isNumeric = false) { const element = document.getElementById(id); if (!element) { console.error(`Element ${id} not found`); return isNumeric ? 0 : ''; } return isNumeric ? parseFloat(element.value) : element.value; } function getElementText(id) { // For select dropdown text content const element = document.getElementById(id); if (!element || element.tagName !== 'SELECT') { return getElementValue(id); // fallback or for other types } return element.options[element.selectedIndex]?.text || element.value; } function analyzeRentalMarketTrends() { const vacancyRate = getElementValue('raVacancyRate', true); const construction = getElementValue('raConstructionPipeline'); const jobGrowth = getElementValue('raJobGrowth'); const populationChange = getElementValue('raPopulationChange'); // --- Heuristic Scoring System --- // Lower score = weaker market conditions for landlords (lower rents/occupancy) // Higher score = stronger market conditions for landlords (higher rents/occupancy) // Supply Score (Higher means more supply pressure -> weaker for landlords) let supplyScore = 0; if (vacancyRate > 7) supplyScore += 2; // High vacancy else if (vacancyRate > 4 && vacancyRate <= 7) supplyScore += 1; // Moderate vacancy // Low vacancy (<=4) means supplyScore remains low (good for landlords) if (construction === 'high') supplyScore += 2; else if (construction === 'medium') supplyScore += 1; // Demand Score (Higher means more demand pressure -> stronger for landlords) let demandScore = 0; const jobGrowthMap = {'strong_negative': -2, 'slight_negative': -1, 'neutral': 0, 'slight_positive': 1, 'strong_positive': 2}; const populationChangeMap = {'significant_decrease': -2, 'slight_decrease': -1, 'stable': 0, 'slight_increase': 1, 'significant_increase': 2}; demandScore += jobGrowthMap[jobGrowth] || 0; demandScore += populationChangeMap[populationChange] || 0; // Net Market Pressure Score (Positive = Landlord's market, Negative = Tenant's market) // We want demand to outweigh supply for positive landlord outcomes. // So, effectiveDemandScore = demandScore - supplyScore. let marketPressureScore = demandScore - supplyScore; // Determine Trends based on marketPressureScore let priceTrend, occupancyTrend, summary; let priceTrendClass, occupancyTrendClass; let priceTrendFactor = 0, occupancyTrendFactor = 0; // For chart slope if (marketPressureScore >= 3) { // Very Strong Demand vs Supply priceTrend = "Sharply Increasing"; priceTrendClass = "trend-positive"; priceTrendFactor = 0.10; occupancyTrend = "Increasing"; occupancyTrendClass = "trend-positive"; occupancyTrendFactor = 0.04; summary = "Strong demand significantly outweighs supply, leading to rapid rent growth and rising occupancy."; } else if (marketPressureScore >= 1) { // Moderately Strong Demand priceTrend = "Increasing"; priceTrendClass = "trend-positive"; priceTrendFactor = 0.05; occupancyTrend = "Slightly Increasing / Stable"; occupancyTrendClass = "trend-positive"; occupancyTrendFactor = 0.02; summary = "Demand outpaces supply, supporting rent increases and stable to improving occupancy."; } else if (marketPressureScore === 0) { // Balanced Market priceTrend = "Stable"; priceTrendClass = "trend-neutral"; priceTrendFactor = 0.01; // slight positive bias occupancyTrend = "Stable"; occupancyTrendClass = "trend-neutral"; occupancyTrendFactor = 0; summary = "The market appears balanced, with supply and demand largely in equilibrium, leading to stable rents and occupancy."; } else if (marketPressureScore >= -2) { // Moderately Weak Demand / Oversupply priceTrend = "Slightly Decreasing / Stable"; priceTrendClass = "trend-negative"; priceTrendFactor = -0.03; occupancyTrend = "Slightly Decreasing / Stable"; occupancyTrendClass = "trend-negative"; occupancyTrendFactor = -0.02; summary = "Supply slightly exceeds demand, putting some downward pressure on rents and occupancy."; } else { // Very Weak Demand / Significant Oversupply priceTrend = "Decreasing"; priceTrendClass = "trend-negative"; priceTrendFactor = -0.06; occupancyTrend = "Decreasing"; occupancyTrendClass = "trend-negative"; occupancyTrendFactor = -0.04; summary = "Significant oversupply or weak demand is likely to cause declining rents and falling occupancy rates."; } // Update UI const resultsDisplay = document.getElementById('rentalAnalyzerResultsDisplay'); const chartContainer = document.querySelector('.rental-analyzer-chart-container'); const pdfButton = document.getElementById('rentalAnalyzerPdfDownloadButton'); if(resultsDisplay && chartContainer && pdfButton) { document.getElementById('raPriceTrendResult').textContent = priceTrend; document.getElementById('raPriceTrendResult').className = priceTrendClass; document.getElementById('raOccupancyTrendResult').textContent = occupancyTrend; document.getElementById('raOccupancyTrendResult').className = occupancyTrendClass; document.getElementById('raAnalysisSummary').textContent = summary; resultsDisplay.style.display = 'block'; updateRentalMarketTrendChart(priceTrendFactor, occupancyTrendFactor); chartContainer.style.display = 'block'; pdfButton.style.display = 'block'; } else { console.error("One or more display elements are missing."); } } // --- Chart.js Integration --- function updateRentalMarketTrendChart(priceFactor, occupancyFactor) { const chartCanvas = document.getElementById('rentalMarketTrendChart'); if (!chartCanvas) return; const labels = ['Current', 'Q1', 'Q2', 'Q3', 'Q4']; // Conceptual future quarters // Generate conceptual trend data (indexed from 100) let rentData = [100]; let occupancyData = [100]; for (let i = 1; i < labels.length; i++) { rentData.push(rentData[i-1] * (1 + priceFactor)); occupancyData.push(occupancyData[i-1] * (1 + occupancyFactor)); } // Ensure values don't go too low if factors are very negative, clip at a reasonable floor e.g. 50 rentData = rentData.map(val => Math.max(50, val)); occupancyData = occupancyData.map(val => Math.max(50, val)); const data = { labels: labels, datasets: [ { label: 'Rental Price Index Trend', data: rentData, borderColor: 'rgb(75, 192, 192)', backgroundColor: 'rgba(75, 192, 192, 0.2)', tension: 0.1, fill: false, yAxisID: 'yRent' }, { label: 'Occupancy Rate Index Trend', data: occupancyData, borderColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.2)', tension: 0.1, fill: false, yAxisID: 'yOccupancy' } ] }; if (rentalMarketTrendChartInstance) { rentalMarketTrendChartInstance.data = data; rentalMarketTrendChartInstance.update(); } else { rentalMarketTrendChartInstance = new Chart(chartCanvas, { type: 'line', data: data, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, plugins: { title: { display: true, text: 'Conceptual Rental Market Trends (Indexed)' }, tooltip: { mode: 'index', intersect: false, callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += context.parsed.y.toFixed(2); } return label; } } } }, scales: { x: { title: { display: true, text: 'Time Period (Conceptual)' } }, yRent: { type: 'linear', display: true, position: 'left', title: { display: true, text: 'Rental Price Index' }, //suggestedMin: 80, //suggestedMax: 120 }, yOccupancy: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'Occupancy Index' }, //suggestedMin: 80, //suggestedMax: 120, grid: { drawOnChartArea: false } // only want the grid for the left axis } } } }); } } // --- PDF Download --- async function downloadRentalAnalyzerPdf() { if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined' || typeof html2canvas === 'undefined' || !rentalMarketTrendChartInstance) { alert('Error: Required libraries (jsPDF, html2canvas, Chart.js) not loaded or chart not ready. PDF cannot be generated.'); console.error('PDF generation prerequisites missing.'); return; } const { jsPDF } = window.jspdf; const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' }); // Populate hidden PDF content container const pdfReportContainer = document.getElementById('rentalAnalyzerPdfReportContentContainer'); if (!pdfReportContainer) { alert("PDF report content container not found."); return; } // Populate inputs table for PDF const inputsTableBody = document.createElement('tbody'); const inputs = [ { label: "Current Vacancy Rate (%)", value: getElementValue('raVacancyRate') }, { label: "New Construction Pipeline", value: getElementText('raConstructionPipeline') }, { label: "Job Growth Outlook", value: getElementText('raJobGrowth') }, { label: "Population Change Outlook", value: getElementText('raPopulationChange') } ]; inputs.forEach(input => { const row = inputsTableBody.insertRow(); row.insertCell().textContent = input.label; row.insertCell().textContent = input.value; }); const pdfInputTable = document.getElementById('pdfInputParametersTable'); if (pdfInputTable) { pdfInputTable.innerHTML = ''; // Clear previous const header = pdfInputTable.createTHead().insertRow(); header.insertCell().textContent = "Indicator"; header.insertCell().textContent = "Selected Value"; pdfInputTable.appendChild(inputsTableBody); } document.getElementById('pdfPriceTrend').textContent = document.getElementById('raPriceTrendResult').textContent; document.getElementById('pdfPriceTrend').className = document.getElementById('raPriceTrendResult').className; document.getElementById('pdfOccupancyTrend').textContent = document.getElementById('raOccupancyTrendResult').textContent; document.getElementById('pdfOccupancyTrend').className = document.getElementById('raOccupancyTrendResult').className; document.getElementById('pdfAnalysisSummary').textContent = document.getElementById('raAnalysisSummary').textContent; // Get chart as image const chartCanvas = document.getElementById('rentalMarketTrendChart'); let chartImageURI = ''; if (chartCanvas && rentalMarketTrendChartInstance) { try { chartImageURI = chartCanvas.toDataURL('image/png', 1.0); // Ensure good quality document.getElementById('pdfTrendChartImage').src = chartImageURI; } catch (e) { console.error("Error converting chart to image:", e); document.getElementById('pdfTrendChartImage').alt = "Chart image could not be generated."; } } else { document.getElementById('pdfTrendChartImage').alt = "Chart not available."; } pdfReportContainer.style.display = 'block'; // Make visible for html2canvas try { const canvas = await html2canvas(pdfReportContainer, { scale: 2, useCORS: true, backgroundColor: '#ffffff', logging: false }); const imgData = canvas.toDataURL('image/png'); const imgProps = pdf.getImageProperties(imgData); const pdfWidth = pdf.internal.pageSize.getWidth(); const pageHeight = pdf.internal.pageSize.getHeight(); const margin = 40; // pt const contentWidth = pdfWidth - (2 * margin); const imgHeight = (imgProps.height * contentWidth) / imgProps.width; let heightLeft = imgHeight; let position = margin; pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight); heightLeft -= (pageHeight - (2 * margin)); while (heightLeft > 0) { position = margin - imgHeight + heightLeft; pdf.addPage(); pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight); heightLeft -= (pageHeight - (2 * margin)); } pdf.save('Rental_Market_Trend_Analysis.pdf'); } catch (error) { console.error("Error generating PDF with html2canvas:", error); alert("Failed to generate PDF. Check console for details."); } finally { pdfReportContainer.style.display = 'none'; // Hide it again } }