root = {
let population = await d3.csv("https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2023/2023-08-22/population.csv");
let populationedFilter = population.filter(pop => pop.coa_name == "France" | pop.coa_name == "Canada" | pop.coa_name == "Germany" | pop.coa == "USA" | pop.coa == "TUR" | pop.coa == "GBR" | pop.coa == "ITA" | pop.coa == "RUS");
let populationedSimplified = d3.rollups(populationedFilter.filter(pop => pop.year == 2022), v => d3.sum(v, d => d.refugees), d => d.coa_name, d => d.coo_name);
let allCountries = populationedFilter.map(d => d.coo_name);
let groupAggregator = function(subset) {
let tempArray = [];
for (let i = 0; i < subset.length; i++) {
tempArray.push(subset[i][1]);
}
let sum = 0;
tempArray.forEach(a => {sum += a;});
return ["Other", sum];
}
let aggregatedArray = [];
for (let i = 0; i < populationedSimplified.length; i++) {
let top10 = [populationedSimplified[i][0], populationedSimplified[i][1].sort((a, b) => b[1] - a[1]).slice(0,10)];
let toBeGrouped = populationedSimplified[i][1].sort((a, b) => b[1] - a[1]).slice(10,);
aggregatedArray.push(top10);
aggregatedArray[i][1].push(groupAggregator(toBeGrouped));
}
aggregatedArray.sort((a, b) => (a[0] > b[0]) ? 1 : ((b[0] > a[0]) ? -1 : 0));
let objectTimeSeriesConverter = function(arrival, origin) {
let tempArray = [];
let year = [2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022];
if (origin == "Other") {
let top10Countries = populationedSimplified
.filter(d => d[0] == arrival)[0][1]
.sort((a, b) => b[1] - a[1])
.slice(0,10)
.map(d => d[0]);
let otherCountryRollup = d3.rollups(populationedFilter.filter(pop => pop.coa_name == arrival)
.filter(pop => allCountries.includes(pop.coo_name))
.filter(pop => !top10Countries.includes(pop.coo_name)), v => d3.sum(v, d => d.refugees), d => d.year);
for (let y = 0; y < year.length; y++) {
if (otherCountryRollup.filter(d => d[0] == year[y])[0] !== undefined) {
let tempObject = {name: false,
value: false};
tempObject.name = String(year[y]);
tempObject.value = otherCountryRollup.filter(d => d[0] == year[y])[0][1];
tempArray.push(tempObject);
}
} ;
} else {
for (let y = 0; y < year.length; y++) {
let popSelect = populationedFilter.filter(pop => pop.coa_name == arrival & pop.coo_name == origin & pop.year == year[y])
if (popSelect[0] !== undefined) {
let tempObject = {name: false,
value: false};
tempObject.name = String(year[y]);
tempObject.value = Number(popSelect[0]["refugees"]);
tempArray.push(tempObject);
} else {
let tempObject = {name: false,
value: false};
tempObject.name = String(year[y]);
tempObject.value = 0;
tempArray.push(tempObject);
};
};
};
return tempArray;
};
let objectChildConverter = function(child, k, parent) {
let tempObject = {name: false,
value: false,
children: false};
tempObject.name = child[k][0];
tempObject.value = child[k][1];
tempObject.children = objectTimeSeriesConverter(parent, child[k][0]);
return tempObject;
}
let objectParentConverter = function(data, i) {
let tempObject = {name: false,
children: false};
tempObject.name = data[i][0];
let tempChildren = []
for (let k = 0; k < data[i][1].length; k++) {
tempChildren.push(objectChildConverter(data[i][1], k, data[i][0]))
}
tempObject.children = tempChildren;
return tempObject;
};
let data = [];
for (let i = 0; i < aggregatedArray.length; i++) {
data.push(objectParentConverter(aggregatedArray,i));
}
data[6].name = "United Kingdom"
data[7].name = "United States"
return d3.hierarchy({
name: "refugee_data",
children: data})
.sum(d => d.value)
.sort((a, b) => b.name - a.name)
.eachAfter(d => d.index = d.parent ? d.parent.index = d.parent.index + 1 || 0 : 0)
}
// Creates a set of bars for the given data node, at the specified index.
function bar(svg, down, d, selector) {
const g = svg.insert("g", selector)
.attr("class", "enter")
.attr("transform", `translate(0,${marginTop + barStep * barPadding})`)
.attr("text-anchor", "end")
.style("font-weight", "300")
.style("font-size", "11px");
// .style("font-", "11px Lato Light");
const bar = g.selectAll("g")
.data(d.children)
.join("g")
.attr("cursor", d => !d.children ? null : "pointer")
.on("click", (event, d) => down(svg, d));
bar.append("text")
.attr("x", marginLeft - 6)
.attr("y", barStep * (1 - barPadding) / 2)
.attr("dy", ".35em")
.text(function(d) {
if (d.data.name.length > 18) {
return d.data.name.substring(0,18)+'...';
} else {
return d.data.name
}
});
bar.append("rect")
.attr("x", x(0))
.attr("width", d => x(d.value) - x(0))
.attr("height", barStep * (1 - barPadding));
return g;
}
async function down(svg, d) {
if (!d.children || d3.active(svg.node())) return;
if (d3.active(svg.node())) return;
const bottomLevelIndicator = ["2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021", "2022"].includes(d.data.children[0].name)
// Rebind the current node to the background.
svg.select(".background").datum(d);
// Define two sequenced transitions.
const transition1 = svg.transition().duration(duration);
const transition2 = transition1.transition();
// Mark any currently-displayed bars as exiting.
const exit = svg.selectAll(".enter")
.attr("class", "exit");
// Update the x-axis.
if (bottomLevelIndicator) {
// Entering nodes immediately obscure the clicked-on bar, so hide it.
exit.selectAll("rect")
.transition()
.attr("fill-opacity", p => p !== d ? 0 : null)
.attr("height", 2.3)
.transition()
.duration(300)
.attr("width",0);
exit.transition()
.delay(300)
.duration(duration)
.attr("fill-opacity", 0)
.remove();
svg.selectAll(".above")
.transition()
.duration(300)
.attr("fill-opacity", 0)
.attr("opacity",0);
xBottom.domain(d3.extent(d.data.children, d => new Date(d.name)));
yBottom.domain(d3.extent(d.data.children, d => d.value));
svg.append("g")
.call(xAxisBottom)
.transition()
.delay(500)
.duration(300)
.attr("fill-opacity", 1)
.attr("opacity", 1);
svg.append("g")
.call(yAxisBottom)
.transition()
.delay(500)
.duration(300)
.attr("fill-opacity", 1)
.attr("opacity", 1);
const line = d3.line()
.x(d => xBottom(new Date(d.name)))
.y(d => yBottom(d.value));
const l = length(line(d.data.children));
svg.append("path")
.datum(d.data.children)
.attr("fill", "none")
.attr("stroke", color(true))
.attr("stroke-width", 2.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-dasharray", `0,${l}`)
.attr("d", line)
.attr("class", "bottomline")
.transition()
.delay(500)
.duration(600)
.ease(d3.easeLinear)
.attr("stroke-dasharray", `${l},0`);
} else {
// Entering nodes immediately obscure the clicked-on bar, so hide it.
exit.selectAll("rect")
.attr("fill-opacity", p => p === d ? 0 : null);
// Transition exiting bars to fade out.
exit.transition(transition1)
.attr("fill-opacity", 0)
.remove();
// Enter the new bars for the clicked-on data.
// Per above, entering bars are immediately visible.
const enter = bar(svg, down, d, ".y-axis")
.attr("fill-opacity", 0);
// Have the text fade-in, even though the bars are visible.
enter.transition(transition1)
.attr("fill-opacity", 1);
// Transition entering bars to their new y-position.
enter.selectAll("g")
.attr("transform", stack(d.index))
.transition(transition1)
.attr("transform", stagger());
// Update the x-scale domain.
x.domain([0, d3.max(d.children, d => d.value)]);
svg.selectAll(".x-axis").transition(transition2)
.call(xAxis);
// Transition entering bars to the new x-scale.
enter.selectAll("g").transition(transition2)
.attr("transform", (d, i) => `translate(0,${barStep * i})`);
// Color the bars as parents; they will fade to children if appropriate.
enter.selectAll("rect")
.attr("fill", color(true))
.attr("fill-opacity", 1)
.transition(transition2)
.attr("fill", d => color(!!d.children))
.attr("width", d => x(d.value) - x(0));
}
const transition4 = svg.transition().duration(400)
svg.selectAll(".enter_header")
.attr("class", "lead exit_header")
.transition(transition4)
.attr("x",-800)
.remove();
svg.selectAll(".enter_primary_header")
.attr("class", "h3 exit_primary_header")
.transition(transition4)
.attr("x",-800)
.remove();
if (d.data.name == "refugee_data") {
primaryheader(svg, "starter");
subheader(svg, "starter");
svg.selectAll("#background_image")
.transition()
.duration(400)
.attr("opacity", "0")
.remove();
} else if (bottomLevelIndicator) {
primaryheader(svg, "bottom", d.data.name, d.parent.data.name);
subheader(svg, "bottom");
svg.selectAll("#background_image")
.transition()
.duration(400)
.attr("opacity", "0")
.remove();
} else {
primaryheader(svg, "middle", d.data.name, undefined, "right");
subheader(svg, "middle", "right");
backgroundImage(svg, d.data.name);
svg.selectAll("#background_image")
.transition()
// .delay(duration*1.9)
.duration(1500)
.attr("opacity", ".55");
};
}
function up(svg, d) {
if (!d.parent || !svg.selectAll(".exit").empty()) return;
// Rebind the current node to the background.
svg.select(".background").datum(d.parent);
const bottomLevelIndicator = ["2010", "2011", "2012", "2013", "2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021", "2022"].includes(d.data.children[0].name)
// Define two sequenced transitions.
const transition1 = svg.transition().duration(duration);
const transition2 = transition1.transition();
if (bottomLevelIndicator) {
svg.selectAll(".bottom")
.transition()
.duration(1500)
.attr("fill-opacity", 0)
.attr("opacity",0)
.remove();
svg.selectAll(".bottom")
.transition()
.duration(1500)
.attr("fill-opacity", 0)
.attr("opacity",0)
.remove();
svg.selectAll(".above")
.transition()
.duration(1500)
.attr("fill-opacity", 1)
.attr("opacity",1);
svg.selectAll(".bottomline")
.transition()
.duration(400)
.attr("stroke-dasharray", `0,1500`)
.remove();
}
// Mark any currently-displayed bars as exiting.
const exit = svg.selectAll(".enter")
.attr("class", "exit");
// Update the x-scale domain.
x.domain([0, d3.max(d.parent.children, d => d.value)]);
// Update the x-axis.
svg.selectAll(".x-axis").transition(transition1)
.call(xAxis);
//svg.selectAll("rect")
// .attr("cursor", "alias");
// .attr("class", "background")
// .attr("fill", "none");
// Transition exiting bars to the new x-scale.
exit.selectAll("g").transition(transition1)
.attr("transform", stagger());
// Transition exiting bars to the parent’s position.
exit.selectAll("g").transition(transition2)
.attr("transform", stack(d.index));
// Transition exiting rects to the new scale and fade to parent color.
exit.selectAll("rect").transition(transition1)
.attr("width", d => x(d.value) - x(0))
.attr("fill", color(true));
// Transition exiting text to fade out.
// Remove exiting nodes.
exit.transition(transition2)
.attr("fill-opacity", 0)
.remove();
// Enter the new bars for the clicked-on data's parent.
const enter = bar(svg, down, d.parent, ".exit")
.attr("fill-opacity", 0);
enter.selectAll("g")
.attr("transform", (d, i) => `translate(0,${barStep * i})`);
// Transition entering bars to fade in over the full duration.
enter.transition(transition2)
.attr("fill-opacity", 1);
// Color the bars as appropriate.
// Exiting nodes will obscure the parent bar, so hide it.
// Transition entering rects to the new x-scale.
// When the entering parent rect is done, make it visible!
<!-- enter.selectAll("rect") -->
<!-- .attr("fill", d => color(!!d.children)) -->
<!-- .attr("fill-opacity", p => p === d ? 0 : null) -->
<!-- .transition(transition2) -->
<!-- .attr("width", d => x(d.value) - x(0)) -->
<!-- .on("end", function(p) { d3.select(this).attr("fill-opacity", 1); }); -->
enter.selectAll("rect")
.attr("fill", d => color(!!d.children));
const transition4 = svg.transition().duration(400)
svg.selectAll(".enter_header")
.attr("class", "lead exit_header")
.transition(transition4)
.attr("x",800)
.remove();
svg.selectAll(".enter_primary_header")
.attr("class", "h3 exit_primary_header")
.transition(transition4)
.attr("x",800)
.remove();
if (d.parent.data.name == "refugee_data") {
primaryheader(svg, "starter");
subheader(svg, "starter");
svg.selectAll("#background_image")
.transition()
.duration(400)
.attr("opacity", "0")
.remove();
} else {
primaryheader(svg, "middle", d.parent.data.name, undefined, "left");
subheader(svg, "middle", "left");
backgroundImage(svg, d.parent.data.name);
svg.selectAll("#background_image")
.transition()
// .delay(duration*1.9)
.duration(1500)
.attr("opacity", ".55");
};
}
function stack(i) {
let value = 0;
return d => {
const t = `translate(${x(value) - x(0)},${barStep * i})`;
value += d.value;
return t;
};
}
function stagger() {
let value = 0;
return (d, i) => {
const t = `translate(${x(value) - x(0)},${barStep * i})`;
value += d.value;
return t;
};
}
x = d3.scaleLinear().range([marginLeft, width - marginRight]);
xAxis = g => g
.attr("class", "x-axis above")
.attr("transform", `translate(0,${marginTop})`)
//.style("font", "11px Lato Light")
.style("font-weight", "300")
.style("font-size", "11px")
.call(d3.axisTop(x).ticks(width / 80, "s"))
.call(g => (g.selection ? g.selection() : g).select(".domain").remove());
xBottom = d3.scaleTime()
.range([marginLeft, width - marginRight]);
xAxisBottom = g => g
.attr("class", "x-axis bottom")
.attr("transform", `translate(0, ${height - marginTop})`)
//.style("font", "11px Lato Light")
.style("font-weight", "300")
.style("font-size", "11px")
.call(d3.axisBottom(xBottom).ticks(width / 80).tickFormat(d3.timeFormat('%Y')))
.attr("opacity", 0)
.call(g => (g.selection ? g.selection() : g).select(".domain").remove());
yBottom = d3.scaleLinear()
.domain([0, 100])
.range([height - marginBottom, marginTop]);
yAxis = g => g
.attr("class", "y-axis above")
.attr("class", "above")
.attr("transform", `translate(${marginLeft + 0.5},0)`)
.call(g => g.append("line")
.attr("stroke", "currentColor")
.attr("y1", marginTop)
.attr("y2", height - marginBottom));
yAxisBottom = g => g
.attr("class", "y-axis bottom")
.attr("transform", `translate(${marginLeft + 0.5},0)`)
.attr("opacity", 0)
//.style("font", "11px Lato Light")
.style("font-weight", "300")
.style("font-size", "11px")
.call(d3.axisLeft(yBottom).ticks(height / 40, "s"))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - marginLeft - marginRight)
.attr("stroke-opacity", 0.1));
length = (path) => d3.create("svg:path").attr("d", path).node().getTotalLength();
height = {
<!-- let max = 1; -->
<!-- root.each(d => d.children && (max = Math.max(max, d.children.length))); -->
return 11 * barStep + marginTop + marginBottom;
};
marginBottom = 100;
marginLeft = 100;
marginTop = 100;
marginRight = 30;
barStep = 27;
barPadding = 3 / barStep;
color = d3.scaleOrdinal([true, false], ["#738784ff","#54534380"]);
duration = 750;
function backgroundImage(svg, name) {
let imageLink = countryImageMap.filter(a => a.nameToMap == name)[0]["link"];
svg.append("defs")
.append("linearGradient")
.attr("id", "Gradient1")
.append("stop").attr("offset", "0").attr("stop-color", "black");
svg.selectAll("#Gradient1")
.append("stop")
.attr("offset", ".5")
.attr("stop-color", "white");
svg.selectAll("#Gradient1")
.append("stop")
.attr("offset", ".87")
.attr("stop-color", "black");
svg.selectAll("defs")
.append("mask")
.attr("id", "Mask1")
.append("rect")
.attr("x",marginTop*1.1)
.attr("y1", marginTop*1.1)
.attr("y2",height - marginBottom)
.attr('width', width)
.attr('height', height - marginBottom)
.attr("fill","url(#Gradient1)");
svg.selectAll("defs")
.append("linearGradient")
.attr("id", "Gradient2")
.attr("x1", "0")
.attr("x2", "0")
.attr("y1", "0")
.attr("y2", "1")
.append("stop")
.attr("offset", ".12")
.attr("stop-color", "black")
svg.selectAll("#Gradient2")
.append("stop")
.attr("offset", "1")
.attr("stop-color", "white");
svg.selectAll("defs")
.append("mask")
.attr("id", "Mask2")
.append("rect")
.attr("mask","url(#Mask1)")
.attr("x",marginTop*1.1)
.attr("y1", marginTop*1.1)
.attr("y2",height - marginBottom)
.attr('width', width)
.attr('height', height - marginBottom)
.attr("fill","url(#Gradient2)");
svg.insert("svg", ":first-child")
.append("image")
.attr("id","background_image")
.attr('xlink:href', imageLink)
.attr("width", width-marginLeft)
.attr("height", height - marginBottom)
.attr("y", marginTop)
.attr("x", marginLeft)
.attr("preserveAspectRatio", "xMidYMid slice")
.attr("opacity", "0")
.attr("mask", "url(#Mask2)");
};
countryImageMap = [
{nameToMap: "Canada",
link: "./20230922_images/data_story_canada.jpg?fit=crop&w=200&h=200"},
{nameToMap: "France",
link: "./20230922_images//data_story_paris_skyline.jpg?fit=crop&w=200&h=200"}, // https://www.pexels.com/@alecdoua/
{nameToMap: "Germany",
link: "./20230922_images/data_story_germany.jpg?fit=crop&w=200&h=200"}, //https://www.pexels.com/@lander-lai-64457665/
{nameToMap: "Italy",
link: "./20230922_images/data_story_italy.jpg?fit=crop&w=200&h=200"}, // https://www.pexels.com/@yankrukov/f
{nameToMap: "Russian Federation",
link: "./20230922_images/data_story_kremlin.jpg?fit=crop&w=200&h=200"}, // https://www.pexels.com/@67117688/
{nameToMap: "Türkiye",
link: "./20230922_images/turkey_photo2.jpg?fit=crop&w=200&h=200"},
{nameToMap: "United Kingdom",
link: "./20230922_images/data_story_london_skyline.jpg?fit=crop&w=200&h=200"}, // https://www.pexels.com/@skitterphoto/
{nameToMap: "United States",
link: "./20230922_images/data_story_statue_of_liberty.jpg?fit=crop&w=200&h=200"} // https://www.pexels.com/@domenico-solimeno-6463499/
];
screenWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
async function subheader(svg, starter, from) {
let marginMultiple;
if (screenWidth <= 768) { // You can adjust this threshold as needed
marginMultiple = .15; // Set a smaller font size for mobile screens
} else {
marginMultiple = .85;
}
if (starter == "starter") {
const enterHeader = svg.append("text")
.attr("id","subheader_top")
//.attr("class", "enter_header")
.attr("x", -800)
.attr("y", (marginTop / 1.6))
.attr("class", "lead enter_header")
.text("Click on a country below to see the home countries")
.style("font-size", "16px");
enterHeader.transition()
.duration(400)
.attr("x", (marginLeft*marginMultiple));
} else if (starter == "middle" && from == "right") {
const enterHeader = svg.append("text")
.attr("id","subheader_top")
//.attr("class", "enter_header")
.attr("x", 800)
.attr("y", (marginTop / 1.6))
.attr("class", "lead enter_header")
.text("Click on a country below to see historical trends")
.style("font-size", "16px");
enterHeader.transition()
.duration(400)
.attr("x", (marginLeft*marginMultiple));
} else if (starter == "middle" && from == "left") {
const enterHeader = svg.append("text")
.attr("id","subheader_top")
//.attr("class", "enter_header")
.attr("x", -800)
.attr("y", (marginTop / 1.6))
.attr("class", "lead enter_header")
.text("Click on a country below to see historical trends")
.style("font-size", "16px");
enterHeader.transition()
.duration(400)
.attr("x", (marginLeft*marginMultiple));
} else {
const enterHeader = svg.append("text")
.attr("id","subheader_top")
//.attr("class", "enter_header")
.attr("x", 800)
.attr("y", (marginTop / 1.6))
.attr("class", "lead enter_header")
.text("Click anywhere to return to the previous view")
.style("font-size", "16px");
enterHeader.transition()
.duration(400)
.attr("x", (marginLeft*marginMultiple));
}
}
async function primaryheader(svg, starter, destination, origin, from) {
let marginMultiple;
let fontSize;
if (screenWidth <= 768) {
marginMultiple = .15;
fontSize = "1.20rem";
} else {
marginMultiple = .85;
fontSize = "1.25rem";
};
if (starter == "starter") {
const enterHeader = svg.append("text")
.attr("id","header_top")
.attr("class", "h3 enter_primary_header")
.attr("x", -800)
.attr("y", (marginTop / 2.8))
.attr("opacity", 0)
.style("font-size", fontSize)
.text("Current Refugees by Destination").transition()
.duration(400)
.attr("x", (marginLeft*marginMultiple));
} else if (starter == "middle" && from == "left") {
const enterHeader = svg.append("text")
.attr("id","header_top")
.attr("class", "h3 enter_primary_header")
.attr("x", -800)
.attr("y", (marginTop / 2.8))
.style("font-size", fontSize)
.text("Refugees in" + " " + destination + " by Home Country").transition()
.duration(400)
.attr("x", (marginLeft*marginMultiple));
} else if (starter == "middle" && from == "right") {
const enterHeader = svg.append("text")
.attr("id","header_top")
.attr("class", "h3 enter_primary_header")
.attr("x", 800)
.attr("y", (marginTop / 2.8))
.style("font-size", fontSize)
.text("Refugees in" + " " + destination + " by Origin").transition()
.duration(400)
.attr("x", (marginLeft*marginMultiple));
} else if (starter == "bottom") {
const enterHeader = svg.append("text")
.attr("id","header_top")
.attr("class", "h3 enter_primary_header")
.attr("x", 800)
.attr("y", (marginTop / 2.8))
.style("font-size", fontSize)
.text(destination + " Refugees in " + origin).transition()
.duration(400)
.attr("x", (marginLeft*marginMultiple));
}
}
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");
//x.domain([0, d3.max(population_for_viz, d => d.refugees)])
x.domain([0, root.value])
svg.append("rect")
.attr("pointer-events", "all")
.attr("width", width)
.attr("height", height)
.attr("class", "background")
.attr("fill","none")
.on("click", (event, d) => up(svg, d));
svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);
down(svg, root);
return svg.node();
}
Tracking Refugees w/ D3.js
Refugee destinations and origins
Concept
D3
An example of D3.js’ robust animations and drill-down features
Since 2010, many people find themselves fleeing their home country to escape war, terrorism, and poverty.
Down below is a visualization using D3, an advanced data visualization framework based in JavaScript, that illustrates the story of these refugees based on the country they arrived in.
You can click on the bars to discover where the refugees came from.
Data and Code Sources
This data came from a Tidy Tuesday posting, pulled from the refugee
R package, and was originally compiled by United Nations High Commissioner for Refugees (UNHCR) Refugee Data Finder.
The visualization used Mike Bostock’s D3 example found here, but was modified to use refugee data, incorporate background images, headers, and a line plot.
Photograph Sources:
Canada - https://www.pexels.com/@theshuttervision/
France - https://www.pexels.com/@alecdoua/
Germany - https://www.pexels.com/@lander-lai-64457665/
Italy - https://www.pexels.com/@yankrukov/
Russia - https://www.pexels.com/@67117688/
Türkiye - Me!
United Kingdom - https://www.pexels.com/@skitterphoto/
United States - https://www.pexels.com/@domenico-solimeno-6463499/