|
351 | 351 | <span class="bottom">country GDP</span> |
352 | 352 | </span> |
353 | 353 | </div> |
354 | | -
|
355 | | - <p class="fair-slider-explanation"> |
356 | | - Use the sliders to adjust how much weight population and GDP have in the fair medal calculation. |
357 | | - </p> |
358 | | -
|
359 | | - <div class="fair-sliders"> |
360 | | - <label class="fair-slider-control"> |
361 | | - <span>Population adjustment:</span> |
362 | | - <input id="fair-pop-adjustment" type="range" min="0" max="200" value="100" step="10"> |
363 | | - <strong id="fair-pop-value">100%</strong> |
364 | | - </label> |
365 | | -
|
366 | | - <label class="fair-slider-control"> |
367 | | - <span>GDP adjustment:</span> |
368 | | - <input id="fair-gdp-adjustment" type="range" min="0" max="200" value="100" step="10"> |
369 | | - <strong id="fair-gdp-value">100%</strong> |
370 | | - </label> |
371 | | - </div> |
372 | 354 | </div> |
373 | 355 | `; |
374 | 356 | } |
|
741 | 723 | Number.isFinite(meanGDP) && |
742 | 724 | meanGDP > 0 |
743 | 725 | ) { |
744 | | - const populationFactor = Math.pow(meanPopulation / population, state.populationAdjustment / 100); |
745 | | - const gdpFactor = Math.pow(meanGDP / gdp, state.gdpAdjustment / 100); |
| 726 | + const populationFactor = meanPopulation / population; |
| 727 | + const gdpFactor = meanGDP / gdp; |
746 | 728 |
|
747 | 729 | factor = populationFactor * gdpFactor; |
748 | 730 | } |
|
764 | 746 | }; |
765 | 747 | } |
766 | 748 |
|
| 749 | + function getDisplayData(country, data) { |
| 750 | + const totalActual = |
| 751 | + data.actual.Gold + data.actual.Silver + data.actual.Bronze; |
| 752 | + |
| 753 | + if (totalActual > 0) { |
| 754 | + return { |
| 755 | + ...data, |
| 756 | + similarCountry: null |
| 757 | + }; |
| 758 | + } |
| 759 | + |
| 760 | + const similarCountry = findSimilarGdpMedalCountry(country); |
| 761 | + |
| 762 | + if (!similarCountry) { |
| 763 | + return { |
| 764 | + ...data, |
| 765 | + similarCountry: null |
| 766 | + }; |
| 767 | + } |
| 768 | + |
| 769 | + const similarData = computeFairCounts(similarCountry); |
| 770 | + |
| 771 | + return { |
| 772 | + ...data, |
| 773 | + fair: similarData.fair, |
| 774 | + similarCountry |
| 775 | + }; |
| 776 | + } |
| 777 | + |
| 778 | + function findSimilarGdpMedalCountry(country) { |
| 779 | + const base = getCountryIndicators(country); |
| 780 | + if (!base.gdp) return null; |
| 781 | + |
| 782 | + const countries = getAvailableCountries(state.season, state.year); |
| 783 | + |
| 784 | + const candidates = countries |
| 785 | + .filter(candidate => candidate !== country) |
| 786 | + .map(candidate => { |
| 787 | + const medals = getMedalCounts(candidate); |
| 788 | + const totalMedals = medals.Gold + medals.Silver + medals.Bronze; |
| 789 | + |
| 790 | + if (totalMedals === 0) return null; |
| 791 | + |
| 792 | + const indicators = getCountryIndicators(candidate); |
| 793 | + if (!indicators.gdp) return null; |
| 794 | + |
| 795 | + return { |
| 796 | + country: candidate, |
| 797 | + distance: Math.abs(Math.log(indicators.gdp) - Math.log(base.gdp)) |
| 798 | + }; |
| 799 | + }) |
| 800 | + .filter(Boolean) |
| 801 | + .sort((a, b) => d3.ascending(a.distance, b.distance)); |
| 802 | + |
| 803 | + return candidates[0]?.country || null; |
| 804 | + } |
| 805 | + |
767 | 806 | function render() { |
768 | 807 | const grid = d3.select("#fair-grid"); |
769 | 808 |
|
|
772 | 811 | return; |
773 | 812 | } |
774 | 813 |
|
775 | | - const data1 = computeFairCounts(state.country1); |
776 | | - const data2 = computeFairCounts(state.country2); |
| 814 | + const rawData1 = computeFairCounts(state.country1); |
| 815 | + const rawData2 = computeFairCounts(state.country2); |
| 816 | + |
| 817 | + const data1 = getDisplayData(state.country1, rawData1); |
| 818 | + const data2 = getDisplayData(state.country2, rawData2); |
777 | 819 |
|
778 | 820 | const maxScale = d3.max([ |
779 | 821 | ...medalOrder.map(medal => data1.actual[medal] || 0), |
|
787 | 829 | grid.append("div").html(renderCountryCard(state.country1, data1, maxScale)); |
788 | 830 | grid.append("div").html(renderCountryCard(state.country2, data2, maxScale)); |
789 | 831 | renderRankingTable(); |
790 | | - |
791 | | - d3.selectAll(".fair-prediction-toggle").on("click", function () { |
792 | | - const country = this.dataset.country; |
793 | | - |
794 | | - state.expandedPredictionCountry = |
795 | | - state.expandedPredictionCountry === country ? null : country; |
796 | | - |
797 | | - render(); |
798 | | - }); |
799 | 832 | } |
800 | 833 |
|
801 | 834 | function renderCountryCard(country, data, maxScale) { |
802 | 835 | const fairUnavailable = data.factor === null; |
803 | 836 |
|
804 | | - const allFairZero = |
805 | | - !fairUnavailable && |
806 | | - medalOrder.every(medal => (data.fair[medal] || 0) === 0); |
807 | | - |
808 | 837 | return ` |
809 | 838 | <div class="fair-country-card"> |
810 | 839 | <div class="fair-country-title">${country}</div> |
| 840 | +
|
811 | 841 | <div class="fair-country-subtitle"> |
812 | 842 | Population: ${formatBig(data.population)} • |
813 | 843 | GDP: ${formatBig(data.gdp)} |
814 | 844 | </div> |
815 | 845 |
|
816 | | - ${allFairZero ? ` |
817 | | - <div class="fair-prediction-wrapper"> |
818 | | - ${state.expandedPredictionCountry === country ? ` |
819 | | - <div class="fair-whatif-panel"> |
820 | | - <div class="fair-whatif-title">What if conditions were different?</div> |
821 | | - <div class="fair-whatif-text"> |
822 | | - Adjust the Population and GDP sliders below to explore how changes in resources could affect this country's expected medal performance. |
823 | | - </div> |
824 | | -
|
825 | | - ${renderWhatIfPrediction(country)} |
826 | | - </div> |
827 | | - ` : ` |
828 | | - <button class="fair-prediction-banner fair-prediction-toggle" |
829 | | - data-country="${country}"> |
830 | | - This country won no medals. Click to simulate a possible medal expectation. |
831 | | - </button> |
832 | | - `} |
833 | | - </div> |
834 | | - ` : ""} |
835 | | -
|
836 | 846 | <div class="fair-box"> |
837 | 847 | <div class="fair-box-title">Actual medals</div> |
| 848 | +
|
838 | 849 | ${medalOrder.map(medal => { |
839 | 850 | const value = data.actual[medal] || 0; |
840 | | - const width = value === 0 ? 0 : Math.max(3, (100 * value) / maxScale); |
| 851 | + const width = |
| 852 | + value === 0 ? 0 : Math.max(3, (100 * value) / maxScale); |
841 | 853 |
|
842 | 854 | return ` |
843 | 855 | <div class="fair-medal-row"> |
844 | 856 | <div class="fair-medal-label">${medal}</div> |
845 | 857 | <div class="fair-bar-bg"> |
846 | 858 | <div class="fair-bar-fill" |
847 | | - style="width:${width}%; background:${medalColors[medal]}"></div> |
| 859 | + style="width:${width}%; background:${medalColors[medal]}"></div> |
848 | 860 | </div> |
849 | 861 | <div class="fair-medal-value">${value}</div> |
850 | 862 | </div> |
|
853 | 865 | </div> |
854 | 866 |
|
855 | 867 | <div class="fair-box"> |
856 | | - <div class="fair-box-title">Fair medals with average population and average GDP</div> |
| 868 | + <div class="fair-box-title"> |
| 869 | + Fair medals with average population and average GDP |
| 870 | + </div> |
| 871 | +
|
| 872 | + ${data.similarCountry ? ` |
| 873 | + <div class="fair-warning"> |
| 874 | + No medals for ${country}. Fair values are estimated from ${data.similarCountry}, |
| 875 | + a medal-winning country with similar GDP. |
| 876 | + </div> |
| 877 | + ` : ""} |
| 878 | +
|
857 | 879 | ${medalOrder.map(medal => { |
858 | 880 | const value = data.fair[medal]; |
859 | | - const width = value === null || value === 0 |
860 | | - ? 0 |
861 | | - : Math.max(3, (100 * value) / maxScale); |
| 881 | + const width = |
| 882 | + value === null || value === 0 |
| 883 | + ? 0 |
| 884 | + : Math.max(3, (100 * value) / maxScale); |
862 | 885 |
|
863 | 886 | return ` |
864 | 887 | <div class="fair-medal-row"> |
865 | 888 | <div class="fair-medal-label">${medal}</div> |
866 | 889 | <div class="fair-bar-bg"> |
867 | 890 | <div class="fair-bar-fill" |
868 | | - style="width:${width}%; background:${medalColors[medal]}"></div> |
| 891 | + style="width:${width}%; background:${medalColors[medal]}"></div> |
| 892 | + </div> |
| 893 | + <div class="fair-medal-value"> |
| 894 | + ${value === null ? "n/a" : formatFair(value)} |
869 | 895 | </div> |
870 | | - <div class="fair-medal-value">${formatFair(value)}</div> |
871 | 896 | </div> |
872 | 897 | `; |
873 | 898 | }).join("")} |
874 | 899 |
|
875 | | - ${fairUnavailable ? ` |
| 900 | + ${fairUnavailable && !data.similarCountry ? ` |
876 | 901 | <div class="fair-warning"> |
877 | | - Fair values are unavailable because population or GDP data is missing for this selection. |
| 902 | + Missing population or GDP data. |
878 | 903 | </div> |
879 | 904 | ` : ""} |
880 | 905 | </div> |
|
1044 | 1069 |
|
1045 | 1070 | const populationRatio = |
1046 | 1071 | indicators.population && means.meanPopulation |
1047 | | - ? indicators.population / means.meanPopulation |
| 1072 | + ? means.meanPopulation / indicators.population |
1048 | 1073 | : 1; |
1049 | 1074 |
|
1050 | 1075 | const gdpRatio = |
1051 | 1076 | indicators.gdp && means.meanGDP |
1052 | | - ? indicators.gdp / means.meanGDP |
| 1077 | + ? means.meanGDP / indicators.gdp |
1053 | 1078 | : 1; |
1054 | 1079 |
|
1055 | 1080 | const populationEffect = Math.pow(populationRatio, state.populationAdjustment / 100); |
|
0 commit comments