1/* global ET */ 2/** 3 * Pollsmax composite forecast. Senate/Governor use standardized fixed weights: 4 * Senate (8+ polls): polling 45%, generic 2.5%, Trump 2.5%, incumbency 7.5%, 5 * incumbent's last Senate win 20%, pres avg (2016/20/24) 7.5%, Cook/Pollsmax PVI 7.5%, rating 7.5%. 6 * Governor (8+ polls): polling 45%, generic 2.5%, Trump 2.5%, incumbency 12.5%, 7 * incumbent's last governor win 22.5%, pres avg 2.5%, Cook/Pollsmax PVI 2.5%, rating 10%. 8 * Poll weight drops 5 points for each poll below 8, with the remaining factors rescaled proportionally. 9 * Simulation certainty is fixed across Senate and Governor regardless of poll count. 10 * House @ 6+ polls: polling 30%; Cook 40%; PVI 10%; prior House 10%; incumbency 5%; generic/Trump 2.5% each. 11 * House forecast math uses the same composite + simulation pipeline as the other forecast pages. 12 */ 13(function () { 14 if (!window.ET) window.ET = {}; 15 16 var POLL_STEP = 0.05; 17 var POLL_FULL_COUNT = 8; 18 19 /** Senate @ 8+ polls (non-poll sum = 55%). */ 20 var SENATE_POLL_BASE_AT_8 = 0.45; 21 var SENATE_BASE_NON_POLL = { 22 generic: 0.025, 23 trump: 0.025, 24 incumbency: 0.075, 25 priorSenate: 0.20, 26 presAvg: 0.075, 27 pvi: 0.075, 28 cook: 0.075 29 }; 30 31 /** Governor @ 8+ polls (non-poll sum = 55%). */ 32 var GOV_POLL_BASE_AT_8 = 0.45; 33 var GOV_BASE_NON_POLL = { 34 generic: 0.025, 35 trump: 0.025, 36 incumbency: 0.125, 37 priorGov: 0.225, 38 presAvg: 0.025, 39 pvi: 0.025, 40 cook: 0.10 41 }; 42 43 /** House @ 6+ polls (non-poll sum = 70%). */ 44 var HOUSE_POLL_FULL_COUNT = 6; 45 var HOUSE_POLL_BASE_AT_6 = 0.30; 46 var HOUSE_BASE_NON_POLL = { 47 generic: 0.025, 48 trump: 0.025, 49 incumbency: 0.05, 50 pvi: 0.10, 51 priorHouse: 0.10, 52 cook: 0.40 53 }; 54 /** Bump when HOUSE_BASE_NON_POLL or GOP boost changes (invalidates cached House sim snapshots). */ 55 ET.HOUSE_COOK_BLEND_REVISION = 22; 56 57 /** House-only: shift D-margin down (favors R). Applied after blend; tossups get extra. Disabled (0). */ 58 ET.HOUSE_FORECAST_GOP_MARGIN_BOOST = 0; 59 ET.HOUSE_FORECAST_GOP_TOSSUP_EXTRA_BOOST = 0; 60 61 /** Incumbent score on D-margin scale (Ã weight = contribution). */ 62 var INCUMBENCY_SCORE = 5; 63 64 /** 65 * Win odds steepen slightly away from 50%: signed |margin|^gamma, then logistic. 66 * gamma > 1 => modest acceleration (keep close to legacy linear @ 4.5). 67 */ 68 var WIN_PROB_MARGIN_GAMMA = 1.1; 69 var WIN_PROB_LOGISTIC_SCALE = 4.15; 70 71 function forecastModelEffectiveMargin(margin) { 72 if (margin == null || !isFinite(margin) || margin === 0) return 0; 73 var sign = margin > 0 ? 1 : -1; 74 return sign * Math.pow(Math.abs(margin), WIN_PROB_MARGIN_GAMMA); 75 } 76 77 /** 78 * Input/editorial rating â signed D margin for forecast blend (cook component only). 79 * Moderate Solid / Super Solid â not display buckets or map colors. 80 */ 81 var INPUT_RATING_TO_MARGIN = { 82 megaSolidD: 75, 83 ultraSolidD: 50, 84 supersolidD: 30, 85 solidD: 18, 86 likelyD: 8.5, 87 leanD: 4, 88 tiltD: 1.25, 89 tossup: 0, 90 tiltR: -1.25, 91 leanR: -4, 92 likelyR: -8.5, 93 solidR: -18, 94 supersolidR: -30, 95 ultraSolidR: -50, 96 megaSolidR: -75 97 }; 98 99 var RATING_MARGIN = INPUT_RATING_TO_MARGIN; 100 101 ET.INPUT_RATING_TO_MARGIN = INPUT_RATING_TO_MARGIN; 102 ET.RATING_MARGIN = RATING_MARGIN; 103 104 ET.forecastModelRatingToMargin = function (rating) { 105 if (!rating) return 0; 106 if (RATING_MARGIN.hasOwnProperty(rating)) return RATING_MARGIN[rating]; 107 return 0; 108 }; 109 110 /** House-only cook-component tier margins (Senate/Gov use INPUT_RATING_TO_MARGIN). */ 111 var HOUSE_RATING_TO_MARGIN = Object.assign({}, INPUT_RATING_TO_MARGIN, { 112 likelyD: 15, 113 likelyR: -15, 114 solidD: 25, 115 solidR: -25, 116 supersolidD: 35, 117 supersolidR: -35 118 }); 119 120 ET.HOUSE_INPUT_RATING_TO_MARGIN = HOUSE_RATING_TO_MARGIN; 121 122 ET.forecastModelHouseRatingToMargin = function (rating) { 123 if (!rating) return 0; 124 if (HOUSE_RATING_TO_MARGIN.hasOwnProperty(rating)) return HOUSE_RATING_TO_MARGIN[rating]; 125 return 0; 126 }; 127 128 /** D or R if that party won the state by 20+ (signed D margin) in 2016, 2020, and 2024. */ 129 ET.forecastPresSuperParty = function (stateKey) { 130 if (!stateKey || !ET.FORECAST_PRES_SUPER) return null; 131 var p = ET.FORECAST_PRES_SUPER[stateKey]; 132 return p === 'D' || p === 'R' ? p : null; 133 }; 134 135 function houseStateKeyFromCode(code) { 136 if (!code) return null; 137 var dash = code.indexOf('-'); 138 return dash > 0 ? code.slice(0, dash) : null; 139 } 140 141 /** States where 2024 district House result is excluded from the blend (redrawn / unreliable lines). */ 142 var HOUSE_PRIOR_RESULT_EXEMPT_STATES = { 143 AL: true, 144 CA: true, 145 FL: true, 146 LA: true, 147 NC: true, 148 OH: true, 149 TN: true, 150 TX: true, 151 UT: true 152 }; 153 154 /** Signed 2024 House result in D-margin points, keyed like AL-1 / AK-AL. */ 155 var HOUSE_PREVIOUS_RESULT_BY_CODE = { 156 'AK-AL': -8, 157 'AL-1': -28, 158 'AL-2': 4, 159 'AL-3': -23, 160 'AL-4': -33, 161 'AL-5': -17, 162 'AL-6': -22, 163 'AL-7': 12, 164 'AR-1': -22, 165 'AR-2': -9, 166 'AR-3': -15, 167 'AR-4': -20, 168 'AZ-1': -2, 169 'AZ-2': -6, 170 'AZ-3': 24, 171 'AZ-4': 2, 172 'AZ-5': -11, 173 'AZ-6': -3, 174 'AZ-7': 15, 175 'AZ-8': -10, 176 'AZ-9': -16, 177 'CA-1': -12, 178 'CA-10': 18, 179 'CA-11': 37, 180 'CA-12': 40, 181 'CA-13': 4, 182 'CA-14': 22, 183 'CA-15': 28, 184 'CA-16': 26, 185 'CA-17': 23, 186 'CA-18': 21, 187 'CA-19': 18, 188 'CA-2': 23, 189 'CA-20': -16, 190 'CA-21': 9, 191 'CA-22': 5, 192 'CA-23': -8, 193 'CA-24': 13, 194 'CA-25': 6, 195 'CA-26': 8, 196 'CA-27': 4, 197 'CA-28': 16, 198 'CA-29': 26, 199 'CA-3': -4, 200 'CA-30': 23, 201 'CA-31': 15, 202 'CA-32': 20, 203 'CA-33': 12, 204 'CA-34': 32, 205 'CA-35': 13, 206 'CA-36': 21, 207 'CA-37': 37, 208 'CA-38': 14, 209 'CA-39': 12, 210 'CA-4': 17, 211 'CA-40': -2, 212 'CA-41': -3, 213 'CA-42': 22, 214 'CA-43': 32, 215 'CA-44': 24, 216 'CA-45': 2, 217 'CA-46': 15, 218 'CA-47': 3, 219 'CA-48': -9, 220 'CA-49': 3, 221 'CA-5': -9, 222 'CA-50': 14, 223 'CA-51': 12, 224 'CA-52': 18, 225 'CA-6': 7, 226 'CA-7': 17, 227 'CA-8': 26, 228 'CA-9': 5, 229 'CO-1': 29, 230 'CO-2': 17, 231 'CO-3': -7, 232 'CO-4': -13, 233 'CO-5': -9, 234 'CO-6': 9, 235 'CO-7': 4, 236 'CO-8': 0, 237 'CT-1': 12, 238 'CT-2': 3, 239 'CT-3': 7, 240 'CT-4': 13, 241 'CT-5': 3, 242 'DE-AL': 7, 243 'FL-1': -19, 244 'FL-10': 14, 245 'FL-11': -8, 246 'FL-12': -17, 247 'FL-13': -6, 248 'FL-14': 8, 249 'FL-15': -4, 250 'FL-16': -7, 251 'FL-17': -10, 252 'FL-18': -13, 253 'FL-19': -13, 254 'FL-2': -8, 255 'FL-20': 25, 256 'FL-21': -7, 257 'FL-22': 7, 258 'FL-23': 5, 259 'FL-24': 25, 260 'FL-25': 9, 261 'FL-26': -8, 262 'FL-27': 0, 263 'FL-28': -2, 264 'FL-3': -9, 265 'FL-4': -6, 266 'FL-5': -11, 267 'FL-6': -14, 268 'FL-7': -5, 269 'FL-8': -11, 270 'FL-9': 8, 271 'GA-1': -9, 272 'GA-10': -14, 273 'GA-11': -14, 274 'GA-12': -8, 275 'GA-13': 17, 276 'GA-14': -22, 277 'GA-2': 3, 278 'GA-3': -18, 279 'GA-4': 27, 280 'GA-5': 35, 281 'GA-6': 22, 282 'GA-7': -13, 283 'GA-8': -16, 284 'GA-9': -20, 285 'HI-1': 14, 286 'HI-2': 14, 287 'IA-1': -3, 288 'IA-2': -4, 289 'IA-3': -3, 290 'IA-4': -16, 291 'ID-1': -22, 292 'ID-2': -14, 293 'IL-1': 20, 294 'IL-10': 11, 295 'IL-11': 5, 296 'IL-12': -24, 297 'IL-13': 3, 298 'IL-14': 4, 299 'IL-15': -22, 300 'IL-16': -13, 301 'IL-17': 2, 302 'IL-2': 19, 303 'IL-3': 20, 304 'IL-4': 22, 305 'IL-5': 18, 306 'IL-6': 3, 307 'IL-7': 36, 308 'IL-8': 6,
309 'IL-9': 19, 310 'IN-1': 3, 311 'IN-2': -14, 312 'IN-3': -18, 313 'IN-4': -18, 314 'IN-5': -11, 315 'IN-6': -19, 316 'IN-7': 19, 317 'IN-8': -19, 318 'IN-9': -16, 319 'KS-1': -18, 320 'KS-2': -11, 321 'KS-3': -1, 322 'KS-4': -14, 323 'KY-1': -24, 324 'KY-2': -21, 325 'KY-3': 9, 326 'KY-4': -19, 327 'KY-5': -32, 328 'KY-6': -9, 329 'LA-1': -22, 330 'LA-2': 16, 331 'LA-3': -22, 332 'LA-4': -26, 333 'LA-5': -19, 334 'LA-6': 8, 335 'MA-1': 9, 336 'MA-2': 13, 337 'MA-3': 11, 338 'MA-4': 12, 339 'MA-5': 23, 340 'MA-6': 11, 341 'MA-7': 35, 342 'MA-8': 15, 343 'MA-9': 6, 344 'MD-1': -11, 345 'MD-2': 7, 346 'MD-3': 10, 347 'MD-4': 40, 348 'MD-5': 15, 349 'MD-6': 2, 350 'MD-7': 30, 351 'MD-8': 29, 352 'ME-1': 9, 353 'ME-2': -6, 354 'MI-1': -13, 355 'MI-10': -3, 356 'MI-11': 7, 357 'MI-12': 23, 358 'MI-13': 23, 359 'MI-2': -16, 360 'MI-3': 1, 361 'MI-4': -5, 362 'MI-5': -15, 363 'MI-6': 11, 364 'MI-7': -2, 365 'MI-8': -1, 366 'MI-9': -18, 367 'MN-1': -7, 368 'MN-2': 1, 369 'MN-3': 8, 370 'MN-4': 17, 371 'MN-5': 30, 372 'MN-6': -12, 373 'MN-7': -19, 374 'MN-8': -8, 375 'MO-1': 27, 376 'MO-2': -7, 377 'MO-3': -16, 378 'MO-4': -23, 379 'MO-5': 11, 380 'MO-6': -21, 381 'MO-7': -24, 382 'MO-8': -28, 383 'MS-1': -18, 384 'MS-2': 11, 385 'MS-3': -15, 386 'MS-4': -22, 387 'MT-1': -6, 388 'MT-2': -16, 389 'NC-1': -1, 390 'NC-10': -10, 391 'NC-11': -8, 392 'NC-12': 23, 393 'NC-13': -11, 394 'NC-14': -11, 395 'NC-2': 15, 396 'NC-3': -11, 397 'NC-4': 21, 398 'NC-5': -10, 399 'NC-6': -11, 400 'NC-7': -8, 401 'NC-8': -11, 402 'NC-9': -9, 403 'ND-AL': -20, 404 'NE-1': -9, 405 'NE-2': 0, 406 'NE-3': -29, 407 'NH-1': 0, 408 'NH-2': 2, 409 'NJ-1': 10, 410 'NJ-10': 30, 411 'NJ-11': 6, 412 'NJ-12': 12, 413 'NJ-2': -5, 414 'NJ-3': 5, 415 'NJ-4': -14, 416 'NJ-5': 4, 417 'NJ-6': 8, 418 'NJ-7': -1, 419 'NJ-8': 22, 420 'NJ-9': 8, 421 'NM-1': 5, 422 'NM-2': 1, 423 'NM-3': 4, 424 'NV-1': 3, 425 'NV-2': -8, 426 'NV-3': 1, 427 'NV-4': 3, 428 'NY-1': -4, 429 'NY-10': 34, 430 'NY-11': -6, 431 'NY-12': 34, 432 'NY-13': 38, 433 'NY-14': 27, 434 'NY-15': 35, 435 'NY-16': 21, 436 'NY-17': 3, 437 'NY-18': 2, 438 'NY-19': -1, 439 'NY-2': -4, 440 'NY-20': 7, 441 'NY-21': -10, 442 'NY-22': 3, 443 'NY-23': -12, 444 'NY-24': -13, 445 'NY-25': 8, 446 'NY-26': 10, 447 'NY-3': 3, 448 'NY-4': 5, 449 'NY-5': 30, 450 'NY-6': 14, 451 'NY-7': 30, 452 'NY-8': 27, 453 'NY-9': 25, 454 'OH-1': 2, 455 'OH-10': -4, 456 'OH-11': 28, 457 'OH-12': -18, 458 'OH-13': -1, 459 'OH-14': -9, 460 'OH-15': -6, 461 'OH-2': -25, 462 'OH-3': 20, 463 'OH-4': -20, 464 'OH-5': -15, 465 'OH-6': -16, 466 'OH-7': -7, 467 'OH-8': -14, 468 'OH-9': -3, 469 'OK-1': -14, 470 'OK-2': -29, 471 'OK-3': -24, 472 'OK-4': -19, 473 'OK-5': -12, 474 'OR-1': 18, 475 'OR-2': -15, 476 'OR-3': 22, 477 'OR-4': 4, 478 'OR-5': 2, 479 'OR-6': 4, 480 'PA-1': 0, 481 'PA-10': -5, 482 'PA-11': -13, 483 'PA-12': 8, 484 'PA-13': -25, 485 'PA-14': -18, 486 'PA-15': -21, 487 'PA-16': -13, 488 'PA-17': 0, 489 'PA-2': 20, 490 'PA-3': 39, 491 'PA-4': 7, 492 'PA-5': 14, 493 'PA-6': 5, 494 'PA-7': -2, 495 'PA-8': -4, 496 'PA-9': -21, 497 'RI-1': 12, 498 'RI-2': 4, 499 'SC-1': -7, 500 'SC-2': -8, 501 'SC-3': -21, 502 'SC-4': -12, 503 'SC-5': -12, 504 'SC-6': 14, 505 'SC-7': -11, 506 'SD-AL': -16, 507 'TN-1': -30, 508 'TN-2': -18, 509 'TN-3': -19, 510 'TN-4': -22, 511 'TN-5': -9, 512 'TN-6': -17, 513 'TN-7': -10, 514 'TN-8': -21, 515 'TN-9': 22, 516 'TX-1': -26, 517 'TX-10': -13, 518 'TX-11': -23, 519 'TX-12': -12, 520 'TX-13': -26, 521 'TX-14': -17, 522 'TX-15': -1, 523 'TX-16': 17, 524 'TX-17': -14, 525 'TX-18': 23, 526 'TX-19': -26, 527 'TX-2': -15, 528 'TX-20': 15, 529 'TX-21': -13,
530 'TX-22': -11, 531 'TX-23': -5, 532 'TX-24': -10, 533 'TX-25': -19, 534 'TX-26': -13, 535 'TX-27': -13, 536 'TX-28': 3, 537 'TX-29': 18, 538 'TX-3': -11, 539 'TX-30': 27, 540 'TX-31': -14, 541 'TX-32': 14, 542 'TX-33': 24, 543 'TX-34': 9, 544 'TX-35': 21, 545 'TX-36': -18, 546 'TX-37': 24, 547 'TX-38': -12, 548 'TX-4': -16, 549 'TX-5': -14, 550 'TX-6': -15, 551 'TX-7': 13, 552 'TX-8': -16, 553 'TX-9': 26, 554 'UT-1': -12, 555 'UT-2': -11, 556 'UT-3': -13, 557 'UT-4': -16, 558 'VA-1': -6, 559 'VA-10': 6, 560 'VA-11': 18, 561 'VA-2': -2, 562 'VA-3': 17, 563 'VA-4': 16, 564 'VA-5': -7, 565 'VA-6': -14, 566 'VA-7': 1, 567 'VA-8': 26, 568 'VA-9': -23, 569 'VT-AL': 16, 570 'WA-1': 13, 571 'WA-10': 7, 572 'WA-2': 9, 573 'WA-3': -5, 574 'WA-4': -11, 575 'WA-5': -8, 576 'WA-6': 6, 577 'WA-7': 36, 578 'WA-8': 1, 579 'WA-9': 21, 580 'WI-1': -3, 581 'WI-2': 19, 582 'WI-3': -4, 583 'WI-4': 25, 584 'WI-5': -14, 585 'WI-6': -10, 586 'WI-7': -12, 587 'WI-8': -10, 588 'WV-1': -23, 589 'WV-2': -22, 590 'WY-AL': -25 591 }; 592 593 function forecastPresAvgSignedMargin(stateKey) { 594 if (!stateKey) return null; 595 var v; 596 if (ET.SENATE_FORECAST_PRES_AVG && ET.SENATE_FORECAST_PRES_AVG[stateKey] != null) { 597 v = ET.SENATE_FORECAST_PRES_AVG[stateKey]; 598 } else if (ET.GOV_FORECAST_PRES_AVG && ET.GOV_FORECAST_PRES_AVG[stateKey] != null) { 599 v = ET.GOV_FORECAST_PRES_AVG[stateKey]; 600 } else { 601 return null; 602 } 603 return v != null && isFinite(v) ? v : null; 604 } 605 606 function senateEditorialRatingForModel(key, asOfIso) { 607 if (key && asOfIso && ET.SENATE_EDITORIAL_BLEND_EFFECTIVE_FROM && 608 ET.SENATE_EDITORIAL_BLEND_EFFECTIVE_FROM[key] && 609 asOfIso < ET.SENATE_EDITORIAL_BLEND_EFFECTIVE_FROM[key]) { 610 if (ET.SENATE_EDITORIAL_BLEND_PRIOR && ET.SENATE_EDITORIAL_BLEND_PRIOR[key]) { 611 return ET.SENATE_EDITORIAL_BLEND_PRIOR[key]; 612 } 613 return ET.SENATE_EDITORIAL_RATINGS && ET.SENATE_EDITORIAL_RATINGS[key]; 614 } 615 if (ET.SENATE_EDITORIAL_RATINGS_BASE && ET.SENATE_EDITORIAL_RATINGS_BASE[key]) { 616 return ET.SENATE_EDITORIAL_RATINGS_BASE[key]; 617 } 618 return ET.SENATE_EDITORIAL_RATINGS && ET.SENATE_EDITORIAL_RATINGS[key]; 619 } 620 621 function govEditorialRatingForModel(key) { 622 if (ET.GOV_EDITORIAL_RATINGS_BASE && ET.GOV_EDITORIAL_RATINGS_BASE[key]) { 623 return ET.GOV_EDITORIAL_RATINGS_BASE[key]; 624 } 625 if (ET.COOK_GOV_RATINGS && ET.COOK_GOV_RATINGS[key]) { 626 return ET.COOK_GOV_RATINGS[key]; 627 } 628 return ET.GOV_EDITORIAL_RATINGS && ET.GOV_EDITORIAL_RATINGS[key]; 629 } 630 631 /** Senate cook component: editorial rating tier (INPUT_RATING_TO_MARGIN), optional per-state override. */ 632 ET.forecastModelSenateEditorialBlendMargin = function (stateKey, rating) { 633 if (stateKey && ET.SENATE_EDITORIAL_BLEND_MARGINS && 634 ET.SENATE_EDITORIAL_BLEND_MARGINS[stateKey] != null && 635 isFinite(ET.SENATE_EDITORIAL_BLEND_MARGINS[stateKey])) { 636 return ET.SENATE_EDITORIAL_BLEND_MARGINS[stateKey]; 637 } 638 return ET.forecastModelRatingToMargin(rating); 639 }; 640 641 /** Governor cook component: editorial rating tier, optional per-state override. */ 642 ET.forecastModelGovEditorialBlendMargin = function (stateKey, rating) { 643 if (stateKey && ET.GOV_EDITORIAL_BLEND_MARGINS && 644 ET.GOV_EDITORIAL_BLEND_MARGINS[stateKey] != null && 645 isFinite(ET.GOV_EDITORIAL_BLEND_MARGINS[stateKey])) { 646 return ET.GOV_EDITORIAL_BLEND_MARGINS[stateKey]; 647 } 648 return ET.forecastModelRatingToMargin(rating); 649 }; 650 651 /** Cook/editorial rating â signed D margin (House/Gov; Senate uses forecastModelSenateEditorialBlendMargin). */ 652 ET.forecastModelCookMargin = function (cookRating, stateKey) { 653 return ET.forecastModelRatingToMargin(cookRating); 654 }; 655 656 /** 657 * House cook component: COOK_HOUSE_RATINGS tier unless a 2026 incumbent qualifies for 658 * Ultra Solid (2024 margin â¥25) or Mega Solid (â¥35) â internal blend only. 659 */ 660 ET.forecastModelHouseCookBlendMargin = function (code, cookRating) { 661 var key = houseDistrictLookupKey(code); 662 if (key && ET.HOUSE_INCUMBENT_2024_BLEND_MARGINS && 663 ET.HOUSE_INCUMBENT_2024_BLEND_MARGINS[key] && 664 ET.HOUSE_INCUMBENT_2024_BLEND_MARGINS[key].margin != null && 665 isFinite(ET.HOUSE_INCUMBENT_2024_BLEND_MARGINS[key].margin)) { 666 return ET.HOUSE_INCUMBENT_2024_BLEND_MARGINS[key].margin; 667 } 668 return ET.forecastModelHouseRatingToMargin(cookRating); 669 }; 670 671 ET.forecastModelPollWeightAtCount = function (pollCount, hasPollMargin, pollBaseAtFull, fullCount) { 672 pollBaseAtFull = pollBaseAtFull == null ? 0.45 : pollBaseAtFull; 673 fullCount = fullCount == null ? POLL_FULL_COUNT : fullCount; 674 var n = Math.max(0, Math.floor(pollCount || 0)); 675 if (!hasPollMargin || n <= 0) return 0; 676 if (n >= fullCount) return pollBaseAtFull; 677 return Math.max(0, pollBaseAtFull - (fullCount - n) * POLL_STEP); 678 }; 679 680 function scaleNonPollWeights(pollW, baseNonPoll) { 681 var sum = 0; 682 var k; 683 for (k in baseNonPoll) { 684 if (baseNonPoll.hasOwnProperty(k)) sum += baseNonPoll[k]; 685 } 686 if (sum <= 0) return { poll: pollW }; 687 var scale = pollW >= 1 ? 0 : (1 - pollW) / sum; 688 var w = { poll: pollW }; 689 for (k in baseNonPoll) { 690 if (baseNonPoll.hasOwnProperty(k)) w[k] = baseNonPoll[k] * scale; 691 } 692 return w; 693 } 694 695 /** Senate weights; drops priorSenate / incumbency when not applicable and rescales. */ 696 ET.forecastModelSenateBlendWeights = function (pollCount, hasPollMargin, hasPriorSenate, hasIncumbency) { 697 var pollW = ET.forecastModelPollWeightAtCount(pollCount, hasPollMargin, SENATE_POLL_BASE_AT_8); 698 var base = { 699 generic: SENATE_BASE_NON_POLL.generic, 700 trump: SENATE_BASE_NON_POLL.trump, 701 presAvg: SENATE_BASE_NON_POLL.presAvg, 702 pvi: SENATE_BASE_NON_POLL.pvi, 703 cook: SENATE_BASE_NON_POLL.cook 704 }; 705 if (hasPriorSenate) base.priorSenate = SENATE_BASE_NON_POLL.priorSenate; 706 if (hasIncumbency) base.incumbency = SENATE_BASE_NON_POLL.incumbency; 707 return scaleNonPollWeights(pollW, base); 708 }; 709 710 /** Governor weights; drops priorGov / incumbency when not applicable and rescales. */ 711 ET.forecastModelGovBlendWeights = function (pollCount, hasPollMargin, hasPriorGov, hasIncumbency) { 712 var pollW = ET.forecastModelPollWeightAtCount(pollCount, hasPollMargin, GOV_POLL_BASE_AT_8); 713 var base = { 714 generic: GOV_BASE_NON_POLL.generic, 715 trump: GOV_BASE_NON_POLL.trump, 716 presAvg: GOV_BASE_NON_POLL.presAvg, 717 pvi: GOV_BASE_NON_POLL.pvi, 718 cook: GOV_BASE_NON_POLL.cook 719 }; 720 if (hasPriorGov) base.priorGov = GOV_BASE_NON_POLL.priorGov; 721 if (hasIncumbency) base.incumbency = GOV_BASE_NON_POLL.incumbency; 722 return scaleNonPollWeights(pollW, base); 723 }; 724 725 /** 726 * House weights; drops unavailable incumbency / previous-result inputs and rescales. 727 * @param {{ solidRace?: boolean, closeRace?: boolean }} [opts] 728 */ 729 ET.forecastModelHouseBlendWeights = function (pollCount, hasPollMargin, hasIncumbency, hasPriorHouse, opts) { 730 opts = opts || {}; 731 var pollW = ET.forecastModelPollWeightAtCount( 732 pollCount, hasPollMargin, HOUSE_POLL_BASE_AT_6, HOUSE_POLL_FULL_COUNT); 733 var base = { 734 generic: HOUSE_BASE_NON_POLL.generic, 735 trump: HOUSE_BASE_NON_POLL.trump, 736 pvi: HOUSE_BASE_NON_POLL.pvi, 737 cook: HOUSE_BASE_NON_POLL.cook 738 }; 739 if (hasIncumbency) base.incumbency = HOUSE_BASE_NON_POLL.incumbency; 740 if (hasPriorHouse) base.priorHouse = HOUSE_BASE_NON_POLL.priorHouse; 741 return scaleNonPollWeights(pollW, base); 742 }; 743 744 function recencyWeightedMean(rows, valueKey) { 745 if (!rows || !rows.length) return null; 746 var sorted = rows.slice().sort(function (a, b) { 747 return a.date < b.date ? 1 : (a.date > b.date ? -1 : 0); 748 }); 749 var wSum = 0; 750 var vSum = 0; 751 for (var i = 0; i < sorted.length; i++) {
752 var v = sorted[i][valueKey]; 753 if (v == null || !isFinite(v)) continue; 754 var w = 1 / (1 + i * 0.35); 755 wSum += w; 756 vSum += v * w; 757 } 758 return wSum ? vSum / wSum : null; 759 } 760 761 /** Match generic-ballot / Trump approval charts: ~28-day effective window. */ 762 var NATIONAL_ENV_HALFLIFE_DAYS = 20; 763 var NATIONAL_ENV_ROLLING_DAYS = 28; 764 /** Faster decay for daily forecast trends so generic / Trump drift visibly day-to-day. */ 765 var NATIONAL_ENV_TREND_HALFLIFE_DAYS = 12; 766 var NATIONAL_ENV_TREND_ROLLING_DAYS = 28; 767 768 function isoToMs(iso) { 769 if (!iso || iso.length < 10) return NaN; 770 return new Date(iso.slice(0, 10) + 'T12:00:00Z').getTime(); 771 } 772 773 function nationalPollSampleWt(poll) { 774 if (!poll) return 1; 775 var s = poll.sample; 776 if (s == null || s === '') return 1; 777 if (typeof s === 'number' && isFinite(s)) return s; 778 var cleaned = String(s).replace(/,/g, ''); 779 var m = cleaned.match(/^\s*(\d+)/); 780 return m ? +m[1] : 1; 781 } 782 783 /** 784 * Mean as of a calendar day: sample-weighted with exponential decay by days since fieldwork 785 * so the value drifts slightly every day even between new polls. 786 */ 787 function calendarDecayWeightedMean(rows, valueKey, asOfIso, halfLifeDays) { 788 if (!rows || !rows.length || !asOfIso) return null; 789 halfLifeDays = halfLifeDays == null ? NATIONAL_ENV_HALFLIFE_DAYS : halfLifeDays; 790 var asOfMs = isoToMs(asOfIso); 791 if (!isFinite(asOfMs)) return null; 792 var ln2 = Math.LN2; 793 var wSum = 0; 794 var vSum = 0; 795 var i; 796 for (i = 0; i < rows.length; i++) { 797 var p = rows[i]; 798 if (!p.date || p.date > asOfIso) continue; 799 var v = p[valueKey]; 800 if (v == null || !isFinite(v)) continue; 801 var daysAgo = (asOfMs - isoToMs(p.date)) / 86400000; 802 if (daysAgo < 0 || !isFinite(daysAgo)) continue; 803 var w = nationalPollSampleWt(p) * Math.exp(-daysAgo * ln2 / halfLifeDays); 804 wSum += w; 805 vSum += v * w; 806 } 807 return wSum ? vSum / wSum : null; 808 } 809 810 /** 28-day inclusive rolling window (approval-page style); shifts daily as polls age out. */ 811 function rollingMeanInWindow(rows, valueKey, asOfIso, windowDays) { 812 if (!rows || !rows.length || !asOfIso) return null; 813 windowDays = windowDays == null ? NATIONAL_ENV_ROLLING_DAYS : windowDays; 814 var endMs = isoToMs(asOfIso); 815 if (!isFinite(endMs)) return null; 816 var startMs = endMs - (windowDays - 1) * 86400000; 817 var wSum = 0; 818 var vSum = 0; 819 var i; 820 for (i = 0; i < rows.length; i++) { 821 var p = rows[i]; 822 if (!p.date || p.date > asOfIso) continue; 823 var pm = isoToMs(p.date); 824 if (!isFinite(pm) || pm < startMs || pm > endMs) continue; 825 var v = p[valueKey]; 826 if (v == null || !isFinite(v)) continue; 827 var w = nationalPollSampleWt(p); 828 wSum += w; 829 vSum += v * w; 830 } 831 return wSum ? vSum / wSum : null; 832 } 833 834 function nationalMeanAsOf(rows, valueKey, asOfIso) { 835 var v = calendarDecayWeightedMean(rows, valueKey, asOfIso, NATIONAL_ENV_HALFLIFE_DAYS); 836 if (v != null) return v; 837 v = rollingMeanInWindow(rows, valueKey, asOfIso, NATIONAL_ENV_ROLLING_DAYS); 838 if (v != null) return v; 839 return recencyWeightedMean(rows, valueKey); 840 } 841 842 /** Trend series: faster daily drift; anchor to first reading before national polls exist. */ 843 function nationalMeanAsOfForTrend(rows, valueKey, asOfIso) { 844 var v = calendarDecayWeightedMean(rows, valueKey, asOfIso, NATIONAL_ENV_TREND_HALFLIFE_DAYS); 845 if (v != null) return v; 846 v = rollingMeanInWindow(rows, valueKey, asOfIso, NATIONAL_ENV_TREND_ROLLING_DAYS); 847 if (v != null) return v; 848 var eligible = []; 849 var i; 850 var p; 851 for (i = 0; i < (rows || []).length; i++) { 852 p = rows[i]; 853 if (p.date && p.date <= asOfIso && p[valueKey] != null && isFinite(p[valueKey])) { 854 eligible.push(p); 855 } 856 } 857 if (eligible.length) return recencyWeightedMean(eligible, valueKey); 858 for (i = 0; i < (rows || []).length; i++) { 859 p = rows[i]; 860 if (p[valueKey] != null && isFinite(p[valueKey])) return p[valueKey]; 861 } 862 return recencyWeightedMean(rows, valueKey); 863 } 864 865 function rollingRaceDrAtDate(pollRows, asOfIso) { 866 if (!pollRows || !pollRows.length || !asOfIso) return null; 867 var endMs = isoToMs(asOfIso); 868 if (!isFinite(endMs)) return null; 869 var ln2 = Math.LN2; 870 var wSum = 0; 871 var dSum = 0; 872 var rSum = 0; 873 var i; 874 var p; 875 var d; 876 var r; 877 var daysAgo; 878 var w; 879 for (i = 0; i < pollRows.length; i++) { 880 p = pollRows[i]; 881 if (p.avgExclude) continue; 882 var iso = racePollIso(p) || p.date; 883 if (!iso || iso > asOfIso) continue; 884 d = p.D; 885 r = p.R; 886 if (typeof d !== 'number' || typeof r !== 'number' || !isFinite(d) || !isFinite(r)) continue; 887 daysAgo = (endMs - isoToMs(iso)) / 86400000; 888 if (daysAgo < 0 || !isFinite(daysAgo)) continue; 889 w = nationalPollSampleWt(p) * Math.exp(-daysAgo * ln2 / NATIONAL_ENV_HALFLIFE_DAYS); 890 dSum += d * w; 891 rSum += r * w; 892 wSum += w; 893 } 894 if (!wSum) return null; 895 var dPct = dSum / wSum; 896 var rPct = rSum / wSum; 897 return { margin: dPct - rPct, dPct: dPct, rPct: rPct }; 898 } 899 900 var FORECAST_TREND_START = '2026-01-01'; 901 ET.FORECAST_TREND_START_ISO = FORECAST_TREND_START; 902 903 function pollsOnOrBeforeDate(rows, asOfIso) { 904 if (!rows || !rows.length || !asOfIso) return []; 905 return rows.filter(function (p) { 906 return p.date && p.date <= asOfIso; 907 }); 908 } 909 910 function racePollIso(p) { 911 if (!p) return null; 912 if (typeof ET.pollFieldworkIso === 'function') { 913 var fw = ET.pollFieldworkIso(p); 914 if (fw) return fw; 915 } 916 if (typeof ET.pollSiteListedIso === 'function') { 917 var listed = ET.pollSiteListedIso(p); 918 if (listed) return listed; 919 } 920 return p.date || null; 921 } 922 923 function racePollsOnOrBeforeDate(rows, asOfIso) { 924 if (!rows || !rows.length || !asOfIso) return []; 925 return rows.filter(function (p) { 926 var iso = racePollIso(p); 927 return iso && iso <= asOfIso; 928 }); 929 } 930 931 /** Senate hub control-odds backtest: only polls whose fieldwork/list date is strictly before asOfIso. */ 932 function racePollsStrictlyBeforeDate(rows, asOfIso) { 933 if (!rows || !rows.length || !asOfIso) return []; 934 return rows.filter(function (p) { 935 var iso = racePollIso(p); 936 return iso && iso < asOfIso; 937 }); 938 } 939 940 function pollsStrictlyBeforeDate(rows, asOfIso) { 941 if (!rows || !rows.length || !asOfIso) return []; 942 return rows.filter(function (p) { 943 return p.date && p.date < asOfIso; 944 }); 945 } 946 947 function dailyLabelsEndingAt(startIso, endIso) { 948 if (!endIso) { 949 endIso = typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : FORECAST_TREND_START; 950 } 951 var start = startIso || FORECAST_TREND_START; 952 if (!ET.nextISO) return [endIso]; 953 var out = [start]; 954 var cur = start; 955 while (cur < endIso) { 956 cur = ET.nextISO(cur); 957 out.push(cur); 958 } 959 return out; 960 } 961 962 function marginFromAvgsAtDate(dAvg, rAvg, labels, asOfIso) { 963 var idx = -1; 964 var i; 965 for (i = labels.length - 1; i >= 0; i--) { 966 if (labels[i] <= asOfIso) { 967 idx = i; 968 break; 969 } 970 } 971 if (idx < 0) return null;
972 if (dAvg[idx] != null && rAvg[idx] != null) { 973 return { margin: dAvg[idx] - rAvg[idx], dPct: dAvg[idx], rPct: rAvg[idx] }; 974 } 975 return null; 976 } 977 978 function nationalEnvironmentFromPolls(gb, ta, asOfIso, meanFn) { 979 meanFn = meanFn || nationalMeanAsOf; 980 var genericD = meanFn(gb, 'approve', asOfIso); 981 var genericR = meanFn(gb, 'disapprove', asOfIso); 982 var trumpAp = meanFn(ta, 'approve', asOfIso); 983 var trumpDis = meanFn(ta, 'disapprove', asOfIso); 984 var genericMargin = (genericD != null && genericR != null) ? genericD - genericR : 0; 985 var trumpNet = (trumpAp != null && trumpDis != null) ? trumpAp - trumpDis : 0; 986 /** Pro-D environment when Trump is underwater (disapprove â approve). */ 987 var trumpProD = (trumpAp != null && trumpDis != null) ? trumpDis - trumpAp : 0; 988 return { 989 genericMargin: genericMargin, 990 trumpNet: trumpNet, 991 trumpProD: trumpProD 992 }; 993 } 994 995 ET.forecastModelNationalEnvironmentAsOf = function (asOfIso) { 996 var gbAll = typeof window !== 'undefined' ? window.GENERIC_BALLOT_POLLS : null; 997 var taAll = typeof window !== 'undefined' ? window.TRUMP_APPROVAL_POLLS : null; 998 var gb = asOfIso ? pollsOnOrBeforeDate(gbAll, asOfIso) : gbAll; 999 var ta = asOfIso ? pollsOnOrBeforeDate(taAll, asOfIso) : taAll; 1000 return nationalEnvironmentFromPolls(gb, ta, asOfIso, nationalMeanAsOf); 1001 }; 1002 1003 /** Daily trend / simulation history â generic ballot + Trump approval move every calendar day. */ 1004 ET.forecastModelNationalEnvironmentAsOfForTrend = function (asOfIso) { 1005 var gbAll = typeof window !== 'undefined' ? window.GENERIC_BALLOT_POLLS : null; 1006 var taAll = typeof window !== 'undefined' ? window.TRUMP_APPROVAL_POLLS : null; 1007 var gb = asOfIso ? pollsOnOrBeforeDate(gbAll, asOfIso) : gbAll; 1008 var ta = asOfIso ? pollsOnOrBeforeDate(taAll, asOfIso) : taAll; 1009 return nationalEnvironmentFromPolls(gb, ta, asOfIso, nationalMeanAsOfForTrend); 1010 }; 1011 1012 ET.forecastModelNationalEnvironment = function () { 1013 var today = typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : null; 1014 return ET.forecastModelNationalEnvironmentAsOf(today || '9999-12-31'); 1015 }; 1016 1017 ET.forecastModelDailyTrendLabels = function () { 1018 var end = typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : FORECAST_TREND_START; 1019 return dailyLabelsEndingAt(FORECAST_TREND_START, end); 1020 }; 1021 1022 ET.forecastModelMarginToWinProb = function (margin, chamber) { 1023 if (chamber === 'house' && typeof ET.forecastMarginToTargetWinProb === 'function') { 1024 return ET.forecastMarginToTargetWinProb(margin, 'house'); 1025 } 1026 var eff = forecastModelEffectiveMargin(margin == null || !isFinite(margin) ? 0 : margin); 1027 return 1 / (1 + Math.exp(-eff / WIN_PROB_LOGISTIC_SCALE)); 1028 }; 1029 1030 /** 1031 * Win-probability tiers from favored party odds (no toss-up): 1032 * Solid â¥95%, Likely 80â95%, Lean 65â80%, Tilt <65% (includes 50â55% coin-flip band). 1033 * @param {number} pD â probability Democrats win (0â1) 1034 */ 1035 ET.forecastModelWinProbToRating = function (pD, stateKey) { 1036 var p = pD == null || !isFinite(pD) ? 0.5 : Math.max(0, Math.min(1, pD)); 1037 var lead = p >= 0.5 ? 'D' : 'R'; 1038 var winP = lead === 'D' ? p : 1 - p; 1039 if (winP >= 0.95) return lead === 'D' ? 'solidD' : 'solidR'; 1040 if (winP >= 0.80) return lead === 'D' ? 'likelyD' : 'likelyR'; 1041 if (winP >= 0.65) return lead === 'D' ? 'leanD' : 'leanR'; 1042 return lead === 'D' ? 'tiltD' : 'tiltR'; 1043 }; 1044 1045 /** Favored party from simulated win probabilities (always D or R). */ 1046 ET.forecastModelFavoredPartyFromWinProb = function (pD) { 1047 var p = pD == null || !isFinite(pD) ? 0.5 : Math.max(0, Math.min(1, pD)); 1048 return p >= 0.5 ? 'D' : 'R'; 1049 }; 1050 1051 /** When rounded projected vote is 50/50, break margin label/chip ties using win odds. */ 1052 ET.forecastModelEvenVoteLead = function (winProbD) { 1053 if (typeof ET.forecastEvenVoteLeadFromWinProb === 'function') { 1054 return ET.forecastEvenVoteLeadFromWinProb(winProbD); 1055 } 1056 if (winProbD == null || !isFinite(winProbD)) return 'Tie'; 1057 if (winProbD > 0.5) return 'D'; 1058 if (winProbD < 0.5) return 'R'; 1059 return 'Tie'; 1060 }; 1061 1062 /** True when margin rounds to an even two-party vote share (50.0% / 50.0%). */ 1063 ET.forecastModelMarginIsEvenVote = function (margin) { 1064 if (typeof ET.forecastMarginIsEvenVote === 'function') { 1065 return ET.forecastMarginIsEvenVote(margin); 1066 } 1067 if (margin == null || !isFinite(margin)) return false;
1068 return Math.abs(margin) < 0.05; 1069 }; 1070 1071 /** Display margin label, e.g. D+3.2 or R+8.7; even vote + win-odds lead â D+0.0 / R+0.0. */ 1072 ET.forecastModelMarginDisplayLabel = function (margin, winProbD) { 1073 if (margin == null || !isFinite(margin)) return ''; 1074 if (ET.forecastModelMarginIsEvenVote(margin)) { 1075 var evenLead = ET.forecastModelEvenVoteLead(winProbD); 1076 if (evenLead === 'D') return 'D+0.0'; 1077 if (evenLead === 'R') return 'R+0.0'; 1078 return 'Even'; 1079 } 1080 var party = margin > 0 ? 'D' : 'R'; 1081 var absM = Math.abs(margin); 1082 var dec = typeof ET.oneDec === 'function' ? ET.oneDec(absM) : absM.toFixed(1); 1083 return party + '+' + dec; 1084 }; 1085 1086 /** solid | likely | lean | tilt â matches win-prob rating tiers (for race-page copy). */ 1087 ET.forecastModelMarginTierFromWinProb = function (pD) { 1088 var rating = ET.forecastModelWinProbToRating(pD); 1089 return ET.editorialRatingToMarginTier 1090 ? ET.editorialRatingToMarginTier(rating) 1091 : 'tilt'; 1092 }; 1093 1094 /** @deprecated Use win-probability path; kept for callers expecting a margin tier. */ 1095 ET.forecastModelMarginToRating = function (margin) { 1096 return ET.forecastModelWinProbToRating(ET.forecastModelMarginToWinProb(margin)); 1097 }; 1098 1099 function runBlend(components, pollMargin, pollCount, weightFn, stateKey) { 1100 var hasPoll = pollMargin != null && isFinite(pollMargin); 1101 var w = weightFn(pollCount, hasPoll); 1102 var m = 0; 1103 if (hasPoll && w.poll) m += w.poll * pollMargin; 1104 if (w.generic) m += w.generic * (components.generic || 0); 1105 if (w.trump) m += w.trump * (components.trump || 0); 1106 if (w.incumbency) m += w.incumbency * (components.incumbency || 0); 1107 if (w.priorHouse) m += w.priorHouse * (components.priorHouse || 0); 1108 if (w.priorSenate) m += w.priorSenate * (components.priorSenate || 0); 1109 if (w.priorGov) m += w.priorGov * (components.priorGov || 0); 1110 if (w.presAvg) m += w.presAvg * (components.presAvg || 0); 1111 if (w.pvi) m += w.pvi * (components.pvi || 0); 1112 if (w.cook) m += w.cook * (components.cook || 0); 1113 var pD = ET.forecastModelMarginToWinProb(m); 1114 return { 1115 margin: m, 1116 winProbD: pD, 1117 winProbR: 1 - pD, 1118 rating: ET.forecastModelWinProbToRating(pD, stateKey), 1119 weights: w, 1120 components: components 1121 }; 1122 } 1123 1124 function runHouseBlend(components, pollMargin, pollCount, weightFn, stateKey) { 1125 var hasPoll = pollMargin != null && isFinite(pollMargin); 1126 var w = weightFn(pollCount, hasPoll); 1127 var m = 0; 1128 if (hasPoll && w.poll) m += w.poll * pollMargin; 1129 if (w.generic) m += w.generic * (components.generic || 0); 1130 if (w.trump) m += w.trump * (components.trump || 0); 1131 if (w.incumbency) m += w.incumbency * (components.incumbency || 0); 1132 if (w.priorHouse) m += w.priorHouse * (components.priorHouse || 0); 1133 if (w.pvi) m += w.pvi * (components.pvi || 0); 1134 if (w.cook) m += w.cook * (components.cook || 0); 1135 var pD = typeof ET.forecastMarginToTargetWinProb === 'function' 1136 ? ET.forecastMarginToTargetWinProb(m, 'house') 1137 : ET.forecastModelMarginToWinProb(m, 'house'); 1138 return { 1139 margin: m, 1140 winProbD: pD, 1141 winProbR: 1 - pD, 1142 rating: ET.forecastModelWinProbToRating(pD, stateKey), 1143 weights: w, 1144 components: components 1145 }; 1146 } 1147 1148 ET.forecastModelPollMarginFromDetail = function (det) { 1149 if (!det) return null; 1150 if (det.pollDPct != null && det.pollRPct != null && 1151 isFinite(det.pollDPct) && isFinite(det.pollRPct)) { 1152 return det.pollDPct - det.pollRPct; 1153 } 1154 return null; 1155 }; 1156 1157 /** 1158 * Composite D margin from blend weights (optional noise on generic / Trump components for simulation). 1159 * @param {{ genericNoise?: number, trumpNoise?: number }} [opts] 1160 */ 1161 ET.forecastModelBlendMargin = function (components, weights, pollMargin, opts) { 1162 opts = opts || {}; 1163 var w = weights || {}; 1164 var c = components || {}; 1165 var m = 0; 1166 var hasPoll = pollMargin != null && isFinite(pollMargin); 1167 if (hasPoll && w.poll) m += w.poll * pollMargin; 1168 if (w.generic) m += w.generic * ((c.generic || 0) + (opts.genericNoise || 0)); 1169 if (w.trump) m += w.trump * ((c.trump || 0) + (opts.trumpNoise || 0)); 1170 if (w.incumbency) m += w.incumbency * (c.incumbency || 0); 1171 if (w.priorHouse) m += w.priorHouse * (c.priorHouse || 0); 1172 if (w.priorSenate) m += w.priorSenate * (c.priorSenate || 0
1172); 1173 if (w.priorGov) m += w.priorGov * (c.priorGov || 0); 1174 if (w.presAvg) m += w.presAvg * (c.presAvg || 0); 1175 if (w.pvi) m += w.pvi * (c.pvi || 0); 1176 if (w.cook) m += w.cook * (c.cook || 0); 1177 return m; 1178 }; 1179 1180 ET.forecastModelSenateBlend = function (components, pollMargin, pollCount, blendOpts) { 1181 blendOpts = blendOpts || {}; 1182 var hasPrior = components.priorSenate != null && isFinite(components.priorSenate); 1183 var hasInc = components.incumbency != null && isFinite(components.incumbency); 1184 var detail = runBlend(components, pollMargin, pollCount, function (n, hasPoll) { 1185 return ET.forecastModelSenateBlendWeights(n, hasPoll, hasPrior, hasInc); 1186 }, blendOpts.stateKey); 1187 detail.forecastKind = 'senate'; 1188 return detail; 1189 }; 1190 1191 ET.forecastModelGovBlend = function (components, pollMargin, pollCount, blendOpts) { 1192 blendOpts = blendOpts || {}; 1193 var hasPrior = components.priorGov != null && isFinite(components.priorGov); 1194 var hasInc = components.incumbency != null && isFinite(components.incumbency); 1195 var detail = runBlend(components, pollMargin, pollCount, function (n, hasPoll) { 1196 return ET.forecastModelGovBlendWeights(n, hasPoll, hasPrior, hasInc); 1197 }, blendOpts.stateKey); 1198 detail.forecastKind = 'gov'; 1199 return detail; 1200 }; 1201 1202 ET.forecastModelHouseBlend = function (components, pollMargin, pollCount, blendOpts) { 1203 blendOpts = blendOpts || {}; 1204 var hasInc = components.incumbency != null && isFinite(components.incumbency); 1205 var hasPrior = components.priorHouse != null && isFinite(components.priorHouse); 1206 var detail = runBlend(components, pollMargin, pollCount, function (n, hasPoll) { 1207 return ET.forecastModelHouseBlendWeights(n, hasPoll, hasInc, hasPrior, blendOpts); 1208 }, blendOpts.stateKey); 1209 detail.forecastKind = 'house'; 1210 return forecastModelApplyHouseGopBoost(detail, blendOpts.cookRating); 1211 }; 1212 1213 function forecastModelHouseTossupish(detail, cookRating) { 1214 if (!detail) return false; 1215 if (cookRating === 'tossup' || detail.rating === 'tossup') return true; 1216 var pD = detail.winProbD; 1217 if (pD == null || !isFinite(pD)) { 1218 pD = ET.forecastModelMarginToWinProb(detail.margin != null && isFinite(detail.margin) ? detail.margin : 0); 1219 } 1220 var fav = pD >= 0.5 ? pD : 1 - pD; 1221 return fav < 0.575; 1222 } 1223 1224 function forecastModelHouseGopBoostShift(detail, cookRating) { 1225 if (!detail || detail.margin == null || !isFinite(detail.margin)) return 0; 1226 var base = ET.HOUSE_FORECAST_GOP_MARGIN_BOOST != null && isFinite(ET.HOUSE_FORECAST_GOP_MARGIN_BOOST) 1227 ? ET.HOUSE_FORECAST_GOP_MARGIN_BOOST 1228 : 0; 1229 var extra = forecastModelHouseTossupish(detail, cookRating) 1230 ? (ET.HOUSE_FORECAST_GOP_TOSSUP_EXTRA_BOOST != null && isFinite(ET.HOUSE_FORECAST_GOP_TOSSUP_EXTRA_BOOST) 1231 ? ET.HOUSE_FORECAST_GOP_TOSSUP_EXTRA_BOOST 1232 : 0) 1233 : 0; 1234 return base + extra; 1235 } 1236 1237 function forecastModelApplyHouseGopBoost(detail, cookRating) { 1238 if (!detail) return detail; 1239 var shift = forecastModelHouseGopBoostShift(detail, cookRating); 1240 if (!shift) return detail; 1241 detail.margin = detail.margin - shift; 1242 detail.winProbD = ET.forecastModelMarginToWinProb(detail.margin); 1243 detail.winProbR = 1 - detail.winProbD; 1244 detail.rating = ET.forecastModelWinProbToRating(detail.winProbD, detail.code); 1245 detail._houseGopBoostPts = shift; 1246 return detail; 1247 } 1248 1249 ET.forecastModelApplyHouseGopBoost = forecastModelApplyHouseGopBoost; 1250 1251 /** Name-matched margin from the incumbent's previous Senate election. */ 1252 function senateIncumbentSameRacePrior(key) { 1253 var v = ET.SENATE_FORECAST_PRIOR_SENATE && ET.SENATE_FORECAST_PRIOR_SENATE[key]; 1254 return v != null && isFinite(v) ? v : null; 1255 } 1256 1257 function forecastModelStripStar(name) { 1258 return String(name || '').replace(/\*+$/, '').trim(); 1259 } 1260 1261 function forecastModelNormalizeCandName(name) { 1262 var s = forecastModelStripStar(name); 1263 if (!s) return ''; 1264 return typeof ET.normalizeCandNamePeriods === 'function' ? ET.normalizeCandNamePeriods(s) : s; 1265 } 1266 1267 function forecastModelCandMatchesIncumbent(name, incName) { 1268 var a = forecastModelNormalizeCandName(name).toLowerCase(); 1269 var b = forecastModelNormalizeCandName(incName).toLowerCase(); 1270 return !!a && !!b && a === b; 1271 } 1272 1273 function senateRunningIncumbentParty(key) { 1274 if (!key) return null; 1275 if (ET.SENATE_NO_INCUMBENT_2026 && ET.SENATE_NO_INCUMBENT_2026[key]) return null;
1276 var inc = ET.SENATE_INCUMBENT_FULL_2026 && ET.SENATE_INCUMBENT_FULL_2026[key]; 1277 if (inc && (inc.party === 'D' || inc.party === 'R')) { 1278 var incCand = typeof ET.candName === 'function' ? ET.candName(key, inc.party) : ''; 1279 return forecastModelCandMatchesIncumbent(incCand, inc.name) ? inc.party : null; 1280 } 1281 if (typeof ET.candName === 'function') { 1282 var dFull = ET.candName(key, 'D'); 1283 var rFull = ET.candName(key, 'R'); 1284 if (ET.candFullNameIsIncumbent && ET.candFullNameIsIncumbent(dFull)) return 'D'; 1285 if (ET.candFullNameIsIncumbent && ET.candFullNameIsIncumbent(rFull)) return 'R'; 1286 } 1287 return null; 1288 } 1289 1290 /** Incumbent's most recent win in this Senate seat only; only when the incumbent is on the ballot. */ 1291 function senatePriorElectionScore(key) { 1292 if (!senateRunningIncumbentParty(key)) return null; 1293 return senateIncumbentSameRacePrior(key); 1294 } 1295 1296 function senatePresAvgScore(key) { 1297 if (!ET.SENATE_FORECAST_PRES_AVG || ET.SENATE_FORECAST_PRES_AVG[key] == null) return 0; 1298 return ET.SENATE_FORECAST_PRES_AVG[key]; 1299 } 1300 1301 function senateIncumbencyScore(key) { 1302 var inc = senateRunningIncumbentParty(key); 1303 if (!inc) return null; 1304 if (key === 'NE' || key === 'MT') return 0; 1305 return inc === 'R' ? -INCUMBENCY_SCORE : INCUMBENCY_SCORE; 1306 } 1307 1308 function govRunningIncumbentParty(key) { 1309 if (!key) return null; 1310 if (ET.GOV_NO_INCUMBENT_2026 && ET.GOV_NO_INCUMBENT_2026[key]) return null; 1311 var inc = ET.GOV_INCUMBENT_FULL_2026 && ET.GOV_INCUMBENT_FULL_2026[key]; 1312 if (inc && (inc.party === 'D' || inc.party === 'R')) { 1313 var incCand = typeof ET.govCandName === 'function' ? ET.govCandName(key, inc.party) : ''; 1314 return forecastModelCandMatchesIncumbent(incCand, inc.name) ? inc.party : null; 1315 } 1316 if (typeof ET.govCandName === 'function') { 1317 var dFull = ET.govCandName(key, 'D'); 1318 var rFull = ET.govCandName(key, 'R'); 1319 if (ET.candFullNameIsIncumbent && ET.candFullNameIsIncumbent(dFull)) return 'D'; 1320 if (ET.candFullNameIsIncumbent && ET.candFullNameIsIncumbent(rFull)) return 'R'; 1321 } 1322 return null; 1323 } 1324 1325 function govIncumbencyScore(key) { 1326 var inc = govRunningIncumbentParty(key); 1327 if (!inc) return null; 1328 return inc === 'R' ? -INCUMBENCY_SCORE : INCUMBENCY_SCORE; 1329 } 1330 1331 function govIncumbentSameRacePrior(key) { 1332 var v = ET.GOV_FORECAST_PRIOR_GOV && ET.GOV_FORECAST_PRIOR_GOV[key]; 1333 return v != null && isFinite(v) ? v : null; 1334 } 1335 1336 /** Incumbent's most recent gov win in this state only; only when the incumbent is on the ballot. */ 1337 function govPriorElectionScore(key) { 1338 if (!govRunningIncumbentParty(key)) return null; 1339 return govIncumbentSameRacePrior(key); 1340 } 1341 1342 function govPresAvgScore(key) { 1343 if (!ET.GOV_FORECAST_PRES_AVG || ET.GOV_FORECAST_PRES_AVG[key] == null) return 0; 1344 return ET.GOV_FORECAST_PRES_AVG[key]; 1345 } 1346 1347 /** Normalize KS-01 â KS-1 to match Cook PVI / incumbent lookup keys. */ 1348 function houseDistrictLookupKey(code) { 1349 if (typeof ET.houseDistrictCodeForIncumbentLookup === 'function') { 1350 return ET.houseDistrictCodeForIncumbentLookup(code); 1351 } 1352 var m = /^([A-Z]{2})-(\d+|AL)$/i.exec(String(code || '').trim()); 1353 if (!m) return code; 1354 var st = m[1].toUpperCase(); 1355 var dist = m[2].toUpperCase(); 1356 if (dist === 'AL') return st + '-AL'; 1357 return st + '-' + parseInt(dist, 10); 1358 } 1359 1360 function houseIncumbencyScore(code) { 1361 var key = houseDistrictLookupKey(code); 1362 var row = ET.HOUSE_RATING_INCUMBENT_BY_CODE && ET.HOUSE_RATING_INCUMBENT_BY_CODE[key]; 1363 if (!row || row.open || !row.party) return null; 1364 if (row.party === 'R') return -INCUMBENCY_SCORE; 1365 if (row.party === 'D') return INCUMBENCY_SCORE; 1366 return null; 1367 } 1368 1369 function housePreviousElectionScore(code) { 1370 var st = houseStateKeyFromCode(code); 1371 if (st && HOUSE_PRIOR_RESULT_EXEMPT_STATES[st]) return null; 1372 var key = houseDistrictLookupKey(code); 1373 if (key && HOUSE_PREVIOUS_RESULT_BY_CODE[key] != null && isFinite(HOUSE_PREVIOUS_RESULT_BY_CODE[key])) { 1374 return HOUSE_PREVIOUS_RESULT_BY_CODE[key]; 1375 } 1376 return null; 1377 } 1378
1379 function sortPollsChrono(series) { 1380 return series.slice().sort(function (a, b) { 1381 if (a.date < b.date) return -1; 1382 if (a.date > b.date) return 1; 1383 return 0; 1384 }); 1385 } 1386 1387 function marginFromPollRows(rows) { 1388 if (!rows || !rows.length) return null; 1389 var d = 0; 1390 var r = 0; 1391 var n = 0; 1392 var i; 1393 for (i = 0; i < rows.length; i++) { 1394 if (rows[i].avgExclude) continue; 1395 if (typeof rows[i].D === 'number' && typeof rows[i].R === 'number') { 1396 d += rows[i].D; 1397 r += rows[i].R; 1398 n++; 1399 } 1400 } 1401 if (!n) return null; 1402 return d / n - r / n; 1403 } 1404 1405 function latestDrMarginFromAvgs(dAvg, rAvg) { 1406 var i; 1407 for (i = dAvg.length - 1; i >= 0; i--) { 1408 if (dAvg[i] != null && rAvg[i] != null) { 1409 return { margin: dAvg[i] - rAvg[i], dPct: dAvg[i], rPct: rAvg[i] }; 1410 } 1411 } 1412 return null; 1413 } 1414 1415 function senatePollInfoFromRows(key, pollRows, asOfIso) { 1416 if (typeof ET.senateIsForecastOnlyGe === 'function' && ET.senateIsForecastOnlyGe(key)) { 1417 return { count: 0, margin: null, dPct: null, rPct: null }; 1418 } 1419 if (!pollRows || !pollRows.length) return { count: 0, margin: null, dPct: null, rPct: null }; 1420 /* As-of path uses the exponential roll, which skips runningAvgSenateOnLabels. 1421 Apply the same state cheats (GOP boosts) the polling page adds to the modeled average. */ 1422 function withSenateCheat(info) { 1423 if (!info || info.dPct == null || info.rPct == null) return info; 1424 if (typeof ET.senateStatePollingAvgCheat !== 'function') return info; 1425 var cheat = ET.senateStatePollingAvgCheat(key); 1426 if (!cheat) return info; 1427 var dAdd = cheat.D != null && isFinite(cheat.D) ? cheat.D : 0; 1428 var rAdd = cheat.R != null && isFinite(cheat.R) ? cheat.R : 0; 1429 if (!dAdd && !rAdd) return info; 1430 var d = info.dPct + dAdd; 1431 var r = info.rPct + rAdd; 1432 return { count: info.count, margin: d - r, dPct: d, rPct: r }; 1433 } 1434 if (asOfIso) { 1435 var rollS = rollingRaceDrAtDate(pollRows, asOfIso); 1436 if (rollS) { 1437 return withSenateCheat({ 1438 count: pollRows.length, 1439 margin: rollS.margin, 1440 dPct: rollS.dPct, 1441 rPct: rollS.rPct 1442 }); 1443 } 1444 } 1445 if (ET.runningAvgSenateOnLabels && (ET.dailyRangeToTodayET || asOfIso)) { 1446 var seriesForAvg = ET.senateSeriesForAverage ? ET.senateSeriesForAverage(pollRows, key) : pollRows; 1447 var labelsPoll; 1448 if (asOfIso) { 1449 labelsPoll = dailyLabelsEndingAt(FORECAST_TREND_START, asOfIso); 1450 } else { 1451 labelsPoll = ET.dailyRangeToTodayET(pollRows); 1452 if (ET.extendLabelsTo) labelsPoll = ET.extendLabelsTo(labelsPoll, '2025-10-31'); 1453 } 1454 var dAvg = ET.runningAvgSenateOnLabels(seriesForAvg, 'D', labelsPoll, { statePageAdaptiveRecency: true, stateKey: key }); 1455 var rAvg = ET.runningAvgSenateOnLabels(seriesForAvg, 'R', labelsPoll, { statePageAdaptiveRecency: true, stateKey: key }); 1456 var pair = asOfIso 1457 ? marginFromAvgsAtDate(dAvg, rAvg, labelsPoll, asOfIso) 1458 : latestDrMarginFromAvgs(dAvg, rAvg); 1459 if (pair) return { count: pollRows.length, margin: pair.margin, dPct: pair.dPct, rPct: pair.rPct }; 1460 } 1461 var m = marginFromPollRows(pollRows); 1462 if (m != null && typeof ET.senateStatePollingAvgCheat === 'function') { 1463 var cheatM = ET.senateStatePollingAvgCheat(key); 1464 if (cheatM) m += (cheatM.D || 0) - (cheatM.R || 0); 1465 } 1466 return { count: pollRows.length, margin: m, dPct: null, rPct: null }; 1467 } 1468 1469 function govPollInfoFromRows(key, pollRows, asOfIso) { 1470 if (!pollRows || !pollRows.length) return { count: 0, margin: null, dPct: null, rPct: null }; 1471 function withGovCheat(info) { 1472 if (!info || info.dPct == null || info.rPct == null) return info; 1473 if (typeof ET.applyGovStatePollingAvgCheatToDr !== 'function') return info; 1474 var cheated = ET.applyGovStatePollingAvgCheatToDr(key, info.dPct, info.rPct); 1475 if (!cheated) return info; 1476 return { 1477 count: info.count, 1478 margin: cheated.margin, 1479 dPct: cheated.D, 1480 rPct: cheated.R 1481 }; 1482 } 1483 /* Prefer the published modeled avg (same path as hub/polling pages) so bake 1484 matches the on-page avg â including GOV_STATE_POLLING_AVG_CHEATS. */ 1485 if (ET.runningAvgOnLabels && (ET.dailyRangeToTodayET || asOfIso)) { 1486 var seriesForAvg = ET.govSeriesForGovernorPollingModel 1487 ? ET.govSeriesForGovernorPollingModel(pollRows, key) 1488 : (ET.govSeriesForAverage ? ET.govSeriesForAverage(pollRows) : pollRows); 1489 var labelsPoll; 1490 if (asOfIso) { 1491 labelsPoll = dailyLabelsEndingAt(FORECAST_TREND_START, asOfIso); 1492 } else { 1493 labelsPoll = ET.dailyRangeToTodayET(pollRows); 1494 if (ET.extendLabelsTo) labelsPoll = ET.extendLabelsTo(labelsPoll, '2025-10-31'); 1495 } 1496 /* skipStateAvgCheat: apply once via withGovCheat so as-of + live match. */ 1497 var govAvgOpts = { adaptiveRecencyPowerFromSeries: true, stateKey: key, skipStateAvgCheat: true }; 1498 var dAvg = ET.runningAvgOnLabels(seriesForAvg, 'D', labelsPoll, govAvgOpts); 1499 var rAvg = ET.runningAvgOnLabels(seriesForAvg, 'R', labelsPoll, govAvgOpts); 1500 var pair = asOfIso 1501 ? marginFromAvgsAtDate(dAvg, rAvg, labelsPoll, asOfIso) 1502 : latestDrMarginFromAvgs(dAvg, rAvg); 1503 if (pair) { 1504 return withGovCheat({ 1505 count: pollRows.length, 1506 margin: pair.margin, 1507 dPct: pair.dPct, 1508 rPct: pair.rPct 1509 }); 1510 } 1511 } 1512 if (asOfIso) { 1513 var rollG = rollingRaceDrAtDate(pollRows, asOfIso); 1514 if (rollG) { 1515 return withGovCheat({ 1516 count: pollRows.length, 1517 margin: rollG.margin, 1518 dPct: rollG.dPct, 1519 rPct: rollG.rPct 1520 }); 1521 } 1522 } 1523 var m = marginFromPollRows(pollRows); 1524 if (m != null && typeof ET.govStatePollingAvgCheat === 'function') { 1525 var cheatM = ET.govStatePollingAvgCheat(key); 1526 if (cheatM) { 1527 m += (cheatM.D || 0) - (cheatM.R || 0); 1528 } 1529 } 1530 return { count: pollRows.length, margin: m, dPct: null, rPct: null }; 1531 } 1532 1533 function housePollInfoFromRows(pollRows, pollOpts) { 1534 pollOpts = pollOpts || {}; 1535 if (!pollRows || !pollRows.length) return { count: 0, margin: null, dPct: null, rPct: null }; 1536 var asOfIso = pollOpts.asOfIso; 1537 var raceMeta = pollOpts.raceMeta; 1538 if (asOfIso) { 1539 var rollH = rollingRaceDrAtDate(pollRows, asOfIso); 1540 if (rollH) { 1541 return { count: pollRows.length, margin: rollH.margin, dPct: rollH.dPct, rPct: rollH.rPct }; 1542 } 1543 } 1544 if (asOfIso && ET.runningAvgOnLabels && ET.nextISO) { 1545 var series = pollRows.slice().sort(function (a, b) { 1546 return a.date < b.date ? -1 : (a.date > b.date ? 1 : 0); 1547 }); 1548 var race = raceMeta || { series: pollRows }; 1549 var usePartyTotals = !!(race.caOpenPrimary && ET.pollPartyTotalsDR); 1550 var modelSeries = usePartyTotals && ET.houseCaOpenPartyTotalSeries
1551 ? ET.houseCaOpenPartyTotalSeries(series) 1552 : (ET.houseSeriesForAverage ? ET.houseSeriesForAverage(series) : series); 1553 var labels = dailyLabelsEndingAt(FORECAST_TREND_START, asOfIso); 1554 var avgOpts = { statePageAdaptiveRecency: true }; 1555 var lineD = ET.runningAvgOnLabels(modelSeries, 'D', labels, avgOpts); 1556 var lineR = ET.runningAvgOnLabels(modelSeries, 'R', labels, avgOpts); 1557 var pair = marginFromAvgsAtDate(lineD, lineR, labels, asOfIso); 1558 if (pair) return { count: pollRows.length, margin: pair.margin, dPct: pair.dPct, rPct: pair.rPct }; 1559 } 1560 if (!asOfIso && ET.houseLatestAvg && pollRows.length) { 1561 var tracked = raceMeta || { series: pollRows }; 1562 var avg = ET.houseLatestAvg(tracked); 1563 if (avg && avg.margin != null) { 1564 var dPct = avg.D != null ? avg.D : (avg.margin >= 0 ? 50 + avg.margin / 2 : 50 - Math.abs(avg.margin) / 2); 1565 var rPct = avg.R != null ? avg.R : 100 - dPct; 1566 return { count: pollRows.length, margin: avg.margin, dPct: dPct, rPct: rPct }; 1567 } 1568 } 1569 var m = marginFromPollRows(pollRows); 1570 return { count: pollRows.length, margin: m, dPct: null, rPct: null }; 1571 } 1572 1573 ET.forecastModelSenatePollInfoAtCount = function (key, pollCount) { 1574 if (!ET.SENATE || !key || !ET.SENATE[key] || !ET.SENATE[key].length) return { count: 0, margin: null }; 1575 var sorted = sortPollsChrono(ET.SENATE[key]); 1576 var n = typeof pollCount === 'number' && pollCount >= 0 1577 ? Math.min(pollCount, sorted.length) 1578 : sorted.length; 1579 var subset = sorted.slice(0, n); 1580 return { count: n, margin: marginFromPollRows(subset) }; 1581 }; 1582 1583 function senatePollInfo(key, name) { 1584 if (typeof ET.senateIsForecastOnlyGe === 'function' && ET.senateIsForecastOnlyGe(key)) { 1585 return { count: 0, margin: null, dPct: null, rPct: null }; 1586 } 1587 if (!ET.SENATE || !key || !ET.SENATE[key] || !ET.SENATE[key].length) return { count: 0, margin: null }; 1588 var at = ET.forecastModelSenatePollInfoAtCount(key, ET.SENATE[key].length); 1589 if (at.count && at.margin != null) return at; 1590 var avg = ET.latestAvgByName && name ? ET.latestAvgByName(name) : null; 1591 return { count: ET.SENATE[key].length, margin: avg ? avg.margin : null }; 1592 } 1593 1594 ET.forecastModelGovPollInfoAtCount = function (key, pollCount) { 1595 var series = ET.govSeriesWithLocalFallback ? ET.govSeriesWithLocalFallback(key) : (ET.govSeries ? ET.govSeries(key) : []); 1596 if (!series || !series.length) return { count: 0, margin: null }; 1597 var sorted = sortPollsChrono(series); 1598 var n = typeof pollCount === 'number' && pollCount >= 0 1599 ? Math.min(pollCount, sorted.length) 1600 : sorted.length; 1601 if (typeof ET.isForecastRacePageView === 'function' && ET.isForecastRacePageView() && 1602 typeof ET.latestGovAvgForForecast === 'function') { 1603 var fc = ET.latestGovAvgForForecast(key); 1604 if (fc && fc.D != null && fc.R != null && fc.margin != null) { 1605 return { count: n, margin: fc.margin, dPct: fc.D, rPct: fc.R }; 1606 } 1607 } 1608 return { count: n, margin: marginFromPollRows(sorted.slice(0, n)) }; 1609 }; 1610 1611 function govPollInfo(key) { 1612 var at = ET.forecastModelGovPollInfoAtCount(key, null); 1613 if (at.count) return at; 1614 var series = ET.govSeriesWithLocalFallback ? ET.govSeriesWithLocalFallback(key) : (ET.govSeries ? ET.govSeries(key) : []); 1615 if (!series || !series.length) return { count: 0, margin: null }; 1616 var avg = ET.latestGovAvgForHub ? ET.latestGovAvgForHub(key) : (ET.latestGovAvgByKey ? ET.latestGovAvgByKey(key) : null); 1617 var margin = null; 1618 if (avg) { 1619 if (key === 'MI' && avg.leader && avg.margin != null) { 1620 margin = avg.leader === 'D' ? Math.abs(avg.margin) : -Math.abs(avg.margin); 1621 } else if (avg.D != null && avg.R != null) margin = avg.D - avg.R; 1622 } 1623 return { count: series.length, margin: margin }; 1624 } 1625 1626 ET.forecastModelGovDetailWithPollRows = function (key, pollRows, opts) { 1627 opts = opts || {}; 1628 var env = opts.env || ET.forecastModelNationalEnvironment(); 1629 var pvi = ET.COOK_STATE_PVI && ET.COOK_STATE_PVI[key] != null ? ET.COOK_STATE_PVI[key] : 0; 1630 var cookRating = govEditorialRatingForModel(key); 1631 var cookM = ET.forecastModelGovEditorialBlendMargin(key, cookRating); 1632 var poll = govPollInfoFromRows(key, pollRows || [], opts.asOfIso); 1633 var inc = govIncumbencyScore(key); 1634 var prior = govPriorElectionScore(key); 1635 var components = { 1636 pvi: pvi, 1637 generic: env.genericMargin, 1638 trump: env.trumpProD, 1639 incumbency: inc, 1640 priorGov: prior, 1641 presAvg: govPresAvgScore(key), 1642 cook: cookM 1643 }; 1644 var detail = ET.forecastModelGovBlend(components, poll.margin, poll.count, { 1645 cookRating: cookRating, 1646 stateKey: key 1647 }); 1648 detail.hasPriorGov = prior != null; 1649 detail.hasIncumbency = inc != null; 1650 detail.pollDPct = poll.dPct; 1651 detail.pollRPct = poll.rPct; 1652 return detail; 1653 }; 1654 1655 ET.forecastModelGovDetailAtPollCount = function (key, pollCount) { 1656 var env = ET.forecastModelNationalEnvironment(); 1657 var pvi = ET.COOK_STATE_PVI && ET.COOK_STATE_PVI[key] != null ? ET.COOK_STATE_PVI[key] : 0; 1658 var cookRating = govEditorialRatingForModel(key); 1659 var cookM = ET.forecastModelGovEditorialBlendMargin(key, cookRating); 1660 var poll = ET.forecastModelGovPollInfoAtCount(key, pollCount); 1661 var inc = govIncumbencyScore(key); 1662 var prior = govPriorElectionScore(key); 1663 var components = { 1664 pvi: pvi, 1665 generic: env.genericMargin, 1666 trump: env.trumpProD, 1667 incumbency: inc, 1668 priorGov: prior, 1669 presAvg: govPresAvgScore(key), 1670 cook: cookM 1671 }; 1672 var detail = ET.forecastModelGovBlend(components, poll.margin, poll.count, { 1673 cookRating: cookRating, 1674 stateKey: key 1675 }); 1676 detail.hasPriorGov = prior != null; 1677 detail.hasIncumbency = inc != null; 1678 return detail; 1679 }; 1680 1681 ET.forecastModelHousePollInfoAtCount = function (code, pollCount) { 1682 var row = ET.HOUSE_RATING_BY_CODE && ET.HOUSE_RATING_BY_CODE[code]; 1683 var tracked = row && row.geoid && ET.HOUSE_BY_GEOID ? ET.HOUSE_BY_GEOID[row.geoid] : null; 1684 if (!tracked || !tracked.series || !tracked.series.length) return { count: 0, margin: null }; 1685 var sorted = sortPollsChrono(tracked.series); 1686 var n = typeof pollCount === 'number' && pollCount >= 0 1687 ? Math.min(pollCount, sorted.length) 1688 : sorted.length; 1689 return { count: n, margin: marginFromPollRows(sorted.slice(0, n)) }; 1690 }; 1691 1692 function housePollInfo(code) { 1693 var at = ET.forecastModelHousePollInfoAtCount(code, null); 1694 if (at.count) return at; 1695 var row = ET.HOUSE_RATING_BY_CODE && ET.HOUSE_RATING_BY_CODE[code]; 1696 var tracked = row && row.geoid && ET.HOUSE_BY_GEOID ? ET.HOUSE_BY_GEOID[row.geoid] : null; 1697 if (!tracked || !tracked.series || !tracked.series.length) return { count: 0, margin: null }; 1698 var avg = ET.houseLatestAvg ? ET.houseLatestAvg(tracked) : null; 1699 return { count: tracked.series.length, margin: avg && avg.margin != null ? avg.margin : null }; 1700 } 1701 1702 ET.forecastModelHouseDetailWithPollRows = function (code, pollRows, opts) { 1703 opts = opts || {}; 1704 var env = opts.env || ET.forecastModelNationalEnvironment(); 1705 var houseSt = houseStateKeyFromCode(code); 1706 var pvi = ET.forecastModelHouseDistrictPvi(code); 1707 var cookRating = ET.COOK_HOUSE_RATINGS && ET.COOK_HOUSE_RATINGS[code]; 1708 var cookM = ET.forecastModelHouseCookBlendMargin(code, cookRating); 1709 var poll = housePollInfoFromRows(pollRows || [], { 1710 asOfIso: opts.asOfIso, 1711 raceMeta: opts.raceMeta 1712 }); 1713 var inc = houseIncumbencyScore(code); 1714 var prior = housePreviousElectionScore(code); 1715 var components = { 1716 pvi: pvi, 1717 generic: env.genericMargin, 1718 trump: env.trumpProD, 1719 incumbency: inc, 1720 priorHouse: prior, 1721 cook: cookM 1722 }; 1723 var detail = ET.forecastModelHouseBlend(components, poll.margin, poll.count, { 1724 cookRating: cookRating, 1725 stateKey: houseSt 1726 }); 1727 detail.hasIncumbency = inc != null; 1728 detail.hasPriorHouse = prior != null; 1729 detail.pollDPct = poll.dPct; 1730 detail.pollRPct = poll.rPct; 1731 return detail; 1732 }; 1733 1734 ET.forecastModelHouseDetailAtPollCount = function (code, pollCount) { 1735 var env = ET.forecastModelNationalEnvironment(); 1736 var houseSt = houseStateKeyFromCode(code); 1737 var pvi = ET.forecastModelHouseDistrictPvi(code); 1738 var cookRating = ET.COOK_HOUSE_RATINGS && ET.COOK_HOUSE_RATINGS[code]; 1739 var cookM = ET.forecastModelHouseCookBlendMargin(code, cookRating); 1740 var poll = ET.forecastModelHousePollInfoAtCount(code, pollCount); 1741 var inc = houseIncumbencyScore(code); 1742 var prior = housePreviousElectionScore(code); 1743 var components = { 1744 pvi: pvi, 1745 generic: env.genericMargin, 1746 trump: env.trumpProD, 1747 incumbency: inc, 1748 priorHouse: prior, 1749 cook: cookM 1750 }; 1751 var detail = ET.forecastModelHouseBlend(components, poll.margin, poll.count, { 1752 cookRating: cookRating, 1753 stateKey: houseSt 1754 }); 1755 detail.hasIncumbency = inc != null; 1756 detail.hasPriorHouse = prior != null; 1757 return detail; 1758 }; 1759 1760 ET.forecastModelSenateDetailWithPollRows = function (key, name, pollRows, opts) { 1761 opts = opts || {}; 1762 if (typeof ET.senateIsForecastOnlyGe === 'function' && ET.senateIsForecastOnlyGe(key)) { 1763 pollRows = []; 1764 } 1765 var env = opts.env || ET.forecastModelNationalEnvironment(); 1766 var pvi = ET.COOK_STATE_PVI && ET.COOK_STATE_PVI[key] != null ? ET.COOK_STATE_PVI[key] : 0; 1767 var cookRating = senateEditorialRatingForModel(key, opts.asOfIso); 1768 var cookM = ET.forecastModelSenateEditorialBlendMargin(key, cookRating);
1769 var poll = senatePollInfoFromRows(key, pollRows || [], opts.asOfIso); 1770 var inc = senateIncumbencyScore(key); 1771 var prior = senatePriorElectionScore(key); 1772 var components = { 1773 pvi: pvi, 1774 generic: env.genericMargin, 1775 trump: env.trumpProD, 1776 incumbency: inc, 1777 priorSenate: prior, 1778 presAvg: senatePresAvgScore(key), 1779 cook: cookM 1780 }; 1781 var detail = ET.forecastModelSenateBlend(components, poll.margin, poll.count, { 1782 cookRating: cookRating, 1783 stateKey: key 1784 }); 1785 detail.hasPriorSenate = prior != null; 1786 detail.hasIncumbency = inc != null; 1787 detail.pollDPct = poll.dPct; 1788 detail.pollRPct = poll.rPct; 1789 return detail; 1790 }; 1791 1792 ET.forecastModelSenateDetailAtPollCount = function (key, name, pollCount) { 1793 var env = ET.forecastModelNationalEnvironment(); 1794 var pvi = ET.COOK_STATE_PVI && ET.COOK_STATE_PVI[key] != null ? ET.COOK_STATE_PVI[key] : 0; 1795 var cookRating = senateEditorialRatingForModel(key); 1796 var cookM = ET.forecastModelSenateEditorialBlendMargin(key, cookRating); 1797 var poll = ET.forecastModelSenatePollInfoAtCount(key, pollCount); 1798 var inc = senateIncumbencyScore(key); 1799 var prior = senatePriorElectionScore(key); 1800 var components = { 1801 pvi: pvi, 1802 generic: env.genericMargin, 1803 trump: env.trumpProD, 1804 incumbency: inc, 1805 priorSenate: prior, 1806 presAvg: senatePresAvgScore(key), 1807 cook: cookM 1808 }; 1809 var detail = ET.forecastModelSenateBlend(components, poll.margin, poll.count, { 1810 cookRating: cookRating, 1811 stateKey: key 1812 }); 1813 detail.hasPriorSenate = prior != null; 1814 detail.hasIncumbency = inc != null; 1815 return detail; 1816 }; 1817 1818 ET.forecastModelSenateDetail = function (key, name) { 1819 if (typeof ET.senateIsForecastOnlyGe === 'function' && ET.senateIsForecastOnlyGe(key)) { 1820 return ET.forecastModelSenateDetailAtPollCount(key, name, 0); 1821 } 1822 var env = ET.forecastModelNationalEnvironment(); 1823 var pvi = ET.COOK_STATE_PVI && ET.COOK_STATE_PVI[key] != null ? ET.COOK_STATE_PVI[key] : 0; 1824 var cookRating = senateEditorialRatingForModel(key); 1825 var cookM = ET.forecastModelSenateEditorialBlendMargin(key, cookRating); 1826 var poll = senatePollInfo(key, name); 1827 var inc = senateIncumbencyScore(key); 1828 var prior = senatePriorElectionScore(key); 1829 var components = { 1830 pvi: pvi, 1831 generic: env.genericMargin, 1832 trump: env.trumpProD, 1833 incumbency: inc, 1834 priorSenate: prior, 1835 presAvg: senatePresAvgScore(key), 1836 cook: cookM 1837 }; 1838 var detail = ET.forecastModelSenateBlend(components, poll.margin, poll.count, { 1839 cookRating: cookRating, 1840 stateKey: key 1841 }); 1842 detail.hasPriorSenate = prior != null; 1843 detail.hasIncumbency = inc != null; 1844 return detail; 1845 }; 1846 1847 ET.forecastModelSenateRating = function (key, name) { 1848 return ET.forecastModelSenateDetail(key, name).rating; 1849 }; 1850 1851 ET.forecastModelGovDetail = function (key) { 1852 var env = ET.forecastModelNationalEnvironment(); 1853 var pvi = ET.COOK_STATE_PVI && ET.COOK_STATE_PVI[key] != null ? ET.COOK_STATE_PVI[key] : 0; 1854 var cookRating = govEditorialRatingForModel(key); 1855 var cookM = ET.forecastModelGovEditorialBlendMargin(key, cookRating); 1856 var poll = govPollInfo(key); 1857 var inc = govIncumbencyScore(key); 1858 var prior = govPriorElectionScore(key); 1859 var components = { 1860 pvi: pvi, 1861 generic: env.genericMargin, 1862 trump: env.trumpProD, 1863 incumbency: inc, 1864 priorGov: prior, 1865 presAvg: govPresAvgScore(key), 1866 cook: cookM 1867 }; 1868 var detail = ET.forecastModelGovBlend(components, poll.margin, poll.count, { 1869 cookRating: cookRating, 1870 stateKey: key 1871 }); 1872 detail.hasPriorGov = prior != null; 1873 detail.hasIncumbency = inc != null; 1874 return detail; 1875 }; 1876 1877 ET.forecastModelGovRating = function (key) { 1878 return ET.forecastModelGovDetail(key).rating; 1879 }; 1880 1881 /** House forecast scales raw Cook district PVI before blend (Senate/state PVI unchanged). */ 1882 ET.HOUSE_FORECAST_PVI_SCALE = 1.5; 1883 1884 /** 1885 * District PVI for House forecast. Tries padded code (KS-01), unpadded Cook key (KS-1), 1886 * then statewide PVI for -AL seats. Raw Cook values are scaled by HOUSE_FORECAST_PVI_SCALE. 1887 */ 1888 ET.forecastModelHouseDistrictPvi = function (code) { 1889 var d = ET.COOK_DISTRICT_PVI; 1890 if (!d) return 0; 1891 var raw = null; 1892 if (d[code] != null) raw = d[code]; 1893 else { 1894 var key = houseDistrictLookupKey(code); 1895 if (key && d[key] != null) raw = d[key]; 1896 else if (code && /-AL$/i.test(code) && ET.COOK_STATE_PVI) { 1897 var st = code.slice(0, 2).toUpperCase(); 1898 if (ET.COOK_STATE_PVI[st] != null) raw = ET.COOK_STATE_PVI[st]; 1899 } 1900 } 1901 if (raw == null || !isFinite(raw)) return 0; 1902 var scale = ET.HOUSE_FORECAST_PVI_SCALE != null && isFinite(ET.HOUSE_FORECAST_PVI_SCALE) 1903 ? ET.HOUSE_FORECAST_PVI_SCALE 1904 : 1; 1905 return raw * scale; 1906 }; 1907 1908 ET.forecastModelHouseDetail = function (code) { 1909 var env = ET.forecastModelNationalEnvironment(); 1910 var houseSt = houseStateKeyFromCode(code); 1911 var pvi = ET.forecastModelHouseDistrictPvi(code); 1912 var cookRating = ET.COOK_HOUSE_RATINGS && ET.COOK_HOUSE_RATINGS[code]; 1913 var cookM = ET.forecastModelHouseCookBlendMargin(code, cookRating); 1914 var poll = housePollInfo(code); 1915 var inc = houseIncumbencyScore(code); 1916 var prior = housePreviousElectionScore(code); 1917 var components = { 1918 pvi: pvi, 1919 generic: env.genericMargin, 1920 trump: env.trumpProD, 1921 incumbency: inc, 1922 priorHouse: prior, 1923 cook: cookM 1924 }; 1925 var detail = ET.forecastModelHouseBlend(components, poll.margin, poll.count, { 1926 cookRating: cookRating, 1927 stateKey: houseSt 1928 }); 1929 detail.hasIncumbency = inc != null; 1930 detail.hasPriorHouse = prior != null; 1931 detail.code = code; 1932 return detail; 1933 }; 1934 1935 ET.forecastModelHouseRating = function (code) { 1936 return ET.forecastModelHouseDetail(code).rating; 1937 }; 1938 1939 ET.forecastModelNationalEnvironmentStrictBeforeForTrend = function (asOfIso) { 1940 var gbAll = typeof window !== 'undefined' ? window.GENERIC_BALLOT_POLLS : null; 1941 var taAll = typeof window !== 'undefined' ? window.TRUMP_APPROVAL_POLLS : null; 1942 var gb = asOfIso ? pollsStrictlyBeforeDate(gbAll, asOfIso) : gbAll; 1943 var ta = asOfIso ? pollsStrictlyBeforeDate(taAll, asOfIso) : taAll; 1944 return nationalEnvironmentFromPolls(gb, ta, asOfIso, nationalMeanAsOfForTrend); 1945 }; 1946 1947 /** 1948 * @param {string} key 1949 * @param {string} name 1950 * @param {string} asOfIso 1951 * @param {{ pollsBeforeDateOnly?: boolean }} [detailOpts] 1952 */ 1953 ET.forecastModelSenateDetailAsOfDate = function (key, name, asOfIso, detailOpts) { 1954 detailOpts = detailOpts || {}; 1955 var env = detailOpts.pollsBeforeDateOnly 1956 ? ET.forecastModelNationalEnvironmentStrictBeforeForTrend(asOfIso) 1957 : ET.forecastModelNationalEnvironmentAsOfForTrend(asOfIso); 1958 if (typeof ET.senateIsForecastOnlyGe === 'function' && ET.senateIsForecastOnlyGe(key)) { 1959 return ET.forecastModelSenateDetailWithPollRows(key, name, [], { env: env, asOfIso: asOfIso }); 1960 } 1961 var all = ET.SENATE && key && ET.SENATE[key] ? ET.SENATE[key] : []; 1962 var available = detailOpts.pollsBeforeDateOnly 1963 ? racePollsStrictlyBeforeDate(all, asOfIso) 1964 : racePollsOnOrBeforeDate(all, asOfIso); 1965 return ET.forecastModelSenateDetailWithPollRows(key, name, available, { env: env, asOfIso: asOfIso }); 1966 }; 1967 1968 ET.forecastModelGovDetailAsOfDate = function (key, asOfIso, detailOpts) { 1969 detailOpts = detailOpts || {}; 1970 var series = ET.govSeriesWithLocalFallback 1971 ? ET.govSeriesWithLocalFallback(key) 1972 : (ET.govSeries ? ET.govSeries(key) : []); 1973 var available = detailOpts.pollsBeforeDateOnly 1974 ? racePollsStrictlyBeforeDate(series || [], asOfIso) 1975 : racePollsOnOrBeforeDate(series || [], asOfIso); 1976 var env = detailOpts.pollsBeforeDateOnly 1977 ? ET.forecastModelNationalEnvironmentStrictBeforeForTrend(asOfIso) 1978 : ET.forecastModelNationalEnvironmentAsOfForTrend(asOfIso); 1979 return ET.forecastModelGovDetailWithPollRows(key, available, { env: env, asOfIso: asOfIso }); 1980 }; 1981 1982 ET.forecastModelHouseDetailAsOfDate = function (code, asOfIso) { 1983 var lockParty = typeof ET.housePartyLockoutParty === 'function' ? ET.housePartyLockoutParty(code) : null; 1984 if (lockParty && typeof ET.housePartyLockoutForecastDetail === 'function') { 1985 return ET.housePartyLockoutForecastDetail(code, lockParty); 1986 } 1987 var row = ET.HOUSE_RATING_BY_CODE && ET.HOUSE_RATING_BY_CODE[code]; 1988 var tracked = row && row.geoid && ET.HOUSE_BY_GEOID ? ET.HOUSE_BY_GEOID[row.geoid] : null; 1989 var all = tracked && tracked.series ? tracked.series : []; 1990 var available = racePollsOnOrBeforeDate(all, asOfIso); 1991 var env = ET.forecastModelNationalEnvironmentAsOfForTrend(asOfIso); 1992 return ET.forecastModelHouseDetailWithPollRows(code, available, { 1993 env: env, 1994 asOfIso: asOfIso, 1995 raceMeta: tracked 1996 }); 1997 }; 1998 1999 /** 2000 * Full forecast blend for a race page context on a calendar day (ET). 2001 * ctx: { kind: 'senate'|'gov'|'house', stateKey, houseRace } 2002 */ 2003 ET.forecastModelRaceDetailAsOf = function (ctx, asOfIso) { 2004 if (!ctx || !asOfIso) return null; 2005 if (ctx.kind === 'senate') {
2006 var nm = ET.NAME_BY_KEY && ET.NAME_BY_KEY[ctx.stateKey]; 2007 if (!nm) return null; 2008 return ET.forecastModelSenateDetailAsOfDate(ctx.stateKey, nm, asOfIso); 2009 } 2010 if (ctx.kind === 'gov') { 2011 return ET.forecastModelGovDetailAsOfDate(ctx.stateKey, asOfIso); 2012 } 2013 if (ctx.kind === 'house' && ctx.houseRace) { 2014 var row = typeof ET.houseRatingRowForRace === 'function' 2015 ? ET.houseRatingRowForRace(ctx.houseRace) 2016 : null; 2017 var codeH = row && row.code ? row.code : ctx.houseRace.code; 2018 if (codeH && typeof ET.houseDistrictCodeForIncumbentLookup === 'function') { 2019 codeH = ET.houseDistrictCodeForIncumbentLookup(codeH) || codeH; 2020 } 2021 if (codeH) return ET.forecastModelHouseDetailAsOfDate(codeH, asOfIso); 2022 } 2023 return null; 2024 }; 2025 2026 /** Daily trend series â same composite as headline forecast and simulation histogram. */ 2027 ET.forecastModelRaceDetailAsOfForTrend = function (ctx, asOfIso) { 2028 return ET.forecastModelRaceDetailAsOf(ctx, asOfIso); 2029 }; 2030 2031 var FORECAST_SIM_DRAW_COUNT = 100000; 2032 var FORECAST_SIM_HOUSE_DRAW_COUNT = 10000; 2033 ET.FORECAST_SIM_HOUSE_DRAW_COUNT = FORECAST_SIM_HOUSE_DRAW_COUNT; 2034 var _forecastModelSimCache = {}; 2035 2036 /** 2037 * Per-race margin Ï from calibrated distribution (used for trend bands when sim has run). 2038 */ 2039 function forecastSimulationSigma(pollCount, det) { 2040 if (det && det.simSigma != null && isFinite(det.simSigma)) return det.simSigma; 2041 if (typeof ET.forecastSimParamsFromDetail === 'function') { 2042 var p = ET.forecastSimParamsFromDetail(det, det.forecastKind || 'house', { pollCount: pollCount }); 2043 if (p && p.sigma != null) return p.sigma; 2044 } 2045 return 7; 2046 } 2047 2048 function forecastSimulationNationalNoiseScale(winProbD, pollCount) { 2049 return 1; 2050 } 2051 2052 ET.forecastSimulationSigma = forecastSimulationSigma; 2053 ET.forecastModelNormalInv = null; 2054 2055 function forecastModelNormalCdf(z) { 2056 if (z < -8) return 0; 2057 if (z > 8) return 1; 2058 var t = 1 / (1 + 0.2316419 * Math.abs(z)); 2059 var d = 0.3989423 * Math.exp(-z * z / 2); 2060 var p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.7814779 + t * (-1.8212560 + t * 1.3302744)))); 2061 return z > 0 ? 1 - p : p; 2062 } 2063 2064 /** Acklam rational approximation for Φâ»Â¹(p), p â (0, 1). */ 2065 function forecastModelNormalInv(p) { 2066 p = Math.max(1e-12, Math.min(1 - 1e-12, p)); 2067 var a = [ 2068 -3.969683028665376e+01, 2.209460984245205e+02, 2069 -2.759285104469138e+02, 1.383577518672690e+02, 2070 -3.066479806614716e+01, 2.506628277459239e+00 2071 ]; 2072 var b = [ 2073 -5.447738719110352e+01, 1.615858368580409e+02, 2074 -1.556989962850169e+02, 6.680131188771972e+01, 2075 -1.328068155288572e+01 2076 ]; 2077 var c = [ 2078 -7.784894002430293e-03, -3.224671290700398e-01, 2079 -2.445806658489466e+00, -2.272964305932520e+00, 2080 4.584458827487364e+00, 2.938163982698053e+00 2081 ]; 2082 var d = [ 2083 7.784695709041462e-03, 3.224671290700398e-01, 2084 2.445806658489466e+00, 2.272964305932520e+00 2085 ]; 2086 var q; 2087 var r; 2088 var i; 2089 if (p < 0.02425) { 2090 q = Math.sqrt(-2 * Math.log(p)); 2091 r = (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / 2092 ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1); 2093 return r; 2094 } 2095 if (p > 1 - 0.02425) { 2096 q = Math.sqrt(-2 * Math.log(1 - p)); 2097 r = -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) / 2098 ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1); 2099 return r; 2100 } 2101 q = p - 0.5; 2102 r = q * q; 2103 var num = (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q; 2104 var den = (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1); 2105 return num / den; 2106 } 2107 2108 ET.forecastModelNormalInv = forecastModelNormalInv; 2109 2110 (function ensureForecastCalibrationFallback() { 2111 if (typeof ET.forecastProbabilityToTargetMargin === 'function' && 2112 typeof ET.forecastSimParamsFromDetail === 'function') { 2113 return; 2114 } 2115 })(); 2116 2117 function forecastModelHash32(str) { 2118 var h = 2166136261; 2119 var i; 2120 for (i = 0; i < str.length; i++) { 2121 h ^= str.charCodeAt(i); 2122 h = Math.imul(h, 16777619); 2123 } 2124 return h >>> 0; 2125 } 2126 2127 function forecastModelMulberry32(seed) { 2128 return function () { 2129 seed |= 0; 2130 seed = (seed + 0x6d2b79f5) | 0; 2131 var t = Math.imul(seed ^ (seed >>> 15), 1 | seed); 2132 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; 2133 return ((t ^ (t >>> 14)) >>> 0) / 4294967296; 2134 }; 2135 } 2136 2137 function forecastModelSeededRandn(rnd) { 2138 var u = rnd(); 2139 while (u === 0) u = rnd(); 2140 var v = rnd(); 2141 while (v === 0) v = rnd(); 2142 return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); 2143 } 2144 2145 function forecastModelSimDrawCenter(det, pollMargin, rnd, nationalScale) { 2146 var nat = nationalScale == null ? 1 : nationalScale; 2147 var genNoise = forecastModelSeededRandn(rnd) * NATIONAL_SIM_GENERIC_SIGMA * nat; 2148 var trumpNoise = forecastModelSeededRandn(rnd) * NATIONAL_SIM_TRUMP_SIGMA * nat; 2149 if (det && det.components && det.weights) { 2150 return ET.forecastModelBlendMargin(det.components, det.weights, pollMargin, { 2151 genericNoise: genNoise, 2152 trumpNoise: trumpNoise 2153 }); 2154 } 2155 return det && det.margin != null && isFinite(det.margin) ? det.margin : 0; 2156 } 2157 2158 function forecastModelSimCacheKey(det, seedKey, pollCount, drawCount) { 2159 var c = det && det.components ? det.components : {}; 2160 var pollM = ET.forecastModelPollMarginFromDetail(det); 2161 var pm = pollM != null && isFinite(pollM) ? pollM : 'np'; 2162 var cm = det && det.margin != null && isFinite(det.margin) ? det.margin.toFixed(4) : '0'; 2163 return seedKey + '|' + pollCount + '|' + drawCount + '|simMarginCal:v3|' + cm + '|' + 2164 ((c.generic || 0).toFixed(4)) + '|' + ((c.trump || 0).toFixed(4)) + '|' + 2165 ((c.cook != null && isFinite(c.cook)) ? c.cook.toFixed(4) : '
2165nc') + '|' + pm; 2166 } 2167 2168 function forecastModelMedianOfSorted(sorted) { 2169 var n = sorted.length; 2170 if (!n) return 0; 2171 var mid = Math.floor(n / 2); 2172 if (n % 2) return sorted[mid]; 2173 return (sorted[mid - 1] + sorted[mid]) / 2; 2174 } 2175 2176 function forecastModelPercentileSorted(sorted, q) { 2177 var n = sorted.length; 2178 if (!n) return 0; 2179 var idx = (n - 1) * q; 2180 var lo = Math.floor(idx); 2181 var hi = Math.ceil(idx); 2182 if (lo === hi) return sorted[lo]; 2183 return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo); 2184 } 2185 2186 /** 2187 * Per-race MC: margin ~ Normal(calibratedCenter, Ï); D wins iff margin > 0. 2188 * Win % and projected margin come from the same simulatedMargin array. 2189 * @param {{ seedKey: string, pollCount?: number, drawCount?: number, keepMargins?: boolean }} opts 2190 */ 2191 ET.forecastModelSimAggregateForDetail = function (det, opts) { 2192 opts = opts || {}; 2193 if (!det) { 2194 return { 2195 margin: 0, 2196 marginMedian: 0, 2197 winProbD: 0.5, 2198 margins: null, 2199 dWon: null, 2200 compositeMargin: 0, 2201 pollCount: 0, 2202 targetWinD: 0.5, 2203 centerMargin: 0, 2204 sigma: 7 2205 }; 2206 } 2207 var drawCount = opts.drawCount == null ? FORECAST_SIM_DRAW_COUNT : opts.drawCount; 2208 var pollCount = opts.pollCount == null ? 0 : opts.pollCount; 2209 var seedKey = opts.seedKey || 'forecast|sim'; 2210 var cacheKey = forecastModelSimCacheKey(det, seedKey, pollCount, drawCount); 2211 var cached = _forecastModelSimCache[cacheKey]; 2212 if (cached && (!opts.keepMargins || (cached.margins && cached.margins.length))) return cached; 2213 2214 var chamber = opts.chamber || det.forecastKind || 'house'; 2215 var simParams = typeof ET.forecastSimParamsFromDetail === 'function' 2216 ? ET.forecastSimParamsFromDetail(det, chamber, { 2217 pollCount: pollCount, 2218 pureCalibration: !!opts.pureCalibration, 2219 targetWinD: opts.targetWinD 2220 }) 2221 : (function () { 2222 var cm = det && det.margin != null && isFinite(det.margin) ? det.margin : 0; 2223 var tw = ET.forecastModelMarginToWinProb(cm); 2224 return { 2225 targetWinD: tw, 2226 centerMargin: cm, 2227 finalTargetMargin: cm, 2228 sigma: 7, 2229 favoredProb: tw >= 0.5 ? tw : 1 - tw, 2230 calibratedMarginFromOdds: cm, 2231 originalCompositeMargin: cm, 2232 volatilityMultiplier: 1 2233 }; 2234 })(); 2235 var center = simParams.finalTargetMargin != null ? simParams.finalTargetMargin : simParams.centerMargin; 2236 var sigma = simParams.sigma; 2237 var targetWinD = simParams.targetWinD; 2238 var rnd = forecastModelMulberry32(forecastModelHash32(seedKey)); 2239 var sumM = 0; 2240 var sumWin = 0; 2241 var marginBuf = []; 2242 var i; 2243 var draw; 2244 for (i = 0; i < drawCount; i++) { 2245 draw = center + forecastModelSeededRandn(rnd) * sigma; 2246 marginBuf.push(draw); 2247 sumM += draw; 2248 if (draw > 0) sumWin += 1; 2249 } 2250 var sorted = marginBuf.slice().sort(function (a, b) { return a - b; }); 2251 var marginMean = sumM / drawCount; 2252 var marginMedian = forecastModelMedianOfSorted(sorted); 2253 var margins = opts.keepMargins ? marginBuf : null; 2254 var dWon = opts.keepMargins ? marginBuf.map(function (m) { return m > 0 ? 1 : 0; }) : null; 2255 var absMed = Math.abs(marginMedian); 2256 var out = { 2257 margin: marginMean, 2258 marginMedian: marginMedian, 2259 winProbD: sumWin / drawCount, 2260 margins: margins, 2261 dWon: dWon, 2262 compositeMargin: simParams.originalCompositeMargin, 2263 targetWinD: targetWinD, 2264 centerMargin: center, 2265 finalTargetMargin: center, 2266 calibratedMarginFromOdds: simParams.calibratedMarginFromOdds, 2267 originalCompositeMargin: simParams.originalCompositeMargin, 2268 originalCompositeMarginAdjusted: simParams.originalCompositeMarginAdjusted, 2269 sigma: sigma, 2270 volatilityMultiplier: simParams.volatilityMultiplier, 2271 chamber: chamber, 2272 marginExceeded25: absMed > 25.05 || Math.max.apply(null, marginBuf.map(Math.abs)) > 25.05, 2273 appearsCappedAt25: absMed > 24.95 && absMed < 25.05 && Math.abs(marginMean - marginMedian) < 0.15, 2274 pollCount: pollCount, 2275 p05: sorted && sorted.length ? forecastModelPercentileSorted(sorted, 0.05) : null, 2276 p25: sorted && sorted.length ? forecastModelPercentileSorted(sorted, 0.25) : null, 2277 p75: sorted && sorted.length ? forecastModelPercentileSorted(sorted, 0.75) : null, 2278 p95: sorted.length ? forecastModelPercentileSorted(sorted, 0.95) : null 2279 }; 2280 _forecastModelSimCache[cacheKey] = out; 2281 return out; 2282 }; 2283 2284 /** @deprecated Alias â production uses margin-first calibrated draws (same as aggregate). */ 2285 ET.forecastModelSimAggregateUnconditionalForDetail = function (det, opts) { 2286 return ET.forecastModelSimAggregateForDetail(det, opts); 2287 }; 2288 2289 ET.FORECAST_SIM_METHODOLOGY = { 2290 production: { 2291 description: 'Normal(finalTargetMargin, Ï): D wins if margin > 0; finalTarget blends odds-calibrated margin with composite fundamentals', 2292 senateDrawCount: FORECAST_SIM_DRAW_COUNT, 2293 governorDrawCount: FORECAST_SIM_DRAW_COUNT, 2294 houseRacePageDrawCount: FORECAST_SIM_HOUSE_DRAW_COUNT, 2295 houseHubDailyBundleDrawCount: FORECAST_SIM_HOUSE_DRAW_COUNT,
2296 winProbFrom: 'share of simulated margins with margin > 0', 2297 projectedMarginFrom: 'median (fallback mean) of same simulated margins', 2298 compositeWinProbFrom: 'logistic(composite blend margin) â target for Ï', 2299 calibration: 'forecast-calibration.js anchors + blend weights + sigma = |finalTarget| / Φâ»Â¹(favoredProb) à volatilityMultiplier', 2300 inputRatingMargins: 'House: ET.HOUSE_INPUT_RATING_TO_MARGIN / forecastModelHouseRatingToMargin; Senate/Gov: ET.INPUT_RATING_TO_MARGIN' 2301 } 2302 }; 2303 2304 /** Win % for headlines / map â simulated average when available, else composite logistic. */ 2305 ET.forecastModelDisplayWinProbD = function (det) { 2306 if (!det) return null; 2307 if (det.displayWinProbD != null && isFinite(det.displayWinProbD)) return det.displayWinProbD; 2308 if (det.winProbDSim != null && isFinite(det.winProbDSim)) return det.winProbDSim; 2309 if (det.winProbD != null && isFinite(det.winProbD)) return det.winProbD; 2310 return null; 2311 }; 2312 2313 /** Projected D margin â median of calibrated margin draws (mean in marginSimMean). */ 2314 ET.forecastModelDisplayMargin = function (det) { 2315 if (!det) return null; 2316 if (det.displayMargin != null && isFinite(det.displayMargin)) return det.displayMargin; 2317 if (det.marginSim != null && isFinite(det.marginSim)) return det.marginSim; 2318 if (det.marginSimMedian != null && isFinite(det.marginSimMedian)) return det.marginSimMedian; 2319 if (det.marginSimMean != null && isFinite(det.marginSimMean)) return det.marginSimMean; 2320 if (det.margin != null && isFinite(det.margin)) return det.margin; 2321 return null; 2322 }; 2323 2324 /** Shared display snapshot so house hub + house forecast pages use identical current values. */ 2325 ET.forecastModelDisplaySnapshot = function (det) { 2326 if (!det) return null; 2327 var margin = ET.forecastModelDisplayMargin(det); 2328 var winProbD = ET.forecastModelDisplayWinProbD(det); 2329 if ((margin == null || !isFinite(margin)) && (winProbD == null || !isFinite(winProbD))) return null; 2330 var marginDisplay = null; 2331 var voteShareDDisplay = null; 2332 var voteShareRDisplay = null; 2333 if (margin != null && isFinite(margin) && typeof ET.forecastVoteSharesFromDisplayMargin === 'function') { 2334 var sharePack = ET.forecastVoteSharesFromDisplayMargin(margin); 2335 if (sharePack) { 2336 marginDisplay = sharePack.marginDisplay; 2337 voteShareDDisplay = sharePack.voteShareD; 2338 voteShareRDisplay = sharePack.voteShareR; 2339 } 2340 } else if (margin != null && isFinite(margin)) { 2341 marginDisplay = parseFloat(typeof ET.oneDec === 'function' ? ET.oneDec(margin) : Number(margin).toFixed(1)); 2342 voteShareDDisplay = marginDisplay != null ? (50 + marginDisplay / 2) : null; 2343 voteShareRDisplay = marginDisplay != null ? (50 - marginDisplay / 2) : null; 2344 } 2345 var voteShareDRaw = (margin != null && isFinite(margin)) ? (50 + margin / 2) : null; 2346 var voteShareRRaw = (margin != null && isFinite(margin)) ? (50 - margin / 2) : null; 2347 var winPctRaw = (winProbD != null && isFinite(winProbD)) 2348 ? Math.max(0, Math.min(100, winProbD * 100)) 2349 : null; 2350 var winPctCapped = (winPctRaw != null && typeof ET.floorWinProbDisplayPct === 'function') 2351 ? ET.floorWinProbDisplayPct(winPctRaw) 2352 : winPctRaw; 2353 var winPctDDisplay = winPctCapped != null 2354 ? parseFloat(typeof ET.oneDec === 'function' ? ET.oneDec(winPctCapped) : Number(winPctCapped).toFixed(1)) 2355 : null; 2356 if (det.partyLockout === 'D' || det.partyLockout === 'R') { 2357 winPctDDisplay = det.partyLockout === 'D' ? 100 : 0; 2358 voteShareDDisplay = det.partyLockout === 'D' ? 100 : 0; 2359 voteShareRDisplay = det.partyLockout === 'R' ? 100 : 0; 2360 marginDisplay = det.partyLockout === 'D' ? 100 : -100; 2361 } 2362 var winPctRDisplay = winPctDDisplay != null 2363 ? parseFloat(typeof ET.oneDec === 'function' ? ET.oneDec(100 - winPctDDisplay) : Number(100 - winPctDDisplay).toFixed(1)) 2364 : null; 2365 return { 2366 detail: det, 2367 margin: margin, 2368 marginDisplay: marginDisplay, 2369 voteShareDRaw: voteShareDRaw, 2370 voteShareRRaw: voteShareRRaw, 2371 voteShareDDisplay: voteShareDDisplay, 2372 voteShareRDisplay: voteShareRDisplay, 2373 winProbD: winProbD, 2374 winPctDDisplay: winPctDDisplay, 2375 winPctRDisplay: winPctRDisplay, 2376 rating: (winProbD != null && isFinite(winProbD) && typeof ET.forecastModelWinProbToRating === 'function') 2377 ? ET.forecastModelWinProbToRating(winProbD) 2378 : (det.rating || null) 2379 }; 2380 }; 2381
2382 function houseForecastSnapshotPayload() { 2383 return ET.HOUSE_FORECAST_SNAPSHOT && ET.HOUSE_FORECAST_SNAPSHOT.districts 2384 ? ET.HOUSE_FORECAST_SNAPSHOT 2385 : null; 2386 } 2387 2388 function applyHouseSnapshotDistrictToDetail(detail, districtSnap, asOfIso) { 2389 if (!detail || !districtSnap) return false; 2390 if (districtSnap.demWinProb != null && isFinite(districtSnap.demWinProb)) { 2391 detail.winProbDSim = districtSnap.demWinProb; 2392 detail.displayWinProbD = districtSnap.demWinProb; 2393 detail.winProbD = districtSnap.demWinProb; 2394 } 2395 if (districtSnap.repWinProb != null && isFinite(districtSnap.repWinProb)) { 2396 detail.winProbR = districtSnap.repWinProb; 2397 } 2398 if (districtSnap.projectedMargin != null && isFinite(districtSnap.projectedMargin)) { 2399 var snapM = districtSnap.projectedMargin; 2400 /* 2401 * Soft-cap / incumbent snap can disagree with the composite blend used on prior 2402 * trend days. Prefer composite so hub + forecast pages stay continuous. 2403 */ 2404 var composite = detail.margin; 2405 if (composite != null && isFinite(composite) && Math.abs(snapM - composite) >= 4) { 2406 var softCapRail = Math.abs(Math.abs(snapM) - 40) <= 1.25; 2407 var moreExtreme = Math.abs(snapM) > Math.abs(composite) + 3.5; 2408 if (softCapRail || moreExtreme) { 2409 snapM = composite; 2410 detail._houseTerminalJumpRepaired = true; 2411 } 2412 } 2413 detail.marginSim = snapM; 2414 detail.marginSimMedian = snapM; 2415 detail.displayMargin = snapM; 2416 if (detail._houseTerminalJumpRepaired && typeof ET.forecastModelMarginDisplayLabel === 'function') { 2417 detail.marginLabel = ET.forecastModelMarginDisplayLabel( 2418 snapM, 2419 detail.winProbDSim != null ? detail.winProbDSim : detail.winProbD 2420 ); 2421 } 2422 } 2423 if (districtSnap.rating) detail.rating = districtSnap.rating; 2424 if (districtSnap.favoredParty) detail.favoredParty = districtSnap.favoredParty; 2425 if (districtSnap.marginLabel && !detail._houseTerminalJumpRepaired) { 2426 detail.marginLabel = districtSnap.marginLabel; 2427 } 2428 if (districtSnap.pollDPct != null) detail.pollDPct = districtSnap.pollDPct; 2429 if (districtSnap.pollRPct != null) detail.pollRPct = districtSnap.pollRPct; 2430 detail._fromHouseForecastSnapshot = true; 2431 detail._forecastSimSig = 'houseSnapshot|' + (asOfIso || districtSnap.asOfIso || '') + '|' + 2432 (districtSnap.code || ''); 2433 return true; 2434 } 2435 2436 ET.houseForecastSnapshotDistrict = function (code, asOfIso) { 2437 if (!code) return null; 2438 var payload = houseForecastSnapshotPayload(); 2439 if (!payload || !payload.districts) return null; 2440 if (asOfIso && payload.asOfIso && payload.asOfIso !== asOfIso) { 2441 var today = typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : null; 2442 /* 2443 * Daily snapshot not rebuilt yet for "today" â use the latest published 2444 * snapshot so hub maps and district /forecast/ pages stay aligned. 2445 */ 2446 if (!(today && asOfIso === today && payload.asOfIso <= today)) { 2447 return null; 2448 } 2449 } 2450 return payload.districts[code] || null; 2451 }; 2452 2453 ET.houseForecastSnapshotSig = function () { 2454 var parts = ['houseSnapshotSigV1', String(FORECAST_DAILY_BUNDLE_VERSION)]; 2455 if (typeof ET.editorialRatingsBakeFingerprint === 'function') { 2456 parts.push(ET.editorialRatingsBakeFingerprint()); 2457 } 2458 if (ET.HOUSE_COOK_BLEND_REVISION != null) { 2459 parts.push('houseCookBlend' + ET.HOUSE_COOK_BLEND_REVISION); 2460 } 2461 return parts.join('|'); 2462 }; 2463 2464 ET.applyHouseForecastSnapshot = function () { 2465 if (typeof ET.houseHubUsesBakedForecastOnly === 'function' && ET.houseHubUsesBakedForecastOnly()) { 2466 return false;
2467 } 2468 var payload = houseForecastSnapshotPayload(); 2469 if (!payload || !payload.districts || ET._houseForecastSnapshotApplied) return false; 2470 var districts = payload.districts; 2471 var asOfIso = payload.asOfIso || null; 2472 ET.FORECAST_MODEL_HOUSE_DETAIL = ET.FORECAST_MODEL_HOUSE_DETAIL || {}; 2473 Object.keys(districts).forEach(function (code) { 2474 var d = districts[code]; 2475 if (!d) return; 2476 var det = ET.FORECAST_MODEL_HOUSE_DETAIL[code] || {}; 2477 applyHouseSnapshotDistrictToDetail(det, d, asOfIso); 2478 ET.FORECAST_MODEL_HOUSE_DETAIL[code] = det; 2479 }); 2480 ET._forecastDailyBundleIso = asOfIso; 2481 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 2482 ET._houseForecastSnapshotApplied = true; 2483 if (typeof document !== 'undefined') { 2484 try { 2485 document.dispatchEvent(new CustomEvent('pollsmax-house-forecast-snapshot-applied')); 2486 } catch (eSnap) { /* ignore */ } 2487 } 2488 return true; 2489 }; 2490 2491 if (typeof ET.HOUSE_FORECAST_SNAPSHOT !== 'undefined' && ET.HOUSE_FORECAST_SNAPSHOT) { 2492 ET.applyHouseForecastSnapshot(); 2493 } 2494 2495 ET.houseForecastDetail = function (code, asOfIso) { 2496 if (!code) return null; 2497 /* 2498 * Canonicalize the district code before ANY lookup/sim. "AZ-1" and "AZ-01" 2499 * must resolve to the same result: the published snapshot (payload.districts) 2500 * and the rating rows are keyed by the unpadded form ("AZ-1"), so a padded 2501 * caller (hub map via geoid, race.code = "AZ-01") would miss the snapshot and 2502 * fall back to a fresh MC draw whose seed string ("house:AZ-01|â¦") differs 2503 * from the unpadded seed â flipping a knife-edge race DâR vs the hub list / 2504 * forecast hero. Normalize so every surface shows identical odds/margin/rating. 2505 */ 2506 if (typeof ET.houseDistrictCodeForIncumbentLookup === 'function') { 2507 var canonCode = ET.houseDistrictCodeForIncumbentLookup(code); 2508 if (canonCode) code = canonCode; 2509 } 2510 var lockParty = typeof ET.housePartyLockoutParty === 'function' ? ET.housePartyLockoutParty(code) : null; 2511 if (lockParty && typeof ET.housePartyLockoutForecastDetail === 'function') { 2512 var lockDet = ET.housePartyLockoutForecastDetail(code, lockParty); 2513 if (lockDet) { 2514 if (ET.FORECAST_MODEL_HOUSE_DETAIL) ET.FORECAST_MODEL_HOUSE_DETAIL[code] = lockDet; 2515 return lockDet; 2516 } 2517 } 2518 var iso = asOfIso || (typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : null); 2519 var det = null; 2520 if (iso && typeof ET.forecastModelHouseDetailAsOfDate === 'function') { 2521 det = ET.forecastModelHouseDetailAsOfDate(code, iso); 2522 } 2523 if (!det) det = ET.FORECAST_MODEL_HOUSE_DETAIL && ET.FORECAST_MODEL_HOUSE_DETAIL[code]; 2524 if (!det && typeof ET.forecastModelHouseDetail === 'function') { 2525 det = ET.forecastModelHouseDetail(code); 2526 } 2527 if (det && typeof ET.forecastModelEnsureDetailSim === 'function') { 2528 ET.forecastModelEnsureDetailSim(det, 'house', code, null, iso || asOfIso); 2529 } 2530 if (det && typeof ET.forecastModelDisplayWinProbD === 'function' && 2531 typeof ET.forecastModelWinProbToRating === 'function') { 2532 var pD = ET.forecastModelDisplayWinProbD(det); 2533 if (pD != null && isFinite(pD)) det.rating = ET.forecastModelWinProbToRating(pD, code); 2534 } 2535 if (det && ET.FORECAST_MODEL_HOUSE_DETAIL) ET.FORECAST_MODEL_HOUSE_DETAIL[code] = det; 2536 return det || null; 2537 }; 2538 2539 ET.houseForecastDisplaySnapshot = function (code, asOfIso) { 2540 var det = ET.houseForecastDetail(code, asOfIso); 2541 var snap = ET.forecastModelDisplaySnapshot(det); 2542 if (!snap) return det ? { detail: det, rating: det.rating || null } : null; 2543 snap.detail = det; 2544 snap.rating = det && det.rating ? det.rating : (snap.rating || null); 2545 return snap; 2546 }; 2547 2548 function attachSimWinProbToDetail(detail, seedKey, pollCount, drawCount, kind) { 2549 if (!detail) return detail; 2550 if (kind) detail.forecastKind = kind; 2551 var sim = ET.forecastModelSimAggregateForDetail(detail, { 2552 seedKey: seedKey, 2553 pollCount: pollCount, 2554 drawCount: drawCount, 2555 chamber: kind || detail.forecastKind 2556 }); 2557 detail.winProbDSim = sim.winProbD; 2558 detail.marginSimMean = sim.margin; 2559 detail.marginSimMedian = sim.marginMedian != null ? sim.marginMedian : sim.margin; 2560 detail.marginSim = detail.marginSimMedian; 2561 detail.targetWinD = sim.targetWinD; 2562 detail.simTargetMargin = sim.finalTargetMargin != null ? sim.finalTargetMargin : sim.centerMargin; 2563 detail.simCalibratedMargin = sim.calibratedMarginFromOdds; 2564 detail.simSigma = sim.sigma; 2565 detail.simVolatilityMultiplier = sim.volatilityMultiplier; 2566 detail.displayWinProbD = detail.winProbDSim; 2567 detail.displayMargin = detail.marginSim; 2568 return detail; 2569 } 2570 2571 function forecastModelPollCountForKind(kind, key, asOfIso) { 2572 var iso = asOfIso || (typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : '9999-12-31'); 2573 if (kind === 'senate' && ET.SENATE && ET.SENATE[key]) { 2574 return racePollsOnOrBeforeDate(ET.SENATE[key], iso).length; 2575 } 2576 if (kind === 'gov' && typeof ET.govSeriesWithLocalFallback === 'function') { 2577 return racePollsOnOrBeforeDate(ET.govSeriesWithLocalFallback(key) || [], iso).length; 2578 } 2579 if (kind === 'house' && ET.HOUSE_FORECAST_RATINGS) { 2580 var row = ET.HOUSE_RATING_BY_CODE && ET.HOUSE_RATING_BY_CODE[key]; 2581 var tracked = row && row.geoid && ET.HOUSE_BY_GEOID ? ET.HOUSE_BY_GEOID[row.geoid] : null; 2582 if (tracked && tracked.series) return racePollsOnOrBeforeDate(tracked.series, iso).length; 2583 } 2584 return 0; 2585 } 2586 2587 ET.forecastModelPollCountForKind = forecastModelPollCountForKind; 2588 2589 function forecastModelDetailSimSignature(kind, key, det, pollCount) { 2590 var pm = ET.forecastModelPollMarginFromDetail(det); 2591 var pmStr = pm != null && isFinite(pm) ? pm.toFixed(4) : '
2591np'; 2592 var cm = det && det.margin != null && isFinite(det.margin) ? det.margin.toFixed(4) : '0'; 2593 var blendRev = '0'; 2594 var houseRev = kind === 'house' && ET.HOUSE_COOK_BLEND_REVISION != null 2595 ? '|houseCookBlend' + ET.HOUSE_COOK_BLEND_REVISION 2596 : ''; 2597 var houseCalRev = kind === 'house' ? '|houseMarginCal2' : ''; 2598 if (kind === 'senate' && ET.SENATE_EDITORIAL_BLEND_REVISION != null) { 2599 blendRev = String(ET.SENATE_EDITORIAL_BLEND_REVISION); 2600 } else if (kind === 'gov' && ET.GOV_EDITORIAL_BLEND_REVISION != null) { 2601 blendRev = String(ET.GOV_EDITORIAL_BLEND_REVISION); 2602 } 2603 return 'simSig:v15|marginCal9|cal4|' + kind + '|' + key + '|' + pollCount + '|br' + blendRev + '|' + cm + '|' + pmStr + houseRev + houseCalRev; 2604 } 2605 2606 function forecastModelDetailSimSeedKey(kind, key, asOfIso) { 2607 var iso = asOfIso || (typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : '9999-12-31'); 2608 if (kind === 'senate') return 'senate:' + key + '|' + iso + '|trend100k'; 2609 if (kind === 'gov') return 'gov:' + key + '|' + iso + '|trend100k'; 2610 return 'house:' + key + '|' + iso + '|trend10k'; 2611 } 2612 2613 /** Stamp cache sig after a fresh 100k run (state forecast page sync). */ 2614 ET.forecastModelStampDetailSimCache = function (detail, kind, key, pollCount) { 2615 if (!detail || !key) return detail; 2616 detail._forecastSimSig = forecastModelDetailSimSignature( 2617 kind, key, detail, pollCount == null ? forecastModelPollCountForKind(kind, key) : pollCount); 2618 return detail; 2619 }; 2620 2621 /** 2622 * Re-run 100k sim when composite/polls changed so hub map % matches state forecast pages. 2623 */ 2624 ET.forecastModelEnsureDetailSim = function (detail, kind, key, name, asOfIso) { 2625 if (!detail || !key) return detail; 2626 var iso = asOfIso || (typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : '9999-12-31'); 2627 var pollCount = forecastModelPollCountForKind(kind, key, iso); 2628 var sig = forecastModelDetailSimSignature(kind, key, detail, pollCount); 2629 if (detail._forecastSimSig === sig && 2630 detail.winProbDSim != null && isFinite(detail.winProbDSim) && 2631 forecastSimSnapshotConsistentWithDetail(detail, { 2632 ws: detail.winProbDSim, 2633 ms: detail.marginSim != null ? detail.marginSim : detail.marginSimMedian 2634 })) { 2635 return detail; 2636 } 2637 if (kind === 'house') { 2638 var districtSnap = ET.houseForecastSnapshotDistrict && ET.houseForecastSnapshotDistrict(key, iso); 2639 if (districtSnap && applyHouseSnapshotDistrictToDetail(detail, districtSnap, iso)) { 2640 if (typeof ET.forecastModelWinProbToRating === 'function' && 2641 detail.displayWinProbD != null && isFinite(detail.displayWinProbD)) { 2642 detail.rating = ET.forecastModelWinProbToRating(detail.displayWinProbD, key); 2643 } 2644 detail._forecastSimSig = sig; 2645 return detail; 2646 } 2647 /* Keep hub bake / last published snapshot â do not re-sim past them. */ 2648 if (detail._fromHouseForecastSnapshot && 2649 detail.winProbDSim != null && isFinite(detail.winProbDSim)) { 2650 return detail; 2651 } 2652 } 2653 var drawCount = kind === 'house' ? FORECAST_SIM_HOUSE_DRAW_COUNT : FORECAST_SIM_DRAW_COUNT; 2654 attachSimWinProbToDetail( 2655 detail, 2656 forecastModelDetailSimSeedKey(kind, key, iso), 2657 pollCount, 2658 drawCount, 2659 kind); 2660 detail._forecastSimSig = sig; 2661 if (typeof ET.forecastModelWinProbToRating === 'function') { 2662 var probForRating = (kind === 'house' && detail.displayWinProbD != null && isFinite(detail.displayWinProbD)) 2663 ? detail.displayWinProbD 2664 : detail.winProbDSim; 2665 if (probForRating != null && isFinite(probForRating)) { 2666 detail.rating = ET.forecastModelWinProbToRating(probForRating, key); 2667 } 2668 } 2669 return detail; 2670 }; 2671 2672 /** Hub + state pages: one 100k sim batch per calendar day (localStorage), not every page load. */ 2673 var FORECAST_DAILY_BUNDLE_PREFIX = 'pollsmax|forecastDaily|v9|'; 2674 var FORECAST_DAILY_BUNDLE_VERSION = 43; 2675 ET.FORECAST_DAILY_BUNDLE_VERSION = FORECAST_DAILY_BUNDLE_VERSION; 2676 var FORECAST_DAILY_MIN_SENATE = 35; 2677 var FORECAST_DAILY_MIN_GOV = 10; 2678 var FORECAST_DAILY_MIN_HOUSE = 100; 2679
2680 function readForecastDailyStorage(key) { 2681 try { 2682 var raw = localStorage.getItem(key); 2683 if (!raw) return null; 2684 return JSON.parse(raw); 2685 } catch (e) { 2686 return null; 2687 } 2688 } 2689 2690 function writeForecastDailyStorage(key, payload) { 2691 try { 2692 localStorage.setItem(key, JSON.stringify(payload)); 2693 } catch (e) { /* quota / private mode */ } 2694 } 2695 2696 function forecastDailyBundleKey(iso) { 2697 return FORECAST_DAILY_BUNDLE_PREFIX + iso; 2698 } 2699 2700 function forecastDailyBundleNeedsKind(kind) { 2701 var path = ''; 2702 try { path = location && location.pathname ? location.pathname : ''; } catch (e) { path = ''; } 2703 var ctx = typeof ET.parseRacePageContext === 'function' ? ET.parseRacePageContext() : null; 2704 if (kind === 'house') { 2705 return !!( 2706 document.getElementById('houseCdMap') || 2707 /^\/2026-us-house(?:\/index\.html)?\/?$/.test(path) 2708 ); 2709 } 2710 if (kind === 'senate') { 2711 return !!document.getElementById('usMap'); 2712 } 2713 if (kind === 'gov') { 2714 return !!document.getElementById('govMap'); 2715 } 2716 return false; 2717 } 2718 2719 function forecastDailyBundleRequirements() { 2720 return { 2721 sen: !!(ET.COOK_STATE_PVI && ET.SENATE_EDITORIAL_RATINGS && ET.SENATE_FORECAST_PRES_AVG && 2722 ET.ELECTION_2026 && ET.NAME_BY_FIPS && ET.KEY_BY_NAME && 2723 forecastDailyBundleNeedsKind('senate')), 2724 gov: !!(ET.GOV_EDITORIAL_RATINGS && ET.GOV_FORECAST_PRES_AVG && ET.COOK_GOV_RATINGS && 2725 forecastDailyBundleNeedsKind('gov')), 2726 house: !!(ET.HOUSE_FORECAST_RATINGS && ET.HOUSE_RATING_BY_CODE && ET.HOUSE_BY_GEOID && 2727 ET.COOK_HOUSE_RATINGS && ET.COOK_DISTRICT_PVI && 2728 forecastDailyBundleNeedsKind('house')) 2729 }; 2730 } 2731 2732 function forecastDailySectionReady(section, minKeys) { 2733 return section && typeof section === 'object' && Object.keys(section).length >= minKeys; 2734 } 2735 2736 function forecastDailyBundleMeetsRequirements(bundle, iso, req) { 2737 if (!bundle || bundle.v !== FORECAST_DAILY_BUNDLE_VERSION || bundle.iso !== iso) return false; 2738 if (req.sen && !forecastDailySectionReady(bundle.sen, FORECAST_DAILY_MIN_SENATE)) return false; 2739 if (req.gov && !forecastDailySectionReady(bundle.gov, FORECAST_DAILY_MIN_GOV)) return false; 2740 if (req.house && !forecastDailySectionReady(bundle.house, FORECAST_DAILY_MIN_HOUSE)) return false; 2741 return true; 2742 } 2743 2744 function loadForecastDailyBundle(iso) { 2745 return readForecastDailyStorage(forecastDailyBundleKey(iso)); 2746 } 2747 2748 function mergeForecastDailyBundle(iso, partial) { 2749 var bundle = loadForecastDailyBundle(iso) || { 2750 v: FORECAST_DAILY_BUNDLE_VERSION, 2751 iso: iso, 2752 sen: null, 2753 gov: null, 2754 house: null 2755 }; 2756 bundle.v = FORECAST_DAILY_BUNDLE_VERSION; 2757 bundle.iso = iso; 2758 if (partial.sen) bundle.sen = partial.sen; 2759 if (partial.gov) bundle.gov = partial.gov; 2760 if (partial.house) bundle.house = partial.house; 2761 writeForecastDailyStorage(forecastDailyBundleKey(iso), bundle); 2762 return bundle; 2763 } 2764 2765 function forecastSimSnapshotConsistentWithDetail(det, snap) { 2766 if (!det || !snap) return false; 2767 var cm = det.margin != null && isFinite(det.margin) ? det.margin : null; 2768 var sm = snap.ms != null && isFinite(snap.ms) ? snap.ms : null; 2769 var sw = snap.ws != null && isFinite(snap.ws) ? snap.ws : null; 2770 if (cm == null || !isFinite(cm)) return true; 2771 if (sm != null && Math.abs(cm) >= 2 && Math.abs(sm) < 0.75) return false; 2772 if (sm != null && cm * sm < 0 && Math.abs(cm) >= 1 && Math.abs(sm) >= 1) return false; 2773 if (sw != null && Math.abs(cm) >= 2 && Math.abs(sw - 0.5) < 0.02) return false; 2774 return true; 2775 } 2776 2777 function applySimSnapshotToDetail(det, snap, currentSig) { 2778 if (!det || !snap) return det; 2779 if (!snap.sig) return det; 2780 if (currentSig && snap.sig !== currentSig) return det; 2781 if (!forecastSimSnapshotConsistentWithDetail(det, snap)) return det; 2782 if (snap.ws != null && isFinite(snap.ws)) { 2783 det.winProbDSim = snap.ws; 2784 det.displayWinProbD = snap.ws; 2785 } 2786 if (snap.ms != null && isFinite(snap.ms)) { 2787 det.marginSim = snap.ms; 2788 det.marginSimMedian = snap.ms; 2789 det.displayMargin = snap.ms; 2790 } 2791 if (snap.mm != null && isFinite(snap.mm)) det.marginSimMean = snap.mm; 2792 if (snap.sig) det._forecastSimSig = snap.sig; 2793 return det; 2794 } 2795 2796 function simSnapshotFromDetail(det, kind, key, pollCount) { 2797 if (!det) return null; 2798 return { 2799 ws: det.winProbDSim, 2800 ms: det.marginSim, 2801 mm: det.marginSimMean, 2802 sig: forecastModelDetailSimSignature(kind, key, det, pollCount) 2803 }; 2804 } 2805 2806 function restoreSenateFromDailyBundle(bundleSen, todayIso) { 2807 ET.FORECAST_MODEL_SENATE_DETAIL = {}; 2808 for (var f in ET.ELECTION_2026) { 2809 if (!ET.ELECTION_2026.hasOwnProperty(f)) continue;
2810 var nm = ET.NAME_BY_FIPS[f]; 2811 var key = ET.KEY_BY_NAME[nm]; 2812 if (!key) continue; 2813 var detailSen = ET.forecastModelSenateDetailAsOfDate(key, nm, todayIso); 2814 var senPollN = ET.SENATE && ET.SENATE[key] 2815 ? racePollsOnOrBeforeDate(ET.SENATE[key], todayIso).length 2816 : 0; 2817 var senSig = forecastModelDetailSimSignature('senate', key, detailSen, senPollN); 2818 applySimSnapshotToDetail(detailSen, bundleSen[key], senSig); 2819 ET.FORECAST_MODEL_SENATE_DETAIL[key] = detailSen; 2820 } 2821 } 2822 2823 function restoreGovFromDailyBundle(bundleGov, todayIso) { 2824 ET.FORECAST_MODEL_GOV_DETAIL = {}; 2825 Object.keys(ET.GOV_EDITORIAL_RATINGS).forEach(function (key) { 2826 if (ET.govEditorialNoElectionKeys && ET.govEditorialNoElectionKeys().has(key)) return; 2827 var detailGov = ET.forecastModelGovDetailAsOfDate(key, todayIso); 2828 var govPollN0 = typeof ET.govSeriesWithLocalFallback === 'function' 2829 ? racePollsOnOrBeforeDate(ET.govSeriesWithLocalFallback(key) || [], todayIso).length 2830 : 0; 2831 var govSig = forecastModelDetailSimSignature('gov', key, detailGov, govPollN0); 2832 applySimSnapshotToDetail(detailGov, bundleGov[key], govSig); 2833 ET.FORECAST_MODEL_GOV_DETAIL[key] = detailGov; 2834 ET.GOV_EDITORIAL_RATINGS[key] = detailGov.rating; 2835 }); 2836 } 2837 2838 function restoreHouseFromDailyBundle(bundleHouse, todayIso) { 2839 ET.FORECAST_MODEL_HOUSE_DETAIL = {}; 2840 ET.HOUSE_FORECAST_RATINGS.forEach(function (row) { 2841 if (!row || !row.code) return; 2842 var detailHouse = ET.forecastModelHouseDetailAsOfDate(row.code, todayIso); 2843 var houseRow = ET.HOUSE_RATING_BY_CODE && ET.HOUSE_RATING_BY_CODE[row.code]; 2844 var houseTracked = houseRow && houseRow.geoid && ET.HOUSE_BY_GEOID 2845 ? ET.HOUSE_BY_GEOID[houseRow.geoid] 2846 : null; 2847 var housePollN = houseTracked && houseTracked.series 2848 ? racePollsOnOrBeforeDate(houseTracked.series, todayIso).length 2849 : 0; 2850 var houseSig = forecastModelDetailSimSignature('house', row.code, detailHouse, housePollN); 2851 applySimSnapshotToDetail(detailHouse, bundleHouse[row.code], houseSig); 2852 ET.FORECAST_MODEL_HOUSE_DETAIL[row.code] = detailHouse; 2853 row.rating = detailHouse.rating; 2854 }); 2855 } 2856 2857 function restoreForecastFromDailyBundle(bundle, todayIso, req) { 2858 if (req.sen && bundle.sen) restoreSenateFromDailyBundle(bundle.sen, todayIso); 2859 if (req.gov && bundle.gov) restoreGovFromDailyBundle(bundle.gov, todayIso); 2860 if (req.house && bundle.house) restoreHouseFromDailyBundle(bundle.house, todayIso); 2861 } 2862 2863 function computeSenateDailySim(todayIso) { 2864 var snaps = {}; 2865 for (var f in ET.ELECTION_2026) { 2866 if (!ET.ELECTION_2026.hasOwnProperty(f)) continue; 2867 var nm = ET.NAME_BY_FIPS[f]; 2868 var key = ET.KEY_BY_NAME[nm]; 2869 if (!key) continue; 2870 var detailSen = ET.forecastModelSenateDetailAsOfDate(key, nm, todayIso); 2871 var senPollN = ET.SENATE && ET.SENATE[key] 2872 ? racePollsOnOrBeforeDate(ET.SENATE[key], todayIso).length 2873 : 0; 2874 attachSimWinProbToDetail( 2875 detailSen, 'senate:' + key + '|' + todayIso + '|trend100k', senPollN, FORECAST_SIM_DRAW_COUNT, 'senate'); 2876 detailSen._forecastSimSig = forecastModelDetailSimSignature('senate', key, detailSen, senPollN); 2877 ET.FORECAST_MODEL_SENATE_DETAIL[key] = detailSen; 2878 snaps[key] = simSnapshotFromDetail(detailSen, 'senate', key, senPollN); 2879 } 2880 return snaps; 2881 } 2882 2883 function computeGovDailySim(todayIso) { 2884 var snaps = {}; 2885 Object.keys(ET.GOV_EDITORIAL_RATINGS).forEach(function (key) { 2886 if (ET.govEditorialNoElectionKeys && ET.govEditorialNoElectionKeys().has(key)) return; 2887 var detailGov = ET.forecastModelGovDetailAsOfDate(key, todayIso); 2888 var govSeries = ET.govSeriesWithLocalFallback 2889 ? ET.govSeriesWithLocalFallback(key) 2890 : (ET.govSeries ? ET.govSeries(key) : []); 2891 var govPollN = racePollsOnOrBeforeDate(govSeries || [], todayIso).length; 2892 attachSimWinProbToDetail( 2893 detailGov, 'gov:' + key + '|' + todayIso + '|trend100k', govPollN, FORECAST_SIM_DRAW_COUNT, 'gov'); 2894 detailGov._forecastSimSig = forecastModelDetailSimSignature('gov', key, detailGov, govPollN); 2895 ET.FORECAST_MODEL_GOV_DETAIL[key] = detailGov; 2896 ET.GOV_EDITORIAL_RATINGS[key] = detailGov.rating; 2897 snaps[key] = simSnapshotFromDetail(detailGov, 'gov', key, govPollN); 2898 }); 2899 return snaps; 2900 } 2901 2902 function computeHouseDailySim(todayIso) { 2903 var snaps = {};
2904 ET.HOUSE_FORECAST_RATINGS.forEach(function (row) { 2905 if (!row || !row.code) return; 2906 var detailHouse = ET.forecastModelHouseDetailAsOfDate(row.code, todayIso); 2907 var houseRow = ET.HOUSE_RATING_BY_CODE && ET.HOUSE_RATING_BY_CODE[row.code]; 2908 var houseTracked = houseRow && houseRow.geoid && ET.HOUSE_BY_GEOID 2909 ? ET.HOUSE_BY_GEOID[houseRow.geoid] 2910 : null; 2911 var housePollN = houseTracked && houseTracked.series 2912 ? racePollsOnOrBeforeDate(houseTracked.series, todayIso).length 2913 : 0; 2914 attachSimWinProbToDetail( 2915 detailHouse, 2916 'house:' + row.code + '|' + todayIso + '|trend100k', 2917 housePollN, 2918 FORECAST_SIM_HOUSE_DRAW_COUNT, 2919 'house'); 2920 ET.FORECAST_MODEL_HOUSE_DETAIL[row.code] = detailHouse; 2921 row.rating = detailHouse.rating; 2922 detailHouse._forecastSimSig = forecastModelDetailSimSignature('house', row.code, detailHouse, housePollN); 2923 snaps[row.code] = simSnapshotFromDetail(detailHouse, 'house', row.code, housePollN); 2924 }); 2925 return snaps; 2926 } 2927 2928 function inMemoryForecastDailyComplete(todayIso, req) { 2929 if (ET._forecastDailyBundleIso !== todayIso) return false; 2930 if (ET._forecastDailyBundleVersion !== FORECAST_DAILY_BUNDLE_VERSION) return false; 2931 if (req.sen) { 2932 if (!ET.FORECAST_MODEL_SENATE_DETAIL || 2933 Object.keys(ET.FORECAST_MODEL_SENATE_DETAIL).length < FORECAST_DAILY_MIN_SENATE) { 2934 return false; 2935 } 2936 } 2937 if (req.gov) { 2938 if (!ET.FORECAST_MODEL_GOV_DETAIL || 2939 Object.keys(ET.FORECAST_MODEL_GOV_DETAIL).length < FORECAST_DAILY_MIN_GOV) { 2940 return false; 2941 } 2942 } 2943 if (req.house) { 2944 if (!ET.FORECAST_MODEL_HOUSE_DETAIL || 2945 Object.keys(ET.FORECAST_MODEL_HOUSE_DETAIL).length < FORECAST_DAILY_MIN_HOUSE) { 2946 return false; 2947 } 2948 } 2949 return true; 2950 } 2951 2952 function refreshForecastUiAfterApply() { 2953 if (typeof ET.refreshStateRacePageHeadFromForecast === 'function' && 2954 typeof ET.racePageViewMode === 'function' && 2955 ET.racePageViewMode() === 'forecast') { 2956 ET.refreshStateRacePageHeadFromForecast(); 2957 } 2958 if (typeof ET.scheduleRaceOverviewLiveDataRefresh === 'function') { 2959 ET.scheduleRaceOverviewLiveDataRefresh(); 2960 } 2961 if (typeof ET.refreshRaceForecastPageIfMounted === 'function') { 2962 ET.refreshRaceForecastPageIfMounted(); 2963 } else if (typeof ET.initRaceForecastPage === 'function' && 2964 typeof ET.racePageViewMode === 'function' && 2965 ET.racePageViewMode() === 'forecast' && 2966 !ET._raceForecastPageInitInFlight) { 2967 ET.initRaceForecastPage(); 2968 } 2969 if (typeof ET.refreshForecastHubAsideRaces === 'function') { 2970 ET.refreshForecastHubAsideRaces('usMap'); 2971 ET.refreshForecastHubAsideRaces('govMap'); 2972 } 2973 if (typeof ET.refreshHouseCdMap === 'function') { 2974 try { ET.refreshHouseCdMap('houseCdMap'); } catch (eHouseMap) { /* prerender join / fill */ } 2975 } 2976 if (typeof ET.refreshHouseHubOddsCharts === 'function' && document.getElementById('houseHubControlOddsChart')) { 2977 ET.refreshHouseHubOddsCharts({ clearMemos: true }); 2978 } 2979 if (typeof ET.updateSeatBar === 'function') { 2980 var senIds = { r: 'rSeats', d: 'dSeats', tie: 'tieSeats', nr: 'rCount', nd: 'dCount' }; 2981 if (document.getElementById('rSeats')) ET.updateSeatBar(senIds); 2982 } 2983 if (typeof ET.updateGovSeatBar === 'function' && document.getElementById('govHubSeatBar')) { 2984 ET.updateGovSeatBar(); 2985 } 2986 if (typeof ET.refreshForecastHubSeatBar === 'function') { 2987 var uiSen = ET.getForecastHubMapUi && ET.getForecastHubMapUi('usMap'); 2988 if (uiSen && uiSen.mapSource === 'ratings') { 2989 ET.refreshForecastHubSeatBar({ r: 'rSeats', d: 'dSeats', tie: 'tieSeats', nr: 'rCount', nd: 'dCount' }, 'senate', 'usMap'); 2990 } 2991 var uiGov = ET.getForecastHubMapUi && ET.getForecastHubMapUi('govMap'); 2992 if (uiGov && uiGov.mapSource === 'ratings') { 2993 ET.refreshForecastHubSeatBar({ r: 'govRSeats', d: 'govDSeats', tie: 'govTieSeats', nr: 'govRCount', nd: 'govDCount' }, 'gov', 'govMap'); 2994 } 2995 } 2996 if (typeof ET.homeMapUsesForecastMode === 'function' && ET.homeMapUsesForecastMode()) { 2997 if (typeof ET.ensureHomeForecastMapUi === 'function') { 2998 ET.ensureHomeForecastMapUi('usMap'); 2999 ET.ensureHomeForecastMapUi('govMap'); 3000 ET.ensureHomeForecastMapUi('houseCdMap'); 3001 } 3002 if (typeof ET.refreshForecastHubSenateMap === 'function' && document.getElementById('usMap')) { 3003 ET.refreshForecastHubSenateMap('usMap'); 3004 } 3005 if (typeof ET.refreshForecastHubGovMap === 'function' && document.getElementById('govMap')) { 3006 ET.refreshForecastHubGovMap('govMap'); 3007 } 3008 if (typeof ET.refreshHouseCdMap === 'function' && document.getElementById('houseCdMap')) { 3009 try { ET.refreshHouseCdMap('houseCdMap'); } catch (eHouseMap2) { /* prerender join / fill */ } 3010 } 3011 /* Home: keep 50/50 preload bars until senate+gov maps reveal together. */ 3012 if (!ET.homeForecastDeferSeatBars) { 3013 if (typeof ET.updateSeatBar === 'function' && document.getElementById('barR') && 3014 !(typeof ET.isHouseHubSeatBarPage === 'function' && ET.isHouseHubSeatBarPage())) { 3015 ET.updateSeatBar({ r: 'barR', d: 'barD', tie: 'barTie', nr: 'numR', nd: 'numD', rFlip: 'barRFlip', dFlip: 'barDFlip' }); 3016 } 3017 if (typeof ET.updateGovSeatBar === 'function' && document.getElementById('govBarR')) { 3018 ET.updateGovSeatBar({ r: 'govBarR', d: 'govBarD', nr: 'govNumR', nd: 'govNumD', rFlip: 'govBarRFlip', dFlip: 'govBarDFlip' }); 3019 } 3020 if (typeof ET.refreshHouseHubSeatBar === 'function' && document.getElementById('houseBarR')) { 3021 ET.refreshHouseHubSeatBar({ 3022 r: 'houseBarR', 3023 d: 'houseBarD', 3024 tie: 'houseBarTie', 3025 nr: 'houseNumR', 3026 nd: 'houseNumD', 3027 rFlip: 'houseBarRFlip', 3028 dFlip: 'houseBarDFlip' 3029 }, 'houseCdMap'); 3030 } 3031 } 3032 notifyHomeForecastReady(); 3033 } 3034 try { 3035 ET.forecastModelApplied = true; 3036 document.dispatchEvent(new CustomEvent('pollsmax-forecast-model-applied')); 3037 } catch (e) { /* ignore */ } 3038 } 3039 3040 function applySenateHubLiteForecast(todayIso, req) { 3041 if (chamberHubBakedDetailsIntact('senate')) { 3042 ET._forecastDailyBundleIso = todayIso; 3043 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3044 refreshForecastUiAfterApply(); 3045 return 'baked'; 3046 } 3047 if (ET.SENATE_HUB_BAKED_FORECAST && typeof ET.applySenateHubBakedForecast === 'function') { 3048 ET.SENATE_HUB_BAKED_FORECAST._appliedKey = null; 3049 if (ET.applySenateHubBakedForecast() && chamberHubBakedDetailsIntact('senate')) { 3050 ET._forecastDailyBundleIso = todayIso; 3051 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3052 refreshForecastUiAfterApply(); 3053 return 'baked'; 3054 } 3055 } 3056 ET.FORECAST_MODEL_ENV = ET.forecastModelNationalEnvironment(); 3057 if (req.sen && ET.ELECTION_2026) { 3058 ET.FORECAST_MODEL_SENATE_DETAIL = {}; 3059 for (var f in ET.ELECTION_2026) { 3060 if (!ET.ELECTION_2026.hasOwnProperty(f)) continue;
3061 var nm = ET.NAME_BY_FIPS[f]; 3062 var key = ET.KEY_BY_NAME[nm]; 3063 if (!key) continue; 3064 ET.FORECAST_MODEL_SENATE_DETAIL[key] = 3065 ET.forecastModelSenateDetailAsOfDate(key, nm, todayIso); 3066 } 3067 } 3068 ET._forecastDailyBundleIso = todayIso; 3069 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3070 refreshForecastUiAfterApply(); 3071 return 'live'; 3072 } 3073 3074 function applyGovHubLiteForecast(todayIso, req) { 3075 if (chamberHubBakedDetailsIntact('gov')) { 3076 ET._forecastDailyBundleIso = todayIso; 3077 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3078 refreshForecastUiAfterApply(); 3079 return 'baked'; 3080 } 3081 if (ET.GOV_HUB_BAKED_FORECAST && typeof ET.applyGovHubBakedForecast === 'function') { 3082 ET.GOV_HUB_BAKED_FORECAST._appliedKey = null; 3083 if (ET.applyGovHubBakedForecast() && chamberHubBakedDetailsIntact('gov')) { 3084 ET._forecastDailyBundleIso = todayIso; 3085 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3086 refreshForecastUiAfterApply(); 3087 return 'baked'; 3088 } 3089 } 3090 ET.FORECAST_MODEL_ENV = ET.forecastModelNationalEnvironment(); 3091 if (req.gov && ET.GOV_EDITORIAL_RATINGS) { 3092 ET.FORECAST_MODEL_GOV_DETAIL = {}; 3093 var govKeys = ET.GOV_PAGE_KEYS && ET.GOV_PAGE_KEYS.length 3094 ? ET.GOV_PAGE_KEYS 3095 : Object.keys(ET.GOV_EDITORIAL_RATINGS); 3096 for (var gi = 0; gi < govKeys.length; gi++) { 3097 var gk = govKeys[gi]; 3098 ET.FORECAST_MODEL_GOV_DETAIL[gk] = ET.forecastModelGovDetailAsOfDate(gk, todayIso); 3099 } 3100 } 3101 ET._forecastDailyBundleIso = todayIso; 3102 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3103 refreshForecastUiAfterApply(); 3104 return 'live'; 3105 } 3106 3107 function applyHouseHubLiteForecast(todayIso, req) { 3108 /* Home House map: keep hub bake numbers (same as Senate/Gov home maps). */ 3109 if (typeof ET.homeMapUsesForecastMode === 'function' && ET.homeMapUsesForecastMode() && 3110 typeof ET.applyHouseHubBakedForecast === 'function' && ET.HOUSE_HUB_BAKED_FORECAST) { 3111 ET._houseHubBakedForecastApplied = false; 3112 if (ET.applyHouseHubBakedForecast()) { 3113 ET._forecastDailyBundleIso = todayIso; 3114 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3115 refreshForecastUiAfterApply(); 3116 notifyHouseHubForecastReady(); 3117 if (typeof notifyHomeForecastReady === 'function') notifyHomeForecastReady(); 3118 return 'baked'; 3119 } 3120 } 3121 if (typeof ET.houseHubUsesBakedForecastOnly === 'function' && ET.houseHubUsesBakedForecastOnly()) { 3122 notifyHouseHubForecastReady(); 3123 return; 3124 } 3125 ET.FORECAST_MODEL_ENV = ET.forecastModelNationalEnvironment(); 3126 if (req.house && ET.HOUSE_FORECAST_RATINGS) { 3127 ET.FORECAST_MODEL_HOUSE_DETAIL = {}; 3128 ET.HOUSE_FORECAST_RATINGS.forEach(function (row) { 3129 if (!row || !row.code) return; 3130 var detailHouse = ET.forecastModelHouseDetailAsOfDate(row.code, todayIso); 3131 ET.FORECAST_MODEL_HOUSE_DETAIL[row.code] = detailHouse; 3132 row.rating = detailHouse.rating; 3133 }); 3134 } 3135 ET._forecastDailyBundleIso = todayIso; 3136 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3137 refreshForecastUiAfterApply(); 3138 notifyHouseHubForecastReady(); 3139 } 3140 3141 function isHouseForecastHubPage() { 3142 try { 3143 var path = location && location.pathname ? location.pathname : ''; 3144 return /^\/2026-us-house(?:\/index\.html)?\/?$/.test(path) && !!document.getElementById('houseCdMap'); 3145 } catch (e) { 3146 return false; 3147 } 3148 } 3149 3150 function notifyHomeForecastReady() { 3151 if (typeof document === 'undefined') return; 3152 if (typeof ET.homeMapUsesForecastMode !== 'function' || !ET.homeMapUsesForecastMode()) return; 3153 setTimeout(function () { 3154 try { 3155 document.dispatchEvent(new CustomEvent('pollsmax-home-forecast-ready')); 3156 } catch (eHome) { /* IE */ } 3157 }, 0); 3158 } 3159 3160 /** Home maps use the same senate/gov/house hub bakes as hub pages â do not wait on today's live MC. */ 3161 function homeChamberHubBakeReady() { 3162 if (typeof ET.homeMapUsesForecastMode !== 'function' || !ET.homeMapUsesForecastMode()) return false;
3163 if (!ET.SENATE_HUB_BAKED_FORECAST || !ET.GOV_HUB_BAKED_FORECAST) return false; 3164 var sen = ET.FORECAST_MODEL_SENATE_DETAIL; 3165 var gov = ET.FORECAST_MODEL_GOV_DETAIL; 3166 if (!sen || !gov) return false; 3167 if (Object.keys(sen).length < FORECAST_DAILY_MIN_SENATE || 3168 Object.keys(gov).length < FORECAST_DAILY_MIN_GOV) { 3169 return false; 3170 } 3171 if (typeof document !== 'undefined' && document.getElementById('houseCdMap')) { 3172 if (typeof ET.applyHouseHubBakedForecast === 'function' && !ET._houseHubBakedForecastApplied) { 3173 try { ET.applyHouseHubBakedForecast(); } catch (eHouseBake) { /* ignore */ } 3174 } 3175 if (!ET.HOUSE_HUB_BAKED_FORECAST) return false; 3176 var house = ET.FORECAST_MODEL_HOUSE_DETAIL; 3177 if (!house || Object.keys(house).length < FORECAST_DAILY_MIN_HOUSE) return false; 3178 } 3179 return true; 3180 } 3181 3182 /** True when chamber hub bake is already applied into FORECAST_MODEL_*_DETAIL. */ 3183 function chamberHubBakedDetailsIntact(chamber) { 3184 if (chamber === 'house') { 3185 if (typeof ET.applyHouseHubBakedForecast === 'function' && !ET._houseHubBakedForecastApplied) { 3186 try { ET.applyHouseHubBakedForecast(); } catch (eHouseBake2) { /* ignore */ } 3187 } 3188 var houseMap = ET.FORECAST_MODEL_HOUSE_DETAIL; 3189 var houseSrc = ET.HOUSE_HUB_BAKED_FORECAST; 3190 if (!houseSrc || !houseMap) return false; 3191 var houseKeys = Object.keys(houseMap); 3192 if (houseKeys.length < FORECAST_DAILY_MIN_HOUSE) return false; 3193 var houseBaked = 0; 3194 var hi; 3195 for (hi = 0; hi < houseKeys.length; hi++) { 3196 if (houseMap[houseKeys[hi]] && houseMap[houseKeys[hi]]._fromHouseForecastSnapshot) houseBaked++; 3197 } 3198 return houseBaked >= FORECAST_DAILY_MIN_HOUSE; 3199 } 3200 var map = chamber === 'gov' ? ET.FORECAST_MODEL_GOV_DETAIL : ET.FORECAST_MODEL_SENATE_DETAIL; 3201 var minN = chamber === 'gov' ? FORECAST_DAILY_MIN_GOV : FORECAST_DAILY_MIN_SENATE; 3202 var bakedSrc = chamber === 'gov' ? ET.GOV_HUB_BAKED_FORECAST : ET.SENATE_HUB_BAKED_FORECAST; 3203 if (!bakedSrc || !map) return false; 3204 var keys = Object.keys(map); 3205 if (keys.length < minN) return false; 3206 var baked = 0; 3207 var i; 3208 for (i = 0; i < keys.length; i++) { 3209 if (map[keys[i]] && map[keys[i]]._fromChamberHubBaked) baked++; 3210 } 3211 return baked >= minN; 3212 } 3213 3214 /** 3215 * Senate/gov/house hub (and home): keep race-forecast bake numbers so map tips, race lists, 3216 * and forecast pages stay aligned â do not overwrite with a fresh live MC draw. 3217 */ 3218 function preferChamberHubBakedForecast(req) { 3219 req = req || forecastDailyBundleRequirements(); 3220 var onHome = typeof ET.homeMapUsesForecastMode === 'function' && ET.homeMapUsesForecastMode(); 3221 var onSenHub = typeof document !== 'undefined' && document.body && 3222 document.body.classList.contains('senate-hub-page'); 3223 var onGovHub = typeof document !== 'undefined' && document.body && 3224 document.body.classList.contains('gov-hub-page'); 3225 /* House hub page uses its own lite/bake path; home House map uses hub bake like Senate/Gov. */ 3226 if (req.house && !onHome) return false; 3227 if (!onHome && !onSenHub && !onGovHub) return false; 3228 if (req.sen && !chamberHubBakedDetailsIntact('senate')) return false; 3229 if (req.gov && !chamberHubBakedDetailsIntact('gov')) return false; 3230 if (req.house && !chamberHubBakedDetailsIntact('house')) return false; 3231 if (!req.sen && !req.gov && !req.house) return false; 3232 return true; 3233 } 3234 3235 /** Re-apply chamber hub bakes on home so Senate / Gov / House maps share hub numbers. */ 3236 ET.reapplyHomeChamberHubBakes = function () { 3237 if (typeof ET.homeMapUsesForecastMode !== 'function' || !ET.homeMapUsesForecastMode()) return; 3238 try { 3239 if (ET.SENATE_HUB_BAKED_FORECAST && typeof ET.applySenateHubBakedForecast === 'function') { 3240 ET.SENATE_HUB_BAKED_FORECAST._appliedKey = null; 3241 ET.applySenateHubBakedForecast(); 3242 } 3243 if (ET.GOV_HUB_BAKED_FORECAST && typeof ET.applyGovHubBakedForecast === 'function') { 3244 ET.GOV_HUB_BAKED_FORECAST._appliedKey = null; 3245 ET.applyGovHubBakedForecast(); 3246 } 3247 if (ET.HOUSE_HUB_BAKED_FORECAST && typeof ET.applyHouseHubBakedForecast === 'function') { 3248 ET._houseHubBakedForecastApplied = false;
3249 ET.applyHouseHubBakedForecast(); 3250 } 3251 } catch (eReapply) { /* ignore */ } 3252 }; 3253 3254 ET.isHomeForecastReady = function () { 3255 if (typeof ET.homeMapUsesForecastMode !== 'function' || !ET.homeMapUsesForecastMode()) return true; 3256 if (homeChamberHubBakeReady()) return true; 3257 var req = forecastDailyBundleRequirements(); 3258 if (!req.sen && !req.gov) return true; 3259 var todayIso = typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : '9999-12-31'; 3260 return inMemoryForecastDailyComplete(todayIso, req); 3261 }; 3262 3263 ET.whenHomeForecastReady = function (fn) { 3264 if (typeof fn !== 'function') return; 3265 if (ET.isHomeForecastReady()) { 3266 fn(); 3267 return; 3268 } 3269 var done = false; 3270 function finish() { 3271 if (done || !ET.isHomeForecastReady()) return; 3272 done = true; 3273 fn(); 3274 } 3275 document.addEventListener('pollsmax-home-forecast-ready', finish, { once: true }); 3276 var tries = 0; 3277 (function tick() { 3278 if (done) return; 3279 try { 3280 if (typeof ET.applyForecastModelRatings === 'function') ET.applyForecastModelRatings(); 3281 } catch (eApply) { /* map refresh must not stall home ready */ } 3282 if (ET.isHomeForecastReady()) { 3283 finish(); 3284 return; 3285 } 3286 if (++tries < 120) setTimeout(tick, 50); 3287 })(); 3288 }; 3289 3290 function notifyHouseHubForecastReady() { 3291 if (typeof document === 'undefined') return; 3292 setTimeout(function () { 3293 try { 3294 document.dispatchEvent(new CustomEvent('pollsmax-house-forecast-ready')); 3295 } catch (e) { /* IE */ } 3296 }, 0); 3297 } 3298 3299 ET.isHouseHubForecastReady = function () { 3300 var req = forecastDailyBundleRequirements(); 3301 if (!req.house) return true; 3302 var todayIso = typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : '9999-12-31'; 3303 return inMemoryForecastDailyComplete(todayIso, req); 3304 }; 3305 3306 ET.whenHouseHubForecastReady = function (fn) { 3307 if (typeof fn !== 'function') return; 3308 if (ET.isHouseHubForecastReady()) { 3309 fn(); 3310 return; 3311 } 3312 document.addEventListener('pollsmax-house-forecast-ready', function onReady() { 3313 fn(); 3314 }, { once: true }); 3315 setTimeout(function () { 3316 if (ET.isHouseHubForecastReady()) fn(); 3317 }, 0); 3318 }; 3319 3320 function scheduleFullForecastDailySimDeferred() { 3321 if (typeof window !== 'undefined' && window.POLLSMAX_HOUSE_HUB_LITE && isHouseForecastHubPage()) { 3322 return; 3323 } 3324 if (ET._forecastFullDailySimQueued) return; 3325 ET._forecastFullDailySimQueued = true; 3326 function runFull() { 3327 if (typeof window !== 'undefined') { 3328 window.POLLSMAX_SENATE_HUB_LITE = false; 3329 window.POLLSMAX_GOV_HUB_LITE = false; 3330 window.POLLSMAX_HOUSE_HUB_LITE = false; 3331 } 3332 ET._forecastFullDailySimQueued = false; 3333 ET.applyForecastModelRatings(); 3334 } 3335 if (typeof requestIdleCallback === 'function') { 3336 requestIdleCallback(runFull, { timeout: 12000 }); 3337 } else { 3338 setTimeout(runFull, 2500); 3339 } 3340 } 3341 3342 ET.applyForecastModelRatings = function () { 3343 if (!ET.SENATE_EDITORIAL_RATINGS_BASE && ET.SENATE_EDITORIAL_RATINGS) { 3344 ET.SENATE_EDITORIAL_RATINGS_BASE = Object.assign({}, ET.SENATE_EDITORIAL_RATINGS); 3345 } 3346 if (!ET.GOV_EDITORIAL_RATINGS_BASE && ET.GOV_EDITORIAL_RATINGS) { 3347 ET.GOV_EDITORIAL_RATINGS_BASE = Object.assign({}, ET.GOV_EDITORIAL_RATINGS); 3348 } 3349 3350 var req = forecastDailyBundleRequirements(); 3351 if (!req.sen && !req.gov && !req.house) return true; 3352 3353 var todayIso = typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : '9999-12-31'; 3354 /* Hub + home: keep chamber-hub bake so map tips, race lists, and forecast pages match. */ 3355 if (preferChamberHubBakedForecast(req)) { 3356 if (req.house && typeof ET.applyHouseHubBakedForecast === 'function') { 3357 try { ET.applyHouseHubBakedForecast(); } catch (eHousePref) { /* ignore */ } 3358 } 3359 ET._forecastDailyBundleIso = todayIso; 3360 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3361 refreshForecastUiAfterApply(); 3362 if (typeof ET.homeMapUsesForecastMode === 'function' && ET.homeMapUsesForecastMode()) { 3363 notifyHomeForecastReady(); 3364 } 3365 return true; 3366 } 3367 /* Home: keep chamber-hub bake (matches hub maps); skip live daily overwrite. */ 3368 if (homeChamberHubBakeReady()) { 3369 if (req.house && typeof ET.applyHouseHubBakedForecast === 'function') { 3370 try { ET.applyHouseHubBakedForecast(); } catch (eHouseHome) { /* ignore */ } 3371 } 3372 refreshForecastUiAfterApply(); 3373 notifyHomeForecastReady(); 3374 return true; 3375 } 3376 if (inMemoryForecastDailyComplete(todayIso, req)) {
3377 if (req.house && isHouseForecastHubPage()) notifyHouseHubForecastReady(); 3378 if (typeof ET.homeMapUsesForecastMode === 'function' && ET.homeMapUsesForecastMode()) { 3379 refreshForecastUiAfterApply(); 3380 notifyHomeForecastReady(); 3381 } 3382 return true; 3383 } 3384 3385 if (typeof window !== 'undefined' && window.POLLSMAX_SENATE_HUB_LITE && req.sen) { 3386 var senLite = applySenateHubLiteForecast(todayIso, req); 3387 if (senLite !== 'baked') scheduleFullForecastDailySimDeferred(); 3388 return true; 3389 } 3390 3391 if (typeof window !== 'undefined' && window.POLLSMAX_GOV_HUB_LITE && req.gov) { 3392 var govLite = applyGovHubLiteForecast(todayIso, req); 3393 if (govLite !== 'baked') scheduleFullForecastDailySimDeferred(); 3394 return true; 3395 } 3396 3397 if (typeof window !== 'undefined' && window.POLLSMAX_HOUSE_HUB_LITE && req.house) { 3398 applyHouseHubLiteForecast(todayIso, req); 3399 scheduleFullForecastDailySimDeferred(); 3400 return true; 3401 } 3402 3403 if (ET._forecastModelApplyInFlight) { 3404 return false; 3405 } 3406 ET._forecastModelApplyInFlight = true; 3407 try { 3408 ET.FORECAST_MODEL_ENV = ET.forecastModelNationalEnvironment(); 3409 var bundle = loadForecastDailyBundle(todayIso); 3410 var partialSave = {}; 3411 3412 if (!forecastDailyBundleMeetsRequirements(bundle, todayIso, req)) { 3413 ET._forecastDailyBundleIso = null; 3414 ET._forecastDailyBundleVersion = null; 3415 } 3416 3417 if (forecastDailyBundleMeetsRequirements(bundle, todayIso, req)) { 3418 restoreForecastFromDailyBundle(bundle, todayIso, req); 3419 ET._forecastDailyBundleIso = todayIso; 3420 } else { 3421 ET.FORECAST_MODEL_SENATE_DETAIL = ET.FORECAST_MODEL_SENATE_DETAIL || {}; 3422 ET.FORECAST_MODEL_GOV_DETAIL = ET.FORECAST_MODEL_GOV_DETAIL || {}; 3423 ET.FORECAST_MODEL_HOUSE_DETAIL = ET.FORECAST_MODEL_HOUSE_DETAIL || {}; 3424 3425 if (req.sen) { 3426 if (bundle && forecastDailySectionReady(bundle.sen, FORECAST_DAILY_MIN_SENATE)) { 3427 restoreSenateFromDailyBundle(bundle.sen, todayIso); 3428 } else { 3429 partialSave.sen = computeSenateDailySim(todayIso); 3430 } 3431 } 3432 if (req.gov) { 3433 if (bundle && forecastDailySectionReady(bundle.gov, FORECAST_DAILY_MIN_GOV)) { 3434 restoreGovFromDailyBundle(bundle.gov, todayIso); 3435 } else { 3436 partialSave.gov = computeGovDailySim(todayIso); 3437 } 3438 } 3439 if (req.house) { 3440 if (bundle && forecastDailySectionReady(bundle.house, FORECAST_DAILY_MIN_HOUSE)) { 3441 restoreHouseFromDailyBundle(bundle.house, todayIso); 3442 } else { 3443 partialSave.house = computeHouseDailySim(todayIso); 3444 } 3445 } 3446 3447 if (partialSave.sen || partialSave.gov || partialSave.house) { 3448 bundle = mergeForecastDailyBundle(todayIso, partialSave); 3449 } 3450 ET._forecastDailyBundleIso = todayIso; 3451 } 3452 3453 ET._forecastDailyBundleVersion = FORECAST_DAILY_BUNDLE_VERSION; 3454 if (ET.SENATE_EDITORIAL_BLEND_REVISION != null) { 3455 ET._forecastModelAppliedBlendRevision = ET.SENATE_EDITORIAL_BLEND_REVISION; 3456 ET._forecastModelAppliedSenateBlendRevision = ET.SENATE_EDITORIAL_BLEND_REVISION; 3457 } 3458 if (ET.GOV_EDITORIAL_BLEND_REVISION != null) { 3459 ET._forecastModelAppliedGovBlendRevision = ET.GOV_EDITORIAL_BLEND_REVISION; 3460 } 3461 refreshForecastUiAfterApply(); 3462 if (req.house && isHouseForecastHubPage()) notifyHouseHubForecastReady(); 3463 return true; 3464 } finally { 3465 ET._forecastModelApplyInFlight = false;
3466 } 3467 }; 3468 3469 /** Hub map / seat bar slot from daily 100k sim + composite (not static editorial ratings). */ 3470 ET.forecastHubRaceSlotFromModel = function (kind, key, name) { 3471 if (!key) return null; 3472 var det = typeof ET.hubAsideForecastDetail === 'function' 3473 ? ET.hubAsideForecastDetail(kind, key, name) 3474 : null; 3475 if (!det) return null; 3476 var pD = ET.forecastModelDisplayWinProbD(det); 3477 if (pD == null || !isFinite(pD)) return null; 3478 var margin = ET.forecastModelDisplayMargin(det); 3479 if (margin == null || !isFinite(margin)) margin = 0; 3480 // Color maps/bars as the slight favorite (tilt), not gray TIE â e.g. IA gov ~50/50 â Lahn tiltR. 3481 // Governors: never emit TIE/gray. Any edge in win prob or margin â that party; perfect 3482 // 50/50 â party currently holding the governorship (open seats included). 3483 var winner = 'R'; 3484 var perfectTie = false; 3485 if (pD > 0.5) winner = 'D'; 3486 else if (pD < 0.5) winner = 'R'; 3487 else if (margin > 0) winner = 'D'; 3488 else if (margin < 0) winner = 'R'; 3489 else { 3490 perfectTie = true; 3491 if (kind === 'gov') { 3492 var heldGov = (typeof ET.govFlipBaselineParty === 'function' && ET.govFlipBaselineParty(key)) 3493 || (ET.GOV_INCUMBENT_PARTY && ET.GOV_INCUMBENT_PARTY[key]) 3494 || null; 3495 winner = (heldGov === 'D' || heldGov === 'R') ? heldGov : 'R'; 3496 } 3497 } 3498 if (kind === 'senate' && ET.isSenateIndGeState && ET.isSenateIndGeState(key) && margin > 0) { 3499 winner = 'I'; 3500 } 3501 var rating = typeof ET.forecastModelWinProbToRating === 'function' 3502 ? ET.forecastModelWinProbToRating(pD, key) 3503 : (det.rating || null); 3504 if (kind === 'gov' && perfectTie && (winner === 'D' || winner === 'R')) { 3505 rating = winner === 'D' ? 'tiltD' : 'tiltR'; 3506 } 3507 var tier = rating && typeof ET.editorialRatingToMarginTier === 'function' 3508 ? ET.editorialRatingToMarginTier(rating) 3509 : (typeof ET.forecastHubMarginTier === 'function' 3510 ? ET.forecastHubMarginTier(true, Math.abs(margin)) 3511 : 'tilt'); 3512 if (kind === 'gov' && perfectTie) tier = 'tilt'; 3513 var flip = false; 3514 if (winner !== 'TIE' && winner !== 'I') { 3515 if (kind === 'senate' && typeof ET.senateHubFlipLead === 'function') { 3516 var flSen = ET.senateHubFlipLead(name, key, null, { mapSource: 'ratings', showFlips: true }); 3517 flip = flSen === 'R' || flSen === 'D'; 3518 } else if (rating && typeof ET.editorialRatingFlipLead === 'function') { 3519 flip = !!(ET.editorialRatingFlipLead(kind, key, rating, name)); 3520 } 3521 } 3522 var hasPolls = false; 3523 if (kind === 'senate' && ET.SENATE && ET.SENATE[key] && ET.SENATE[key].length) hasPolls = true; 3524 if (kind === 'gov' && typeof ET.govSeriesWithLocalFallback === 'function') { 3525 var gs = ET.govSeriesWithLocalFallback(key); 3526 hasPolls = !!(gs && gs.length); 3527 } 3528 return { 3529 winner: winner, 3530 tier: tier, 3531 flip: flip, 3532 hasPolls: hasPolls, 3533 key: key, 3534 name: name, 3535 marginAbs: Math.abs(margin), 3536 forecastRating: rating, 3537 winProbD: pD 3538 }; 3539 }; 3540 3541 ET.senateForecastHubRaceSlotFromForecast = function (name, key) { 3542 return ET.forecastHubRaceSlotFromModel('senate', key, name); 3543 }; 3544 3545 ET.govForecastHubRaceSlotFromForecast = function (key) { 3546 var nm = ET.NAME_BY_KEY && ET.NAME_BY_KEY[key]; 3547 return ET.forecastHubRaceSlotFromModel('gov', key, nm || null); 3548 }; 3549 3550 ET.forecastHubModelStateFill = function (kind, key, name, ui) { 3551 ui = ui || { showFlips: true, showMargins: false }; 3552 if (kind === 'senate' && ui.showFlips && 3553 typeof ET.senateHubMapFlipPatternFill === 'function') { 3554 var senateFlipFill = ET.senateHubMapFlipPatternFill(name, key, ui); 3555 if (senateFlipFill) return senateFlipFill; 3556 } 3557 var slot = ET.forecastHubRaceSlotFromModel(kind, key, name); 3558 if (!slot) return null; 3559 if (slot.winner === 'TIE') { 3560 // Governors must never paint gray â break remaining ties to held party tilt. 3561 if (kind === 'gov') { 3562 var heldFill = (typeof ET.govFlipBaselineParty === 'function' && ET.govFlipBaselineParty(key)) 3563 || (ET.GOV_INCUMBENT_PARTY && ET.GOV_INCUMBENT_PARTY[key]) 3564 || 'R';
3565 if (heldFill !== 'D' && heldFill !== 'R') heldFill = 'R'; 3566 slot = { 3567 winner: heldFill, 3568 tier: 'tilt', 3569 flip: false, 3570 hasPolls: !!slot.hasPolls, 3571 key: slot.key, 3572 name: slot.name, 3573 marginAbs: slot.marginAbs || 0, 3574 forecastRating: heldFill === 'D' ? 'tiltD' : 'tiltR', 3575 winProbD: slot.winProbD 3576 }; 3577 } else { 3578 return '#64748b'; 3579 } 3580 } 3581 if (slot.winner === 'I') return ET.COLORS.IND_GOLD || '#a5b2c2'; 3582 var useTierColors = typeof ET.forecastHubMapUsesTierColors === 'function' 3583 ? ET.forecastHubMapUsesTierColors(ui) 3584 : !!ui.showMargins; 3585 if (useTierColors) { 3586 if (ui.showFlips && slot.flip && (slot.winner === 'R' || slot.winner === 'D') && 3587 typeof ET.forecastHubMarginFlipPatternId === 'function') { 3588 return 'url(#' + ET.forecastHubMarginFlipPatternId(kind, slot.winner, slot.tier) + ')'; 3589 } 3590 if (typeof ET.forecastHubMarginFill === 'function') { 3591 return ET.forecastHubMarginFill(slot.winner, slot.tier); 3592 } 3593 } 3594 if (typeof ET.forecastHubSimpleMapPartyFill === 'function') { 3595 return ET.forecastHubSimpleMapPartyFill( 3596 kind, 3597 slot.winner, 3598 slot.flip ? slot.winner : null, 3599 ui 3600 ); 3601 } 3602 return slot.winner === 'D' ? ET.COLORS.BLUE : ET.COLORS.RED; 3603 }; 3604 3605 function countElectionSeatsFromForecastSlots(kind) { 3606 var r = ET.NOT_UP_BASE_R || 0; 3607 var d = ET.NOT_UP_BASE_D || 0; 3608 var toss = 0; 3609 var hasModel = kind === 'gov' 3610 ? (ET.FORECAST_MODEL_GOV_DETAIL && Object.keys(ET.FORECAST_MODEL_GOV_DETAIL).length > 0) 3611 : (ET.FORECAST_MODEL_SENATE_DETAIL && Object.keys(ET.FORECAST_MODEL_SENATE_DETAIL).length > 0); 3612 if (!hasModel) return null; 3613 if (kind === 'senate' && ET.ELECTION_2026) { 3614 for (var f in ET.ELECTION_2026) { 3615 if (!ET.ELECTION_2026.hasOwnProperty(f)) continue; 3616 var nm = ET.NAME_BY_FIPS[f]; 3617 var key = ET.KEY_BY_NAME[nm]; 3618 var slot = ET.senateForecastHubRaceSlotFromForecast(nm, key); 3619 if (!slot) continue; 3620 if (slot.winner === 'TIE') toss++; 3621 else if (slot.winner === 'R') r++; 3622 else d++; 3623 } 3624 return { r: r, d: d, tossup: toss }; 3625 } 3626 if (kind === 'gov' && ET.GOV_EDITORIAL_RATINGS) { 3627 var govNotUp = typeof ET.forecastHubNotUpCounts === 'function' 3628 ? ET.forecastHubNotUpCounts('gov') 3629 : { r: ET.GOV_NOT_UP_BASE_R || 8, d: ET.GOV_NOT_UP_BASE_D || 6 }; 3630 r = govNotUp.r; 3631 d = govNotUp.d; 3632 Object.keys(ET.GOV_EDITORIAL_RATINGS).forEach(function (key) { 3633 if (ET.govEditorialNoElectionKeys && ET.govEditorialNoElectionKeys().has(key)) return; 3634 var slot = ET.govForecastHubRaceSlotFromForecast(key); 3635 if (!slot) return; 3636 if (slot.winner === 'TIE') { 3637 var heldSim = (typeof ET.govFlipBaselineParty === 'function' && ET.govFlipBaselineParty(key)) 3638 || (ET.GOV_INCUMBENT_PARTY && ET.GOV_INCUMBENT_PARTY[key]) 3639 || 'R'; 3640 if (heldSim === 'D') d++; 3641 else r++; 3642 } else if (slot.winner === 'R') r++; 3643 else d++; 3644 }); 3645 return { r: r, d: d, tossup: 0 }; 3646 } 3647 return null; 3648 } 3649 3650 ET.countSenateSeatsFromForecastSim = function () { 3651 return countElectionSeatsFromForecastSlots('senate'); 3652 }; 3653 3654 ET.countGovSeatsFromForecastSim = function () { 3655 return countElectionSeatsFromForecastSlots('gov'); 3656 }; 3657 3658 var SENATE_HUB_STATE_SIM_DRAWS = 100000; 3659 var _senateHubStateWinProbMemo = null; 3660 3661 ET.senateHubClearStateWinProbMemo = function () { 3662 _senateHubStateWinProbMemo = {}; 3663 }; 3664 3665 /** 3666 * Per-state D win probability for Senate hub chamber MC (same 100k sim as race forecast pages). 3667 * @param {string[]} electionKeys 3668 * @param {string} cutoffIso 3669 * @param {{ drawCount?: number }} [opts] 3670 * @returns {{ key: string, pD: number, incumbent: string|null }[]} 3671 */ 3672 ET.senateHubStateWinProbsForCutoff = function (electionKeys, cutoffIso, opts) { 3673 opts = opts || {}; 3674 if ((typeof ET.forecastMathDisabledForKind === 'function' && ET.forecastMathDisabledForKind('senate')) || 3675 ET.FORECAST_MATH_DISABLED) return []; 3676 if (!electionKeys || !electionKeys.length || !cutoffIso) return []; 3677 var isBacktest = !!opts.backtest; 3678 /** Hub history uses strict as-of-day inputs; live uses the current day including same-day updates. */ 3679 var detailOpts = isBacktest ? { pollsBeforeDateOnly: true } : {}; 3680 var memoTag = 'hubWinProb|asOf' + (isBacktest ? '|hist' : '|live'); 3681 if (!_senateHubStateWinProbMemo) _senateHubStateWinProbMemo = {}; 3682 var out = []; 3683 var i; 3684 for (i = 0; i < electionKeys.length; i++) { 3685 var key = electionKeys[i]; 3686 var memoKey = key + '|' + cutoffIso + '|' + memoTag; 3687 if (_senateHubStateWinProbMemo[memoKey]) { 3688 out.push(_senateHubStateWinProbMemo[memoKey]); 3689 continue; 3690 }
3691 var nm = ET.NAME_BY_KEY && ET.NAME_BY_KEY[key]; 3692 var incumbent = null; 3693 if (nm && ET.INCUMBENT_2026) incumbent = ET.INCUMBENT_2026[nm]; 3694 if (!incumbent && nm && typeof ET.senateEditorialIncumbentParty === 'function') { 3695 incumbent = ET.senateEditorialIncumbentParty(nm, key); 3696 } 3697 var pD = 0.5; 3698 var det = null; 3699 if (nm && typeof ET.forecastModelSenateDetailAsOfDate === 'function') { 3700 det = ET.forecastModelSenateDetailAsOfDate(key, nm, cutoffIso, detailOpts); 3701 if (det && typeof ET.forecastModelDisplayWinProbD === 'function') { 3702 var displayPD = ET.forecastModelDisplayWinProbD(det); 3703 if (displayPD != null && isFinite(displayPD)) pD = displayPD; 3704 } else if (det) { 3705 if (det.winProbD != null && isFinite(det.winProbD)) { 3706 pD = det.winProbD; 3707 } else if (det.margin != null && isFinite(det.margin) && 3708 typeof ET.forecastModelMarginToWinProb === 'function') { 3709 pD = ET.forecastModelMarginToWinProb(det.margin); 3710 } 3711 } 3712 } 3713 var margin = det && det.margin != null && isFinite(det.margin) ? det.margin : null; 3714 var sigma = null; 3715 if (margin != null && typeof ET.forecastSimulationSigma === 'function') { 3716 var pollN = 0; 3717 if (ET.SENATE && ET.SENATE[key]) { 3718 pollN = racePollsOnOrBeforeDate(ET.SENATE[key], cutoffIso).length; 3719 } 3720 sigma = ET.forecastSimulationSigma(pollN, det); 3721 } 3722 var row = { key: key, pD: pD, margin: margin, sigma: sigma, incumbent: incumbent }; 3723 _senateHubStateWinProbMemo[memoKey] = row; 3724 out.push(row); 3725 } 3726 return out; 3727 }; 3728 3729 ET.FORECAST_MATH_DISABLED = false; 3730 ET.forecastMathDisabledForKind = function () { 3731 return !!ET.FORECAST_MATH_DISABLED; 3732 }; 3733 3734 function forecastMathDisabledDetail(kind, id, extras) { 3735 var detail = { 3736 kind: kind, 3737 stateKey: kind === 'house' ? null : (id || null), 3738 code: kind === 'house' ? (id || null) : null, 3739 margin: null, 3740 marginSim: null, 3741 winProbD: null, 3742 winProbDSim: null, 3743 rating: null, 3744 disabled: true, 3745 forecastDisabled: true, 3746 forecastMathDisabled: true 3747 }; 3748 if (extras && typeof extras === 'object') { 3749 for (var k in extras) { 3750 if (Object.prototype.hasOwnProperty.call(extras, k)) detail[k] = extras[k]; 3751 } 3752 } 3753 return detail; 3754 } 3755 3756 var forecastModelRaceDetailAsOfLive = ET.forecastModelRaceDetailAsOf; 3757 var forecastModelSimAggregateForDetailLive = ET.forecastModelSimAggregateForDetail; 3758 var forecastModelEnsureDetailSimLive = ET.forecastModelEnsureDetailSim; 3759 3760 ET.forecastModelRaceDetailAsOf = function (ctx, asOfIso) { 3761 if (!ctx) return null; 3762 if (ET.forecastMathDisabledForKind && ET.forecastMathDisabledForKind(ctx.kind)) { 3763 var id = ctx.kind === 'house' 3764 ? ((ctx.houseRace && ctx.houseRace.code) || ctx.houseCode || null) 3765 : (ctx.stateKey || null); 3766 return forecastMathDisabledDetail(ctx.kind || null, id, { asOfIso: asOfIso || null }); 3767 } 3768 return forecastModelRaceDetailAsOfLive(ctx, asOfIso); 3769 }; 3770 3771 ET.forecastModelRaceDetailAsOfForTrend = function (ctx, asOfIso) { 3772 return ET.forecastModelRaceDetailAsOf(ctx, asOfIso); 3773 }; 3774 3775 ET.forecastModelSimAggregateForDetail = function (det, opts) { 3776 opts = opts || {}; 3777 if (det && det.forecastDisabled) { 3778 return { 3779 disabled: true, 3780 forecastDisabled: true, 3781 margin: null, 3782 winProbD: null, 3783 margins: opts.keepMargins ? [] : null, 3784 dWon: opts.keepMargins ? [] : null, 3785 compositeMargin: det && det.margin != null ? det.margin : null, 3786 pollCount: opts.pollCount == null ? null : opts.pollCount 3787 }; 3788 } 3789 return forecastModelSimAggregateForDetailLive(det, opts); 3790 }; 3791 3792 /** 3793 * Stamp published /forecast/ bake onto a detail â hub maps and race pages must not 3794 * live-resimulate past these numbers. 3795 */ 3796 function applyRaceForecastPageBakeToDetail(detail, kind, key) {
3797 if (!detail || !kind || !key) return false; 3798 if (kind !== 'gov' && kind !== 'senate') return false; 3799 var entry = ET.RACE_FORECAST_PAGE_BAKED; 3800 if (!entry || !entry.histogram) return false; 3801 if (entry._raceKey !== (kind + ':' + key)) return false; 3802 var today = typeof ET.etTodayISO === 'function' ? ET.etTodayISO() : null; 3803 if (entry.todayIso && today && entry.todayIso !== today && entry.todayIso > today) { 3804 return false; 3805 } 3806 var h = entry.histogram; 3807 var med = h.marginMedian != null ? h.marginMedian : h.margin; 3808 if (med != null && isFinite(med)) { 3809 detail.marginSim = med; 3810 detail.marginSimMedian = med; 3811 detail.displayMargin = med; 3812 } 3813 if (h.winProbD != null && isFinite(h.winProbD)) { 3814 detail.winProbDSim = h.winProbD; 3815 detail.displayWinProbD = h.winProbD; 3816 } 3817 if (detail.winProbDSim == null || !isFinite(detail.winProbDSim)) return false; 3818 detail._fromRaceForecastBaked = true; 3819 if (typeof ET.forecastModelWinProbToRating === 'function') { 3820 detail.rating = ET.forecastModelWinProbToRating(detail.winProbDSim, key); 3821 } 3822 return true; 3823 } 3824 3825 ET.forecastModelEnsureDetailSim = function (detail, kind, key, name, asOfIso) { 3826 if (detail && detail.partyLockout) { 3827 return detail; 3828 } 3829 if (detail && (detail.forecastDisabled || (kind && ET.forecastMathDisabledForKind && ET.forecastMathDisabledForKind(kind)))) { 3830 detail.winProbDSim = null; 3831 detail.marginSim = null; 3832 detail._forecastSimSig = 'disabled'; 3833 detail.disabled = true; 3834 detail.forecastDisabled = true; 3835 detail.forecastMathDisabled = true; 3836 return detail; 3837 } 3838 /* Hub bake is aggregated from race-forecast-baked â preserve those odds/margins. */ 3839 if (detail && detail._fromChamberHubBaked && 3840 detail.winProbDSim != null && isFinite(detail.winProbDSim)) { 3841 return detail; 3842 } 3843 /* Race forecast page bake â keep page aligned with hub map / published histogram. */ 3844 if (detail && detail._fromRaceForecastBaked && 3845 detail.winProbDSim != null && isFinite(detail.winProbDSim)) { 3846 return detail; 3847 } 3848 /* Prefer published race bake over a fresh live MC draw (same source as hub tips). */ 3849 if (applyRaceForecastPageBakeToDetail(detail, kind, key)) { 3850 if (kind === 'gov') { 3851 ET.FORECAST_MODEL_GOV_DETAIL = ET.FORECAST_MODEL_GOV_DETAIL || {}; 3852 ET.FORECAST_MODEL_GOV_DETAIL[key] = detail; 3853 } else if (kind === 'senate') { 3854 ET.FORECAST_MODEL_SENATE_DETAIL = ET.FORECAST_MODEL_SENATE_DETAIL || {}; 3855 ET.FORECAST_MODEL_SENATE_DETAIL[key] = detail; 3856 } 3857 return detail; 3858 } 3859 return forecastModelEnsureDetailSimLive(detail, kind, key, name, asOfIso); 3860 }; 3861 3862 function scheduleApply() { 3863 if (typeof ET !== 'undefined' && ET._houseHubBakedForecastApplied && isHouseForecastHubPage()) { 3864 return; 3865 } 3866 var tries = 0; 3867 function tick() { 3868 tries++; 3869 if (ET.applyForecastModelRatings()) return; 3870 if (tries < 120) setTimeout(tick, 50); 3871 } 3872 tick(); 3873 } 3874 3875 if (typeof document !== 'undefined') { 3876 if (document.readyState === 'loading') { 3877 document.addEventListener('DOMContentLoaded', scheduleApply); 3878 } else { 3879 scheduleApply(); 3880 } 3881 } 3882})();
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.