Real Estate Market Trend Analyzer
Manually input time-series data for a specific market metric to visualize and analyze simple trends.
Analysis Scope
Add Data Point
Data & Trends for Not Set
| Period | Value | Change from Prev. (%) | 3-Period SMA | Actions |
|---|
No data points added yet.
Basic Trend Visualization
Add data to see visualization.
Overall Change (First to Last): ${overallPctChangeText}
`; } renderChart(); saveData(); } function renderChart() { chartContainer.innerHTML = ''; // Clear previous chart noChartDataMessage.style.display = timeSeriesData.length === 0 ? 'block' : 'none'; if (timeSeriesData.length === 0) return; const values = timeSeriesData.map(item => parseFloat(item.value)).filter(v => !isNaN(v)); if (values.length === 0) { noChartDataMessage.style.display = 'block'; noChartDataMessage.textContent = 'No numeric data to chart.'; return; } const minValue = Math.min(...values); const maxValue = Math.max(...values); const range = maxValue - minValue; const chartHeight = 180; // Max height of a bar in px timeSeriesData.forEach(item => { const value = parseFloat(item.value); if (isNaN(value)) return; const wrapper = document.createElement('div'); wrapper.className = 'remta-chart-bar-wrapper'; const bar = document.createElement('div'); bar.className = 'remta-chart-bar'; let barHeightPercent = 0; if (range === 0) { // All values are the same barHeightPercent = (value !== 0) ? 50 : 5; // Show a mid-size bar or small bar for zero } else { barHeightPercent = ((value - minValue) / range) * 100; if (barHeightPercent < 5 && value !== 0) barHeightPercent = 5; // Min height for non-zero values else if (value === 0 && minValue < 0 && maxValue > 0) { // Handle zero value in a range spanning zero // This bar should be at the zero line. // For simplicity, this bar chart represents magnitude from bottom. // A more complex chart would be needed for values below a zero axis. // For now, 0 value will have small height if range is not 0. barHeightPercent = 5; } } bar.style.height = `${(chartHeight * barHeightPercent) / 100}px`; const valueSpan = document.createElement('span'); valueSpan.className = 'bar-value'; valueSpan.textContent = formatValue(value); bar.appendChild(valueSpan); const periodLabel = document.createElement('div'); periodLabel.className = 'remta-chart-period-label'; periodLabel.textContent = escapeHtml(item.period); wrapper.appendChild(bar); wrapper.appendChild(periodLabel); chartContainer.appendChild(wrapper); }); } addDataButton.addEventListener('click', () => { const period = periodInput.value.trim(); const valueStr = valueInput.value.trim(); if (!period || valueStr === '') { inputErrorDisplay.textContent = "Error: Both Period/Date and Value are required."; return; } const valueNum = parseFloat(valueStr); if (isNaN(valueNum)) { inputErrorDisplay.textContent = "Error: Value must be a valid number."; return; } timeSeriesData.push({ id: Date.now() + Math.random(), period, value: valueNum.toString() // Store as string, parse on use }); // Optionally sort by period if periods have a natural order, // for now, assumes order of entry is desired for simple trends. calculateTrendsAndRender(); periodInput.value = ''; valueInput.value = ''; inputErrorDisplay.textContent = ''; }); dataTableBody.addEventListener('click', function(event) { if (event.target.classList.contains('remta-remove-btn')) { const idToRemove = parseFloat(event.target.dataset.id); timeSeriesData = timeSeriesData.filter(item => item.id !== idToRemove); calculateTrendsAndRender(); } }); clearAllButton.addEventListener('click', () => { if (timeSeriesData.length > 0 && confirm("Are you sure you want to clear all data points?")) { timeSeriesData = []; calculateTrendsAndRender(); } else if (timeSeriesData.length === 0) { inputErrorDisplay.textContent = "No data to clear."; } }); function saveData() { try { const dataToSave = { marketLocation: marketLocationInput.value.trim(), metricName: metricNameInput.value.trim(), series: timeSeriesData }; localStorage.setItem('remta_trendData_v2', JSON.stringify(dataToSave)); } catch (e) { console.warn("REMTA: Could not save data to localStorage.", e); inputErrorDisplay.textContent = "Warning: Could not save data locally (localStorage might be full or disabled)."; } } function loadData() { try { const savedDataStr = localStorage.getItem('remta_trendData_v2'); if (savedDataStr) { const savedData = JSON.parse(savedDataStr); marketLocationInput.value = savedData.marketLocation || ''; metricNameInput.value = savedData.metricName || ''; timeSeriesData = savedData.series || []; } } catch (e) { console.warn("REMTA: Could not load data from localStorage.", e); timeSeriesData = []; localStorage.removeItem('remta_trendData_v2'); } } // Update display scope label on input change marketLocationInput.addEventListener('input', () => displayScopeLabel.textContent = escapeHtml(marketLocationInput.value.trim() || 'Undefined Market') + ' - ' + escapeHtml(metricNameInput.value.trim() || 'Undefined Metric')); metricNameInput.addEventListener('input', () => displayScopeLabel.textContent = escapeHtml(marketLocationInput.value.trim() || 'Undefined Market') + ' - ' + escapeHtml(metricNameInput.value.trim() || 'Undefined Metric')); if (pdfButton) { pdfButton.addEventListener('click', function() { if (timeSeriesData.length === 0 && !inputErrorDisplay.textContent) { alert("No data to download for PDF."); return; } window.print(); }); } // Initial Load const criticalElements = [marketLocationInput, metricNameInput, periodInput, valueInput, addDataButton, dataTableBody, clearAllButton, pdfButton, chartContainer, noChartDataMessage, inputErrorDisplay, noDataTableMessage, displayScopeLabel, overallTrendsDiv]; if (criticalElements.some(el => !el)) { console.error("RealEstateTrendAnalyzer: Critical HTML elements missing."); const appDiv = document.getElementById('realEstateTrendAnalyzerApp'); if(appDiv) appDiv.innerHTML = "Error: Tool failed to initialize. Required elements missing.
"; } else { loadData(); calculateTrendsAndRender(); } });