|
601 | 601 | const topSports = getTopSports(medalRows, 10); |
602 | 602 | const filteredRows = medalRows.filter(d => topSports.has(d.sport)); |
603 | 603 |
|
| 604 | + const readableRows = |
| 605 | + mode === "country" |
| 606 | + ? groupSmallCountries(filteredRows, 20) |
| 607 | + : filteredRows; |
| 608 | + |
604 | 609 | const sourceToMedal = d3.rollups( |
605 | | - filteredRows, |
| 610 | + readableRows, |
606 | 611 | values => d3.sum(values, d => d.value), |
607 | 612 | d => d.source, |
608 | 613 | d => d.medal |
|
614 | 619 | each medal-sport flow comes from. |
615 | 620 | */ |
616 | 621 | const medalToSport = d3.rollups( |
617 | | - filteredRows, |
| 622 | + readableRows, |
618 | 623 | values => { |
619 | 624 | const total = d3.sum(values, d => d.value); |
620 | 625 |
|
621 | 626 | const breakdown = d3.rollups( |
622 | 627 | values, |
623 | | - v => d3.sum(v, d => d.value), |
| 628 | + v => ({ |
| 629 | + value: d3.sum(v, d => d.value), |
| 630 | + countries: Array.from(new Set(v.map(d => d.originalSource || d.source))).sort(d3.ascending) |
| 631 | + }), |
624 | 632 | d => d.source |
625 | 633 | ) |
626 | | - .map(([source, value]) => ({ |
| 634 | + .map(([source, info]) => ({ |
627 | 635 | source, |
628 | | - value |
| 636 | + value: info.value, |
| 637 | + countries: info.countries |
629 | 638 | })) |
630 | 639 | .sort((a, b) => d3.descending(a.value, b.value)); |
631 | 640 |
|
|
642 | 651 | const medalNames = new Set(); |
643 | 652 | const sportNames = new Set(); |
644 | 653 |
|
645 | | - filteredRows.forEach(row => { |
| 654 | + readableRows.forEach(row => { |
646 | 655 | sourceNames.add(row.source); |
647 | 656 | medalNames.add(row.medal); |
648 | 657 | sportNames.add(row.sport); |
649 | 658 | }); |
650 | 659 |
|
651 | 660 | const orderedSources = mode === "continent" |
652 | 661 | ? continentOrder.filter(source => sourceNames.has(source)) |
653 | | - : Array.from(sourceNames).sort(d3.ascending); |
| 662 | + : Array.from(sourceNames).sort((a, b) => { |
| 663 | + if (a === "Other countries") return 1; |
| 664 | + if (b === "Other countries") return -1; |
| 665 | + return d3.ascending(a, b); |
| 666 | + }); |
654 | 667 |
|
655 | 668 | const orderedMedals = medalOrder.filter(medal => medalNames.has(medal)); |
656 | 669 | const orderedSports = Array.from(sportNames).sort(d3.ascending); |
|
725 | 738 | ); |
726 | 739 | } |
727 | 740 |
|
| 741 | + function groupSmallCountries(rows, limit = 20) { |
| 742 | + const topCountries = new Set( |
| 743 | + d3.rollups( |
| 744 | + rows, |
| 745 | + values => d3.sum(values, d => d.value), |
| 746 | + d => d.source |
| 747 | + ) |
| 748 | + .sort((a, b) => d3.descending(a[1], b[1])) |
| 749 | + .slice(0, limit) |
| 750 | + .map(([country]) => country) |
| 751 | + ); |
| 752 | + |
| 753 | + return rows.map(row => ({ |
| 754 | + ...row, |
| 755 | + originalSource: row.source, |
| 756 | + source: topCountries.has(row.source) ? row.source : "Other countries" |
| 757 | + })); |
| 758 | + } |
| 759 | + |
728 | 760 | function normalizeMedal(value) { |
729 | 761 | const text = String(value || "").trim(); |
730 | 762 |
|
|
811 | 843 | .attr("fill", "rgba(255,255,255,0.75)") |
812 | 844 | .attr("font-family", "'Elms Sans', sans-serif") |
813 | 845 | .attr("font-size", 14) |
814 | | - .text(`Flows show ${graph.mode}s → medal type → top 10 sports · ${graph.medalCount} medals shown`); |
| 846 | + .text( |
| 847 | + graph.mode === "country" |
| 848 | + ? `Flows show top medal-winning countries + other countries → medal type → top 10 sports · ${graph.medalCount} medals shown` |
| 849 | + : `Flows show continents → medal type → top 10 sports · ${graph.medalCount} medals shown` |
| 850 | + ); |
815 | 851 |
|
816 | 852 | if (!graph.links.length) { |
817 | 853 | svg.append("text") |
|
933 | 969 |
|
934 | 970 | return ` |
935 | 971 | <div style="display:flex; justify-content:space-between; gap:18px;"> |
936 | | - <span>${d.source}</span> |
| 972 | + <span> |
| 973 | + ${d.source} |
| 974 | + ${d.source === "Other countries" && d.countries |
| 975 | + ? `<br><small>${d.countries.slice(0, 12).join(", ")}${d.countries.length > 12 ? "…" : ""}</small>` |
| 976 | + : ""} |
| 977 | + </span> |
937 | 978 | <strong>${d.value} (${pct}%)</strong> |
938 | 979 | </div> |
939 | 980 | `; |
|
1036 | 1077 | source: nodeIndex.get(d.source), |
1037 | 1078 | target: nodeIndex.get(detail.sport), |
1038 | 1079 | value: d.value, |
| 1080 | + countries: d.countries || [], |
1039 | 1081 | label: `${d.source} → ${detail.sport}: ${d.value} ${detail.medal} medals` |
1040 | 1082 | })); |
1041 | 1083 |
|
|
1090 | 1132 |
|
1091 | 1133 | const pct = total ? Math.round((d.value / total) * 100) : 0; |
1092 | 1134 |
|
1093 | | - tooltip |
1094 | | - .html(` |
1095 | | - <strong>${d.label}</strong><br> |
1096 | | - Share of this ${detail.medal.toLowerCase()}-${detail.sport} flow: |
1097 | | - <strong>${pct}%</strong> |
1098 | | - `) |
| 1135 | + tooltip.html(` |
| 1136 | + <strong>${d.label}</strong><br> |
| 1137 | +
|
| 1138 | + Share of this ${detail.medal.toLowerCase()}-${detail.sport} flow: |
| 1139 | + <strong>${pct}%</strong> |
| 1140 | +
|
| 1141 | + ${ |
| 1142 | + d.source.name === "Other countries" && d.countries.length |
| 1143 | + ? ` |
| 1144 | + <div style=" |
| 1145 | + margin-top:10px; |
| 1146 | + color:#ffd166; |
| 1147 | + max-width:300px; |
| 1148 | + line-height:1.4; |
| 1149 | + font-size:12px; |
| 1150 | + "> |
| 1151 | + <strong>Countries included:</strong><br> |
| 1152 | + ${d.countries.join(", ")} |
| 1153 | + </div> |
| 1154 | + ` |
| 1155 | + : "" |
| 1156 | + } |
| 1157 | + `) |
1099 | 1158 | .style("left", `${event.pageX + 14}px`) |
1100 | 1159 | .style("top", `${event.pageY + 14}px`) |
1101 | 1160 | .style("opacity", 1); |
|
1158 | 1217 | return index === -1 ? 999 : index; |
1159 | 1218 | } |
1160 | 1219 |
|
| 1220 | + if (node.name === "Other countries") { |
| 1221 | + return 999; |
| 1222 | + } |
| 1223 | + |
1161 | 1224 | return 100; |
1162 | 1225 | } |
1163 | 1226 |
|
|
0 commit comments