function makeAnimatedTimeChart() { const csv = `Country of Birth,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024 India,22060,33961,25789,20835,17394,18967,22228,13456,15406,26540,28140,16595,9985,19460,31295,15403,20848,59525,78680,39712 Philippines,11035,15559,12191,11662,11060,11603,16149,10547,14817,27974,31925,23862,14043,19629,33887,15981,18229,41565,36860,17116 People's Republic of China,25776,34467,24349,21022,15998,13406,15569,10400,10095,21649,20070,10789,5998,9719,13454,4708,5147,10813,12635,6261 Pakistan,12426,17113,11625,9435,7833,8051,9936,5631,5283,9076,8669,5776,5086,9394,11178,4739,5546,15204,13383,5615 Iran,4982,8083,5325,4984,3825,3586,4953,3529,3383,9419,8995,3928,3521,10030,14035,4887,4853,13083,10758,4839 United States of America,5060,5118,4267,4135,3734,3716,5090,3832,4470,7360,6668,4497,3387,4229,5624,2480,3479,9242,8626,4638 United Kingdom and Overseas Territories,6978,6625,5243,4715,4357,4502,6046,4338,4767,7335,6261,4170,3010,3519,4844,2021,3043,6889,6771,3403 Syria,898,947,920,799,825,673,770,484,414,1100,1255,656,587,1596,6455,7244,10053,20517,13026,3487 South Korea,5434,7557,5859,5251,3842,3162,4093,3072,3166,5935,5955,2906,1545,2397,3354,1259,1391,2578,2184,999 Nigeria,1088,1510,1151,1206,1081,1405,2218,1262,1341,3015,4228,2158,1899,4394,5015,2283,3290,12672,14312,5931 France,2294,2649,2152,1853,2639,1931,2676,1416,2052,5727,4548,2206,2081,3836,5501,2319,2733,8175,8269,3966 Colombia,2085,3138,3784,4668,4289,3812,4080,2540,3370,7098,5116,2601,1868,2558,3234,1207,1489,4359,3760,1760 Algeria,2146,3327,2552,2149,3159,2455,3321,1586,1847,7278,5694,2468,2003,3338,4244,1594,1543,5208,5463,2369 Morocco,2338,3870,2727,2225,3370,2030,2729,1473,1893,7504,5973,2208,2148,2920,3596,1344,1298,4198,4846,2163 Iraq,2024,2976,1765,1507,1188,1055,1592,1310,2397,4623,5197,2979,2237,3945,5049,2003,2255,7750,6106,1875 Sri Lanka,4580,5649,4704,3690,3186,2917,3347,2006,2451,4145,2997,2530,1474,1710,2464,1033,976,3177,3618,1826 Bangladesh,2856,3410,2023,1873,2139,2281,2892,1486,1691,4316,3562,1730,1330,3235,3600,1515,1667,5073,4583,1813 Jamaica,3965,4855,3379,2436,1859,1853,2333,1558,1764,2600,1916,1579,1083,1718,2736,1418,1757,5435,5136,2462 Ukraine,2925,4083,2844,2514,1890,1659,2218,1342,1607,3260,3111,1774,1224,2356,3054,1366,1924,5184,4020,1780 Russia,4067,4603,3658,3321,2706,2368,2970,1697,1742,3727,2977,1305,1047,1911,2308,964,1219,2998,2750,1054 Romania,4470,5884,4680,4375,4416,3090,3729,1828,1931,2872,2050,1001,855,1171,1578,580,685,1452,1168,409 Egypt,1358,1802,1631,1467,1195,1050,1470,1010,1139,3522,4765,2392,2282,4113,4103,1509,1876,5220,4449,1739 Haiti,1668,2131,1727,1512,2057,1247,1439,755,1434,3962,4041,2605,2417,3146,4168,1459,1362,4593,4306,1590 Mexico,1467,2003,1655,1718,1846,1799,2411,1431,1619,3603,3496,2111,1538,2433,3769,1537,1739,4743,4226,1882 Afghanistan,2868,4213,3246,2553,2160,1534,1620,1200,1163,2156,1880,1596,1187,1341,1983,853,1117,3973,4186,1735 Socialist Republic of Vietnam,1866,3137,2319,2018,1647,1529,1826,1213,1135,2232,1959,1162,811,1769,2605,1152,1505,4424,5390,2545 Lebanon,1952,2407,2119,2140,2205,1824,2337,1147,1212,3081,3038,1278,1073,2138,2594,1106,1256,3485,3158,1351 Brazil,631,813,781,709,661,592,1040,754,1016,2483,2046,1009,712,1296,2048,843,1356,5292,6893,3834`; const colorMap = { India: "#e95824", "People's Republic of China": "#e71b24", Philippines: "#0036a3", Pakistan: "#0f4020", Iran: "#229a3e", "United Kingdom and Overseas Territories": "#012066", "United States of America": "#b90b2e", "South Korea": "#c00c2f", Syria: "#111", Nigeria: "#00834e", France: "#002550", Colombia: "#f7c700", Algeria: "#006331", Morocco: "#bb262c", Iraq: "#111", "Sri Lanka": "#891438", Bangladesh: "#00684d", Jamaica: "#1ca04d", Ukraine: "#015bbb", Russia: "#0037a1", Romania: "#f4ca15", Egypt: "#111", Haiti: "#053973", Mexico: "#006847", Afghanistan: "#111", "Socialist Republic of Vietnam": "#d3241c", Lebanon: "#e61b23", Brazil: "#009638", }; function parseCSV(csv) { const rowSplit = csv.split("\n"); const columnSplit = rowSplit.map((row) => row.split(",")); const headers = columnSplit[0]; const rows = columnSplit.filter((d, i) => i > 0); const data = rows.map((row) => { const obj = {}; row.forEach((entry, i) => { const value = headers[i] === "Country of Birth" ? entry.trim() : Number(entry.trim()); obj[headers[i]] = value; }); return obj; }); return data; } function formatData(raw) { const { yearMin, yearMax } = STATE; const data = raw.map((row) => { const newRow = { country: row["Country of Birth"], total: row.Total, }; for (let i = yearMin; i <= yearMax; i++) { let total = 0; for (let j = i; j >= yearMin; j--) { total += row[j]; } newRow[i] = { new: row[i], total }; } return newRow; }); return data; } const nameMap = { "People's Republic of China": "China", "United Kingdom and Overseas Territories": "U.K.", "Korea, Republic of": "South Korea", "Federal Republic of Cameroon": "Cameroon", "Socialist Republic of Vietnam": "Vietnam", "United States of America": "United States", }; function create(year, settings) { const { yearMax, yearMin } = STATE; const { rowHeight: h, rowPad: pad, numRows: num, duration: dur } = settings; const chart = d3.select(".ctv-time-chart"); const yearContainer = chart.select(".year-container"); const yearData = Array.from( { length: yearMax - yearMin + 1 }, (_, i) => i + yearMin ); yearContainer .selectAll(".year") .data(yearData) .join("div") .attr("class", "year") .classed("highlight", (d) => d === year) .style("transition-delay", settings.delay + "ms") .text((d) => d) .on("click", (e, d) => { pause(); updateToYear(d, clickSettings); }); yearContainer.append("div").attr("class", "marker"); const rows = chart .select(".row-container") .html("") .style("padding", `${pad}px ${0} ${0} ${0}px`) .style("height", (h + pad) * num + "px") .selectAll(".row") .data(data, (d) => d.country) .join("div") .attr("class", "row") .style("height", h + "px") .style("z-index", (d, i, a) => a.length - i); rows .append("div") .attr("class", "name") .text((d) => nameMap[d.country] || d.country); const barContainer = rows.append("div").attr("class", "bar-container"); const bar = barContainer .append("div") .attr("class", "bar") .style("height", h + "px"); bar.append("div").attr("class", "number inside"); bar.append("div").attr("class", "number outside"); } function update(year, settings) { const { yearMin, yearMax } = STATE; data.sort((a, b) => b[year].total - a[year].total); const { rowHeight: h, rowPad: pad, numRows: num, duration: dur } = settings; const ease = year === yearMax || settings.ease ? d3.easeQuadOut : d3.easeLinear; // const duration = year === yearMax && !settings.ease ? dur * 2 : dur; const duration = dur; const chart = d3.select(".ctv-time-chart"); const yearData = Array.from( { length: yearMax - yearMin + 1 }, (_, i) => i + yearMin ); const yearContainer = chart.select(".year-container"); yearContainer .selectAll(".year") .data(yearData) .join("div") .attr("class", "year") .style("transition-delay", settings.delay + "ms") .classed("highlight", (d) => d === year) .text((d) => `'${String(d).substring(2, 4)}`); const marker = yearContainer.select(".marker"); if (year === yearMin && settings.fromZero) { marker.style("left", 100 / (yearMax - yearMin + 1) / 2 + "%"); } marker .transition() .duration(duration) .ease(ease) .style("left", () => { const numYears = yearMax - yearMin + 1; const pad = 100 / numYears / 2; const percent = (100 * (year - yearMin)) / numYears; return percent + pad + "%"; }); const rowContainer = chart.select(".row-container"); const rows = rowContainer .selectAll(".row") .data(data, (d) => d.country) .join("div") .attr("class", "row") .style("z-index", (d, i, a) => a.length - i); rows .transition() .duration(year === yearMin && settings.fromZero ? 0 : duration) .style("transform", (d, i) => `translateY(${(h + pad) * i}px)`); const barContainer = rows.select(".bar-container"); const bar = barContainer.select(".bar"); if (year === yearMin && settings.fromZero) { bar.style("width", 0); } bar .transition() .ease(ease) .duration(duration) .style("width", (d) => (100 * d[year].total) / STATE.totalMax + "%") .style("background-color", (d) => colorMap[d.country] || "#222"); bar.classed("inside", function (d, i) { const maxWidth = barContainer.node().offsetWidth; const targetWidth = maxWidth * (d[year].total / STATE.totalMax); return targetWidth > 50 + 8; }); const insideNumber = bar.select(".number.inside"); insideNumber .transition() .duration(duration) .tween("text", function (d) { const element = d3.select(this); return function (t) { const n = d3.interpolate( d[STATE.previousYear]?.total || 0, d[year].total ); element.text(Math.round(n(t)).toLocaleString()); }; }); const outsideNumber = bar.select(".number.outside"); outsideNumber .transition() .duration(duration) .tween("text", function (d) { const element = d3.select(this); return function (t) { const n = d3.interpolate( d[STATE.previousYear]?.total || 0, d[year].total ); element.text(Math.round(n(t)).toLocaleString()); }; }); } const start = () => { STATE.interval = setInterval(() => { updateToYear(STATE.year + 1, settings); }, settings.duration); }; function updateNext() { STATE.year++; if (STATE.year > STATE.yearMax) { STATE.paused = true; clearInterval(STATE.interval); return; } update(STATE.year, settings); } function updateToYear(year, settings) { if (year > STATE.yearMax || year < STATE.yearMin) { pause(); return; } STATE.previousYear = STATE.year; STATE.year = year; update(STATE.year, settings); } const playButton = d3.select("button.play"); d3.select("button.play").on("click", () => { if (STATE.paused) { play(); } else { pause(); } }); d3.select("button.prev").on("click", () => { pause(); updateToYear(STATE.year - 1, clickSettings); playButton.classed("replay", STATE.year === STATE.yearMax); }); d3.select("button.next").on("click", () => { pause(); updateToYear(STATE.year + 1, clickSettings); playButton.classed("replay", STATE.year === STATE.yearMax); }); function pause() { STATE.paused = true; playButton.classed("playing", false); if (STATE.year === STATE.yearMax) { playButton.classed("replay", true); } clearInterval(STATE.interval); updateToYear(STATE.year, pauseSettings); } function play() { if (STATE.year === STATE.yearMax) { STATE.year = STATE.yearMin; updateToYear(STATE.year, replaySettings); } updateToYear(STATE.year + 1, settings); start(); playButton.classed("playing", true); playButton.classed("replay", false); STATE.paused = false; } const STATE = { yearMin: 2005, yearMax: 2024, totalMax: null, previousYear: null, year: null, paused: false, interval: null, firstPlay: false, }; const settings = { rowHeight: 20, rowPad: 4, numRows: 20, duration: 2000, delay: 1000, fromZero: true, // ease: true, }; const clickSettings = { ...settings, duration: 500, delay: 0, fromZero: false, ease: true, }; const replaySettings = { ...settings, duration: 0, delay: 0, fromZero: false, ease: false, }; const pauseSettings = { ...settings, duration: 250, delay: 0, ease: true, }; const json = parseCSV(csv); const data = formatData(json); // console.log(data); const totalMap = data.map((d) => d[STATE.yearMax].total); STATE.totalMax = Math.max(...totalMap); STATE.year = STATE.yearMin; data.sort((a, b) => b[STATE.year].total - a[STATE.year].total); create(STATE.year, settings); update(STATE.year, replaySettings); STATE.paused = true; // start(); // playButton.classed("playing", true); function handleIntersection(entries) { entries.forEach((entry) => { if (entry.isIntersecting && !STATE.firstPlay) { play(); STATE.firstPlay = true; } }); } const observer = new IntersectionObserver(handleIntersection, { threshold: 0.5, }); observer.observe(document.querySelector(".ctv-time-chart")); console.log("chart loaded"); } function addInteractive(interactiveFunction) { const root = document.querySelector(".root"); if (root) { const mutationTarget = root; const mutationObserverConfig = { attributes: true }; const callback = function (mutations) { for (let mutation of mutations) { if (mutation.attributeName === "data-v-app") { interactiveFunction(); } } }; const observer = new MutationObserver(callback); observer.observe(mutationTarget, mutationObserverConfig); } else { interactiveFunction(); } } addInteractive(makeAnimatedTimeChart);