Technical Documentation & Learning Guide
Built with ❤️ by Nick + Supernova
IT-consulting-template/
├── ri-report.html # Main dashboard (5 tabs)
├── ri-results.html # City drill-down page
├── ri-sales.json.gz # Compressed property data (2.2MB)
├── ri-docs.html # This documentation
└── assets/
└── bridge.jpg # Hero image
Instead of sequential loading (data → then → SQL.js), we load both simultaneously:
// BEFORE: Sequential (slow)
const data = await fetch('ri-sales.json.gz');
const SQL = await initSqlJs(); // Waits for data first
// AFTER: Parallel (fast)
const [dataResponse, SQL] = await Promise.all([
fetch('ri-sales.json.gz?v=' + today),
initSqlJs({ locateFile: f => cdn + f })
]);
// Decompress with pako
const compressed = await dataResponse.arrayBuffer();
const decompressed = pako.inflate(compressed, { to: 'string' });
const rawData = JSON.parse(decompressed);
Inserting 36K rows one-by-one is slow. We batch 500 at a time:
// Insert in batches of 500
const BATCH = 500;
for (let i = 0; i < properties.length; i += BATCH) {
const batch = properties.slice(i, i + BATCH);
const values = batch.map(p =>
`('${escape(p.address)}', '${p.city}', ${p.price}, ...)`
).join(',');
db.run(`INSERT INTO props VALUES ${values}`);
}
// Result: ~70x faster than row-by-row
We append today's date to force fresh data daily while allowing same-day caching:
const today = new Date().toISOString().split('T')[0]; // "2025-07-18"
fetch(`ri-sales.json.gz?v=${today}`);
Instead of relying on pre-aggregated summary data, we compute city statistics on-the-fly from raw properties. This is more flexible and self-healing:
// Compute city stats from raw properties
const cityStats = {};
rawData.properties.forEach(p => {
if (!cityStats[p.city]) {
cityStats[p.city] = { count: 0, prices: [], landCount: 0 };
}
cityStats[p.city].count++;
if (p.price > 0) cityStats[p.city].prices.push(p.price);
if (p.propertyType?.includes('Land')) cityStats[p.city].landCount++;
});
// Transform to sorted array with median prices
const cityData = Object.entries(cityStats)
.map(([city, s]) => ({
city,
count: s.count,
medianPrice: s.prices.sort((a,b) => a-b)[Math.floor(s.prices.length/2)] || 0,
landCount: s.landCount
}))
.sort((a,b) => b.count - a.count)
.slice(0, 15);
// Store globally for charts
window.computedCityStats = cityStats;
summary.cities to be objects with stats, but our data cleanup simplified it to just city names. Computing on-the-fly means charts always work regardless of summary format.
A service worker is a JavaScript file that runs in the background, separate from your web page. It can intercept network requests, cache resources, and enable offline functionality.
const CACHE_NAME = 'ri-real-estate-v1';
const PRECACHE_URLS = [
'/',
'/ri-report.html',
'/ri-results.html',
'/ri-sales.json.gz',
'https://cdn.jsdelivr.net/npm/chart.js',
'https://cdn.jsdelivr.net/npm/[email protected]/dist/pako.min.js'
];
// Install: cache essential files
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log('Precaching app shell');
return cache.addAll(PRECACHE_URLS);
})
);
});
// Activate: clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(k => k !== CACHE_NAME)
.map(k => caches.delete(k))
))
);
});
// Fetch: cache-first for static assets, network-first for data
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Network-first for data (always try fresh)
if (url.pathname.includes('.json')) {
event.respondWith(
fetch(event.request)
.then(response => {
const clone = response.clone();
caches.open(CACHE_NAME).then(c => c.put(event.request, clone));
return response;
})
.catch(() => caches.match(event.request))
);
return;
}
// Cache-first for static assets
event.respondWith(
caches.match(event.request)
.then(cached => cached || fetch(event.request))
);
});
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.error('SW failed:', err));
}
</script>
Our pagination supports dynamic page sizes with a dropdown selector:
let pageSize = 25; // Default
let currentPage = 0;
function renderPage() {
const effectiveSize = pageSize === 'all' ? filtered.length : pageSize;
const start = currentPage * effectiveSize;
const end = Math.min(start + effectiveSize, filtered.length);
const pageData = filtered.slice(start, end);
// Render rows...
// Update all pagination controls
document.querySelectorAll('.page-info').forEach(el => {
el.textContent = pageSize === 'all'
? `Showing all ${filtered.length} results`
: `Page ${currentPage + 1} of ${totalPages}`;
});
}
function changePageSize(val) {
pageSize = val === 'all' ? 'all' : parseInt(val);
currentPage = 0; // Reset to first page
renderPage();
}
We use Chart.js for all visualizations with a consistent dark theme:
const chartOpts = {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: { labels: { color: '#94a3b8' } }
},
scales: {
x: {
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#94a3b8' }
},
y: {
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#94a3b8' }
}
}
};
// Example: Horizontal bar chart
new Chart(document.getElementById('cityChart'), {
type: 'bar',
data: {
labels: cityData.map(c => c.city),
datasets: [{
data: cityData.map(c => c.count),
backgroundColor: cityData.map((_, i) =>
`hsl(${250 + i * 7}, 70%, 60%)` // Gradient colors
),
borderRadius: 6
}]
},
options: {
indexAxis: 'y', // Makes it horizontal
...chartOpts,
plugins: { legend: { display: false } }
}
});
We calculate distances between properties using the Haversine formula (accounts for Earth's curvature):
function getDistance(lat1, lng1, lat2, lng2) {
const R = 3959; // Earth radius in miles
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) *
Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLng/2) * Math.sin(dLng/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c; // Distance in miles
}
function getNearbyProperties(lat, lng, radiusMiles) {
return rawData.properties
.filter(p => p.lat && p.lng)
.map(p => ({
...p,
distance: getDistance(lat, lng, p.lat, p.lng)
}))
.filter(p => p.distance <= radiusMiles)
.sort((a, b) => a.distance - b.distance);
}
When a property lacks lat/lng, we fall back to city-wide comps:
function showNeighborhood(idx) {
const property = window.dealsData[idx];
if (!property.lat || !property.lng) {
// Fallback: show city-wide data instead of error
showCityComps(property.city,
property.address + ' (No coordinates - showing city-wide data)');
return;
}
// Normal neighborhood view...
}
Only load visible table rows, fetch more on scroll (IntersectionObserver).
Store property data in browser's IndexedDB for instant offline access.
Add Leaflet.js map with property markers and clustering.
Scheduled scraper to refresh data daily via GitHub Actions.
// main.js
const worker = new Worker('compute-worker.js');
worker.postMessage({ properties: rawData.properties });
worker.onmessage = (e) => {
const { cityStats, monthlyTrends } = e.data;
initCharts(cityStats, monthlyTrends);
};
// compute-worker.js
self.onmessage = (e) => {
const { properties } = e.data;
// Heavy computation off main thread...
const cityStats = computeCityStats(properties);
self.postMessage({ cityStats });
};
Last updated: July 2025 • Back to Report • AIBridges.org