1// Define the endpoint with multiple stats 2const ninetyDaysAgo = new Date(); 3ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90); 4ninetyDaysAgo.setHours(0, 0, 0, 0); // Normalize to midnight 5const excludedLabels = ['10-year Yield', 'Mortgage Rates']; 6let charts = {}; 7let dataWindowStart; 8let yieldLabels = []; 9 10//console.log("Confirm that initialize charts updated 10:33"); 11 12// Listeners 13window.addEventListener('orchestratorPrefsDataProcessed', (event) => { 14 console.log('Orchestrator prefs data processed. Firing initCharts().'); 15 16 if (event.detail.locationId) { 17 altosUrl = 18 'https://housingwire-api.housingwire.com/rates/altos/multi-stat?altosLocationId=' + 19 event.detail.locationId; 20 } else { 21 altosUrl = hwCharts.nationalAltosUrl; 22 } 23 24 //console.log('start' + dataWindowStart); 25 hwCharts.initCharts(altosUrl); 26 hwCharts.updateLocationName(event.detail.locationName); 27}); 28 29// Mapping for the stat labels 30const statLabelMap = { 31 price_median: 'Median List Price', 32 count: 'Inventory', 33 dom_mean: 'Avg Days on Market', 34 price_decreased_percent: 'Price Reductions', 35 rental_price_median: 'Median Rent', 36 '30 YEAR CONFORMING': 'Mortgage Rates', 37 'Ten Year Yield': '10-year Yield', 38 price_increased_percent: 'Price Percentage Increased', 39 dom_median: 'Median Days on Market', 40 relisted_percent: 'Percentage of Listings Relisted', 41 rental_price_median: 'Median Rent', 42 mai: 'Market Action Index', 43 per_sqft_mean: 'Price per Square Foot', 44}; 45 46const chartClassNames = [ 47 'chart-flex-container flex flex-col h-full', 48 'chart-wrapper grow', 49 'label-data-container text-center pt-1 flex flex-row gap-2 justify-center', 50 'chart-label text-13 flex items-start leading-tight tw-hidden', 51 'chart-data font-bold text-16 lg:text-23', 52 'text-15 sm:text-16 text-center md:mb-2 leading-tight', 53 'flex items-center gap-1 text-13 lg:text-16', 54]; 55 56const chartThemeConfig = { 57 tooltipBackgroundColor: '#ffffff', 58 tooltipTextColor: '#000000', 59 borderColor: '#ffffff', 60 61 //fillColor: 'rgba(255,255,255,.1)', 62 fillColor: '#313638', 63 gridLineColor: 'rgba(255,255,255,.2)', 64}; 65 66// Function to create a chart for each stat 67function createChart( 68 statLabel, 69 labels, 70 data, 71 classNames, 72 containerID, 73 themeConfig, 74 statLabel2, 75 data2, 76) { 77 // Dynamically create a new subarray of charts 78 if (!charts[containerID]) { 79 charts[containerID] = []; 80 } 81 82 const [ 83 chartContainerClass, 84 chartWrapperClass, 85 labelDataDivClass, 86 labelDivClass, 87 dataDivClass, 88 topLabelDivClass, 89 differenceDivClass, 90 ] = classNames; 91 92 const chartContainer = document.createElement('div'); 93 chartContainer.className = chartContainerClass; 94 95 const chartWrapper = document.createElement('div'); 96 chartWrapper.className = chartWrapperClass; 97 98 let tickFontSize; 99 let tickPosition; 100 let tickPadding; 101 let tooltipDateSize; 102 let tooltipBodySize; 103 let tickColor = 'rgba(115, 123, 140, .75)'; 104 105 //chartContainer.appendChild( topLabelDiv ); 106 107 //chartContainer.appendChild( labelDataDiv ); 108 tickFontSize = 10; 109 tickPosition = 'left'; 110 tickPadding = 0; 111 tooltipDateSize = 12; 112 tooltipBodySize = 15; 113 114 chartContainer.appendChild(chartWrapper); 115 116 const canvas = document.createElement('canvas'); 117 canvas.id = `${statLabel.replace(/\s+/g, '_')}_original`; 118 119 chartWrapper.appendChild(canvas); 120 document.getElementById(containerID).appendChild(chartContainer); 121 122 canvas.width = chartWrapper.clientWidth; 123 canvas.height = chartWrapper.clientHeight; 124 125 const ctx = canvas.getContext('2d'); 126 127 const darkerColor = 'rgba(115, 123, 140, .75)'; 128 129 const scalesConfig = { 130 x: { 131 display: 'auto', 132 type: 'category', 133 border: { 134 display: true, 135 dash: [3, 3], 136 dashOffset: 0, 137 color: darkerColor, 138 width: 1, 139 }, 140 grid: { 141 color: 'hsl(220 10% 90%)', 142 drawOnChartArea: true, 143 tickColor: darkerColor, 144 }, 145 ticks: { 146 maxTicksLimit: 6, 147 font: { size: tickFontSize }, 148 callback: function (value) { 149 // Use the scale helper to get the original label (handles numeric index) 150 let label = this.getLabelForValue(value); 151 if (!label || 'string' !== typeof label) { 152 return label; 153 } 154 const parts = label.split('-').map(Number); 155 if (3 !== parts.length || parts.some(isNaN)) { 156 return label; 157 } 158 const [year, month, day] = parts; 159 const date = new Date(year, month - 1, day);
160 return date.toLocaleString('en-US', { 161 month: 'short', 162 day: 'numeric', 163 timeZone: 'UTC', 164 }); 165 }, 166 }, 167 }, 168 y: { 169 display: 'auto', 170 position: tickPosition, 171 border: { 172 display: true, 173 dash: [3, 3], 174 dashOffset: 0, 175 color: darkerColor, 176 width: 1, 177 }, 178 title: { 179 display: true, 180 text: statLabel, 181 color: darkerColor, 182 font: { size: tickFontSize }, 183 }, 184 ticks: { 185 callback: function (value) { 186 return hwCharts.returnFormattedValue( 187 value, 188 value, 189 statLabel, 190 'y-axis', 191 containerID, 192 ); 193 }, 194 padding: tickPadding, 195 maxTicksLimit: 5, 196 align: 'left', 197 font: { size: tickFontSize }, 198 }, 199 grid: { 200 color: 'hsl(220 10% 90%)', 201 drawOnChartArea: true, 202 tickColor: darkerColor, 203 }, 204 }, 205 }; 206 207 const baseYAxis = { 208 ...scalesConfig.y, 209 ticks: { ...(scalesConfig.y.ticks || {}) }, 210 title: { ...(scalesConfig.y.title || {}) }, 211 grid: { ...(scalesConfig.y.grid || {}) }, 212 border: { ...(scalesConfig.y.border || {}) }, 213 }; 214 215 let datasets = [ 216 { 217 label: statLabel, 218 data: data, 219 borderColor: 'rgba(237, 172, 184, 1)', 220 backgroundColor: 'rgba(237, 172, 184, .1)', 221 borderWidth: 2, 222 fill: true, 223 }, 224 ]; 225 226 if (statLabel2 && data2 && 0 < data2.length) { 227 datasets.push({ 228 label: statLabel2, 229 data: data2, 230 borderColor: 'rgba(158, 196, 248, 1)', 231 backgroundColor: 'rgba(158, 196, 248, .1)', 232 borderWidth: 2, 233 fill: true, 234 order: 1, 235 pointRadius: 0, 236 yAxisID: 'y2', 237 }); 238 239 scalesConfig.y2 = { 240 ...baseYAxis, 241 display: 'auto', 242 position: 'right', 243 grid: { 244 ...baseYAxis.grid, 245 display: false, 246 drawTicks: true, 247 tickColor: darkerColor, 248 tickLength: 6, 249 }, 250 title: { 251 ...baseYAxis.title, 252 display: true, 253 text: 'Inventory', 254 color: darkerColor, 255 font: { size: tickFontSize }, 256 }, 257 border: { ...baseYAxis.border, color: darkerColor }, 258 ticks: { 259 ...baseYAxis.ticks, 260 align: 'right', 261 color: darkerColor, 262 callback: function (value) { 263 return hwCharts.returnFormattedValue( 264 value, 265 value, 266 statLabel2, 267 'y-axis', 268 containerID, 269 ); 270 }, 271 }, 272 }; 273 } 274 275 const chartConfig = { 276 type: 'line', 277 data: { 278 labels: labels, 279 datasets: datasets, 280 }, 281 options: { 282 responsive: true, 283 maintainAspectRatio: false, 284 spanGaps: true, 285 scales: scalesConfig, 286 plugins: { 287 legend: { display: false }, 288 tooltip: { 289 enabled: true, 290 backgroundColor: themeConfig.tooltipBackgroundColor, 291 titleColor: '#7C7C7B', 292 bodyColor: themeConfig.tooltipTextColor, 293 borderColor: '#908E8D', 294 borderWidth: 0, 295 titleMarginBottom: 4, 296 titleFont: { 297 weight: 'normal', 298 size: tooltipDateSize, 299 }, 300 bodyFont: { size: tooltipBodySize }, 301 cornerRadius: 0, 302 displayColors: false, 303 callbacks: { 304 label: function (tooltipItem) { 305 return hwCharts.returnFormattedValue( 306 tooltipItem.raw, 307 tooltipItem.formattedValue, 308 statLabel, 309 'tooltip', 310 containerID, 311 ); 312 }, 313 title: function (tooltipItem) { 314 let formattedDate; 315 316 if ('Mortgage Rates' != statLabel) { 317 const originalDate = tooltipItem[0].label; 318 formattedDate = hwCharts.formatDate(originalDate); 319 } else { 320 const originalDate = tooltipItem[0].label; 321 322 //console.log('originalDate: ' + originalDate); 323 const date = new Date(originalDate); 324 const options = { 325 year: 'numeric', 326 month: 'long', 327 day: 'numeric', 328 }; 329 formattedDate = date.toLocaleDateString('en-US', options); 330 } 331 return formattedDate; 332 }, 333 }, 334 }, 335 }, 336 elements: { 337 line: { tension: 0.4 }, 338 point: { 339 pointStyle: false, 340 radius: 10, 341 }, 342 }, 343 }, 344 }; 345 346 const myChart = new Chart(ctx, chartConfig); 347 348 // Push myChart into the correct subarray 349 charts[containerID].push(myChart); 350} 351 352hwCharts.initCharts = async function (altosUrl) { 353 // Clear container 354 const containerID = 'chartsContainer'; 355 const chartsContainer = document.getElementById(containerID); 356 357 if (chartsContainer) { 358 chartsContainer.innerHTML = ''; 359 } 360 361 const chartsToShow = ['price_median', 'count']; 362 dataWindowStart = hwCharts.setWindowStart(90); 363 364 try { 365 const response1 = await fetch(altosUrl); 366 const data1 = await response1.json(); 367 368 hwCharts.updateStatsFromData(data1); 369 370 let anyDatasetEmpty = false; // Track if any dataset is empty 371 372 // Process charts 373 for (let i = 0; i < chartsToShow.length; i += 2) { 374 const statKey1 = chartsToShow[i]; 375 const statKey2 = chartsToShow[i + 1]; 376 const chartsToIgnore = ['10-year Yield', 'Mortgage Rates']; 377 378 const label1 = statLabelMap[statKey1]; 379 if (chartsToIgnore.includes(label1)) { 380 continue; 381 } 382 383 const stat1 = data1.stats.find((s) => s.stat === statKey1) || null; 384 const stat2 = statKey2 385 ? data1.stats.find((s) => s.stat === statKey2) || null 386 : null; 387 388 if (!stat1 && !stat2) { 389 anyDatasetEmpty = true; 390 continue; 391 } 392 393 const proc = (stat) => { 394 if (!stat || !stat.data) { 395 return { labels: [], data: [] }; 396 } 397 const entries = Object.entries(stat.data) 398 .filter(([date]) => new Date(date) >= ninetyDaysAgo) 399 .sort((a, b) => new Date(a[0]) - new Date(b[0])); 400 return { 401 labels: entries.map((e) => e[0]), 402 data: entries.map((e) => e[1]), 403 }; 404 }; 405 406 const ds1 = proc(stat1); 407 const ds2 = proc(stat2); 408 409 if (0 === ds1.data.length && 0 === ds2.data.length) { 410 anyDatasetEmpty = true; 411 continue; 412 } 413 414 // If primary is empty but secondary exists, promote secondary to primary 415 const primaryLabels = ds1.data.length ? ds1.labels : ds2.labels; 416 const primaryData = ds1.data.length ? ds1.data : ds2.data; 417 const secondaryLabels = ds1.data.length ? ds2.labels : [];
418 const secondaryData = ds1.data.length ? ds2.data : []; 419 420 const primaryLabelText = 421 statLabelMap[statKey1] || (stat1 ? stat1.stat : statKey1); 422 if (chartsContainer) { 423 createChart( 424 primaryLabelText, 425 primaryLabels, 426 primaryData, 427 chartClassNames, 428 containerID, 429 chartThemeConfig, 430 secondaryLabels, 431 secondaryData, 432 ); 433 } 434 } 435 436 // Execute function with 'empty' status 437 hwCharts.handleEmptyDataset(anyDatasetEmpty); 438 } catch (error) { 439 console.error('Failed to fetch altosUrl data:', error); 440 } 441}; 442 443window.addEventListener('resize', () => { 444 Object.values(charts).forEach((subArray) => { 445 subArray.forEach((chart) => { 446 chart.resize(); // Resize each chart 447 }); 448 }); 449});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.