datasaur/site/surveyapp/static/graphscripts/map.js
2026-01-25 15:56:01 +00:00

249 lines
8.5 KiB
JavaScript

// ------VARIABLE DECLARATIONS------
const settings = document.querySelectorAll(".axis-setting")
const variable = document.querySelector(".x-axis-value")
const scope = document.querySelector(".scope")
const exportButton = document.querySelector(".export")
// Get the graph data
const data = graphData["chart_data"]
// Width/height needed for saving image to dashboard
const width = document.getElementById('graph').clientWidth;
const height = document.getElementById('graph').clientHeight;
// Get the iso code for all the countries and pair them with country names for later access
let iso = {}
const countries = Datamap.prototype.worldTopo.objects.world.geometries;
for (let i = 0, j = countries.length; i < j; i++) {
iso[countries[i].properties.name] = countries[i].id
}
// Set the projections for various parts of the world
const africa = function(element) {
let projection = d3.geo.equirectangular()
.center([19, 0])
.rotate([4.4, 0])
.scale(400)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
let path = d3.geo.path()
.projection(projection);
return {path: path, projection: projection};
}
const europe = function(element) {
let projection = d3.geo.mercator()
.center([20, 56])
.rotate([0, 0])
.scale(490)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
let path = d3.geo.path()
.projection(projection);
return {path: path, projection: projection};
}
const southAmerica = function(element) {
let projection = d3.geo.mercator()
.center([-65, -24])
.rotate([0, 0])
.scale(350)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
let path = d3.geo.path()
.projection(projection);
return {path: path, projection: projection};
}
const northAmerica = function(element) {
let projection = d3.geo.mercator()
.center([-90, 45])
.rotate([0, 0])
.scale(340)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
let path = d3.geo.path()
.projection(projection);
return {path: path, projection: projection};
}
const asia = function(element) {
let projection = d3.geo.mercator()
.center([100, 35])
.rotate([0, 0])
.scale(340)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
let path = d3.geo.path()
.projection(projection);
return {path: path, projection: projection};
}
const oceania = function(element) {
let projection = d3.geo.mercator()
.center([130, -20])
.rotate([0, 0])
.scale(400)
.translate([element.offsetWidth / 2, element.offsetHeight / 2]);
let path = d3.geo.path()
.projection(projection);
return {path: path, projection: projection};
}
// ------END OF VARIABLE DECLARATIONS------
// ------SET EVENT LISTENERS------
// Add event listener for any axis changes
settings.forEach(setting => {
setting.onchange = function(){
render(variable.options[variable.selectedIndex].value);
}
})
// Runs if variables already set (i.e. if user is choosing to edit rather than create)
if(variable.options[variable.selectedIndex].value != ''){
render(variable.options[variable.selectedIndex].value)
}
// Export button that allows user to export and download the SVG as a PNG image
exportButton.addEventListener("click", () => {
let title = document.querySelector(".title").value
let exportTitle = title == "" ? "plot.png": `${title}.png`
saveSvgAsPng(document.getElementsByTagName("svg")[0], exportTitle, {scale: 2, backgroundColor: "#FFFFFF"});
})
// ------FUNCTIONS FOR DRAWING THE GRAPH------
function render(chosenVariable){
// Needed to delete the old graph (if present)
$("#graph").parent().append( "<div id='graph' class='h-100 position-relative'/>");
$("#graph").remove();
// Get the grouped data
let groupedData = group(chosenVariable);
// Get the most common country value so that we can base our colour scale of that
let max = getMax(groupedData)
// set the legend variables, based on the maximum value. We have 5 in total,
// so each bin comprises a 5th of the maximum value
let veryHigh = `- Between ${(max/5)*4} and ${max}`
let high = `- Between ${(max/5)*3} and ${(max/5)*4 - 1}`
let medium = `- Between ${(max/5)*2} and ${(max/5)*3 - 1}`
let low = `- Between ${max/5} and ${(max/5)*2 - 1}`
let veryLow = `- Less than ${max/5}`
// Get our colour scale and link it to the values in our legend
let colourScale = getColourScale(max, veryHigh, high, medium, veryLow, low)
let result = {};
// Loop through our data, setting the colour for each
groupedData.forEach(country => {
let colour = 'defaultFill'
if(country.values < max) colour = veryHigh
if(country.values < (max / 5)*4) colour = high
if(country.values < (max / 5)*3) colour = medium
if(country.values < (max / 5)*2) colour = low
if(country.values < max / 5) colour = veryLow
let fill = {
"fillKey": colour,
"value": country.values
}
// Convert the country to 3 letter ISO code (if needed)
let countryCode = iso[country.key] == undefined ? country.key : iso[country.key]
result[countryCode] = fill
})
// Get the projection and scope of the graph
// Scope relates to whether it is focused on states of America or countries of the World
let chosenProjection = getProjection()
let usa_world = getScope()
// Draw our map with our colourscale and data
let map = new Datamap({
element: document.getElementById('graph'),
scope: usa_world,
geographyConfig: {
// Set the border colour to same colour as the default fill
highlightBorderColor: '#FC8D59',
// Customise popup to also display the values/counts if they exist
popupTemplate: function(geography, data) {
if(data == null){
return '<div class="hoverinfo"><strong>' + geography.properties.name + '</strong></div>';
}
return '<div class="hoverinfo"><strong>' + geography.properties.name + ':</strong> ' + data.value + '</div>';
}
},
projection: 'mercator',
setProjection: chosenProjection,
fills: colourScale,
data: result
});
// Add legend to the map
map.legend();
}
// Groups the data on the chosenVariable/column
function group(chosenVariable){
// We can create a 'nested' D3 object, with the key as the chosen x-axis variable
let nestedData = d3.nest().key(function(d) { return d[chosenVariable]; })
return nestedData
.rollup(function(v) { return v.length; })
.entries(data)
}
// Gets the maximum value and then increases it so it is divisible by 5 (for the 5 bins)
function getMax(groupedData){
let max = 0
for(let i = 0; i < groupedData.length; i++){
if(groupedData[i].values > max){
max = groupedData[i].values
}
}
// we now increase the maximum until it is directly divisble by 5 (as we have 5 bins)
max++;
while(max % 5 != 0){
max++;
}
return max
}
// Sets the colour scale of the map
function getColourScale(max, veryHigh, high, medium, veryLow, low){
// Save as string so that we can insert in javascript variables
let colourScale = `{
"${veryLow}": "#DEEDCF",
"${low}": "#74C67A",
"${medium}": "#1D9A6C",
"${high}": "#137177",
"${veryHigh}": "#0A2F51",
"defaultFill": "#dae4eb"
}`
// Convert to JSON object to be used in the map
return JSON.parse(colourScale)
}
// Gets the projection of the map, depending on the chosen user input
function getProjection(){
if(scope.options[scope.selectedIndex].value == "Europe"){
return europe
}else if(scope.options[scope.selectedIndex].value == "Africa"){
return africa
}else if(scope.options[scope.selectedIndex].value == "Asia"){
return asia
}else if(scope.options[scope.selectedIndex].value == "South America"){
return southAmerica
}else if(scope.options[scope.selectedIndex].value == "Australia/Oceania"){
return oceania
}else if(scope.options[scope.selectedIndex].value == "North America"){
return northAmerica
}else{
return null
}
}
// Gets the scope (either countries of the world or states of america)
function getScope(){
if(scope.options[scope.selectedIndex].value == "United States of America"){
return "usa"
}else{
return "world"
}
}
// Ajax call used to post, as we are also sending the image of the graph (not in form)
$('form').submit(function (e) {
// prevent default form submission
e.preventDefault();
// call function to post form (separate js file)
postgraph(width, height)
});