4 My features - GiovanniKaaijk/frontend-data GitHub Wiki
My features
Rendering my world map
First I wanted to render a world map in my app. To achieve this, I went looking for examples of D3 maps, I analyzed the code of these examples until I understood it, then I started projecting my own world map.
I used to following to render my worldmap:
- Topojson
- geoPath
- World-atlas
const rendermap = function (d3, topojson) {
const worldMap = d3.geoNaturalEarth1(); //comes in different styles on d3
const pathCreator = d3.geoPath().projection(worldMap);
svg.append('path')
.attr('class', 'sphere')
.attr('d', pathCreator({type: 'Sphere'}));
d3.json('https://unpkg.com/[email protected]/world/110m.json')
.then(json => {
console.log(json, json.objects.countries);
const countries = topojson.feature(json, json.objects.countries);
svg.selectAll('path')
.data(countries.features)
.enter()
.append('path')
.attr('d', pathCreator)
.attr('class', 'country')
});
}(d3, topojson);
Filtering my data
Many of the results included place names instead of countries. To replace these place names with country names, I started looking for a file that contains all capitals of all countries. I found a JSON file while searching. This file had the following format:
{country: "Afghanistan", city: "Kabul"}
{country: "Albania", city: "Tirana"}
{country: "Algeria", city: "Alger"}
etc.
After fetching this file, I went to see if a result matches a country from these data. If this was the case, I replace the city name with the country name. I do this in the following way:
// Resultaten uit de query
results.forEach((result) => {
// Als het resultaat niet overeenkomt met een land uit de array met landen.
if(!countryArray.includes(result.placeName.value)){
// Loop over alle steden om te kijken of een stad gelijk is aan het resultaat
cities.forEach((city) => {
if(city.city == result.placeName.value){
// Vervang het resultaat met het land van de stad
result.placeName.value = city.country;
}
});
}
});
Counting the objects per country
After this I wanted to see how many objects were found per country so that I could make a heat map of it. To keep track of how many objects come from a certain country I have written a loop that goes over all objects, with each object he checks which country the object comes from and then with this country a count is kept of how many objects there are in that country.
I managed to do this with the following code:
results.forEach(result => {
// Als het resultaat overeen komt met een land uit de array
if(countryArray.includes(result.placeName.value)) {
dataCount.forEach((counter) => {
// Zoek het land dat bij het resultaat hoort
if(counter.properties.name == result.placeName.value){
// Tel 1 op bij de count van dit land
counter.properties.count = counter.properties.count += 1;
// Als de counter van dit land hoger is dan de hoogste count van een land tot nu toe
if(counter.properties.count > highestCount) {
// highestCount wordt vervangen door de huidige count, deze wordt later gebruikt voor het domain van de color scale in D3
highestCount = counter.properties.count;
}
}
});
}
})
Filling the heatmap
let scaleColor = d3.scaleSqrt()
.domain([0, (state.highestCount)])
.range(['#ffffff', 'red']);
g.selectAll('path')
.data(state.dataCount)
.enter()
.append('path')
.attr('d', pathCreator)
...
.transition()
.duration(300)
.style('fill', (d) => scaleColor(d.properties.count))
First I am creating all paths of the countries, later on at .style I run a function where the data of the element has been passed into the parameters. In the function I return the color created by the scaleColor function.
Then my map looked as desired, I now wanted to add zooming and highlighting to the map.
Zooming and highlighting
I used D3 zoom to be able to zoom on my map. D3 zoom is a function that ensures that you can zoom in on your SVG element. I managed to do this with the following code:
svg.call(zoom.on('zoom', () => {
g.attr('transform', d3.event.transform);
}))
After this I wanted to make sure that the user could highlight countries. I have added that a user can hover over a country, this makes the border of the country thicker and therefore clearer. Furthermore, a user can click on a country and then the name of the country appears, so that the user can see which countries he is viewing.
Hover:
.style('stroke-opacity', 0.2)
.on('mouseover', function() {
d3.select(this)
.style('stroke-opacity', 1)
})
.on('mouseout', function() {
d3.select(this)
.style('stroke-opacity', 0.2)
}
Tooltip:
// zorgt ervoor dat de tooltip verdwijnt wanneer je niet meer over hetzelfde land hovert
.on('mouseout', function() {
tooltip.style("visibility", "hidden")
})
// laat de tooltip verschijnen met de land titel samen met het aantal objecten gevonden.
.on("click", (d) => { tooltip.style("visibility", "visible").text(d.properties.name + ' = ' + d.properties.count)})
// dit houdt de positie van de muis bij.
.on("mousemove", () => { tooltip.style("top", (event.pageY-40)+"px").style("left",(event.pageX-35)+"px")})
Timeline
After my static visualization was done, I wanted to make an interactive timeline. I made it so that if you click on a certain year, those year values were put into the query and updates the data when the query has run. I used the following codes to create this timeline:
// First I search all elements in the timeline, then I calculate the slider width
const timeLine = () => {
let nodes = document.querySelectorAll('.timeline p')
let currentWidth = document.querySelector('.timeline').offsetWidth / nodes.length
state.nodeWidth = currentWidth
currentSlider.style.width = state.nodeWidth+'px'
nodes.forEach(element => {
// pushToArray keeps track of all the timeline elements
pushToArray(element)
// Bind changeQuery to every element
element.addEventListener('click', changeQuery)
})
}
Now, when a user clicks on an element in the timeline, changeQuery will be fired. changeQuery:
// changeQuery changes the position of the slider in the timeline, then the textContent is taken to create an object containing the year values
let index = state.uniqueNodes.indexOf(this.textContent)
state.currentTime = index
currentSlider.style.left = state.nodeWidth * index + 'px';
let content = state.uniqueNodes[index]
content = content.split("-");
state.timeFilter = {
firstValue: content[0],
secondValue: content[1]
}
updateData()
async function updateData() {
// Fetch the new data with the new year values
let data = await prepareData(state.timeFilter.firstValue, state.timeFilter.secondValue)
// Count the results for the heatmap, returns an object containing the counts + the highest count
let tracker = countTracker(data, state.dataCount, state.countryArray)
state.dataCount = tracker.dataCount;
state.highestCount = tracker.highestCount;
// Reset the scale because the highestCount has changed
let scaleColor = d3.scaleSqrt()
.domain([0, (state.highestCount)])
.range(['#ffffff', 'red']);
// Select all elements to update the data
svg.selectAll('g')
.data(state.dataCount)
.enter()
.selectAll('.country')
.transition()
.duration(300)
.style('fill', (d) => scaleColor(d.properties.count))
}
Colonial history
To display the colonial history of a country, I created a function to be called when clicked on an object:
g.selectAll('path')
.data(state.dataCount)
.enter()
.append('path')
.attr('d', pathCreator)
...
.on("click", (d) => {
historicMoments(d, state.timeFilter.firstValue, state.timeFilter.secondValue);
})
When the function has been fired, a CSV containing all data will be read by D3.
export async function historicMoments(object, firsttimevalue, secondtimevalue){
await d3.csv('koloniale-data.csv').then((data) => {
renderData(data, object, firsttimevalue, secondtimevalue)
});
}
function renderData(data, object, firsttime, secondtime) {
data.forEach(event => {
// If the country name is equal to the name of the current row in the CSV, the CSV row is pushed to an array
event.CurrentNameOfTerritory === country ? eventArray.push(event) : null
})
eventArray.forEach(event => {
event.y = y
// Change date format from xxxx -xxxx to xxxx - xxxx
event.PeriodOfTime = fixDate(event.PeriodOfTime)
y += 20
})
// Render the events in a column
displayData(eventArray)
}