One of my previous questions was how to organize the code between multiple .js files. Now I have a problem.
I have a map in d3.js divided by countries. When the user double-clicks on a country, I would like to pass a variable to another js file.
This is my html file, index.hbs:
<html lang='en'>
<head>
<meta charset='utf-8'>
<script src='https://d3js.org/d3.v5.js' charset='utf-8'></script>
<script src='https://d3js.org/topojson.v2.min.js'></script>
<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
<link href='/css/all.css' rel='stylesheet'/>
</head>
<body>
<div id='map'></div>
<script>
var viewData = {};
viewData.nuts0 = JSON.parse('{{json nuts0}}'.replace(/"/g, '"').replace(/</, ''));
viewData.CONFIG = JSON.parse('{{json CONFIG}}'.replace(/"/g, '"').replace(/</, ''));
</script>
<script src='/script/map.js' rel='script'/><{{!}}/script>
<script src='/script/other.js' rel='script'/><{{!}}/script>
</body>
</html>
map.js:
var NAME=(function map() {
var my = {};
var CONFIG = viewData.CONFIG;
var nuts0 = viewData.nuts0;
// paths
var countries;
// width and height of svg map container
var width = CONFIG.bubbleMap.width;
var height = CONFIG.bubbleMap.height;
// to check if user clicks or double click
var dblclick_timer = false;
// create Hammer projection
var projectionCurrent = d3.geoHammer()
.scale(1)
.translate([width/2, height/2]);
var projectionBase = d3.geoHammer()
.scale(1)
.translate([width/2, height/2]);
// creates a new geographic path generator with the default settings. If projection is specified, sets the current projection
var path = d3.geoPath().projection(projectionCurrent);
// creates the svg element that contains the map
var map = d3.select('#map');
var mapSvg = map.append('svg')
.attr('id', 'map-svg')
.attr('width', width)
.attr('height', height);
var mapSvgGCountry = mapSvg.append('g').attr('id', 'nuts0');
countries = topojson.feature(nuts0, nuts0.objects.nuts0);
projectionCurrent.fitSize([width, height], countries);
var mapSvgGCountryPath = mapSvgGCountry.selectAll('path')
.data(countries.features)
.enter()
.append('path');
mapSvgGCountryPath.attr('class', 'country')
.attr('fill', 'tomato')
.style('stroke', 'white')
.style('stroke-width', 1)
.attr('d', path)
.attr('id', function(c) {
return 'country' + c.properties.nuts_id;
})
.on('click', clickOrDoubleCountry);
function clickOrDoubleCountry(d, i) {
if(dblclick_timer) { // double click
clearTimeout(dblclick_timer);
dblclick_timer = false;
my.countryDoubleClicked = d.country; // <-- variable to pass
}
else { // single click
dblclick_timer = setTimeout(function() {
dblclick_timer = false;
}, 250)
}
}
return my;
}());
other.js:
(function other(NAME) {
console.log('my:', NAME.my); // undefined
console.log('my:', NAME.countryDoubleClicked); // undefined
})(NAME);
I would like to be able to read the my object created in map.js in the other.js file and then be able to access the my.countryDoubleClicked variable from other.js.
This code doesn't work, I get TypeError: NAME.my is undefined.
There are a few things going on:
First you're not revealing the my variable to show up as NAME.my in map.js:
var NAME = (function map() {
var my = {};
//...
return my;
}());
This sets NAME to my, instead of setting NAME.my to my. If you do want to do this, you can do something like this:
var NAME = (function map() {
var my = {};
//...
return {
my: my
};
}());
You can read more about this technique, called the "Revealing Module Pattern", from articles like this one: http://jargon.js.org/_glossary/REVEALING_MODULE_PATTERN.md
Second, as others have mentioned and as you've realized, since the code in other.js runs immediately, it'll run that code before the user has any chance to click on a country. Instead, you want code that can run on demand, (in this case when the user double clicks on something). In JavaScript, this is traditionally done by assigning or passing a function. For simplicity, we can assign something to my.doubleClickHandler and then invoke that function in clickOrDoubleCountry. For this I've made the country an argument passed to the handler, in addition to assigning it to NAME.my.countryDoubleClicked, but you'll probably only need to use one of them.
function clickOrDoubleCountry(d, i) {
if(dblclick_timer) { // double click
clearTimeout(dblclick_timer);
dblclick_timer = false;
my.countryDoubleClicked = d.country; // <-- variable to pass
if (my.doubleClickHandler) {
my.doubleClickHandler(d.country);
}
}
// ...
}
Then in other.js, you'd assign the function you want to run to NAME.my.doubleClickHandler:
(function other(NAME) {
NAME.my.doubleClickHandler = function (country) {
// now this code runs whenever the user double clicks on something
console.log('exposed variable', NAME.my.countryDoubleClicked); // should be the country
console.log('argument', country); // should be the same country
});
})(NAME);
So in addition to the modified other.js above, this is complete modified map.js:
var NAME=(function map() {
var my = {};
var CONFIG = viewData.CONFIG;
var nuts0 = viewData.nuts0;
// paths
var countries;
// width and height of svg map container
var width = CONFIG.bubbleMap.width;
var height = CONFIG.bubbleMap.height;
// to check if user clicks or double click
var dblclick_timer = false;
// create Hammer projection
var projectionCurrent = d3.geoHammer()
.scale(1)
.translate([width/2, height/2]);
var projectionBase = d3.geoHammer()
.scale(1)
.translate([width/2, height/2]);
// creates a new geographic path generator with the default settings. If projection is specified, sets the current projection
var path = d3.geoPath().projection(projectionCurrent);
// creates the svg element that contains the map
var map = d3.select('#map');
var mapSvg = map.append('svg')
.attr('id', 'map-svg')
.attr('width', width)
.attr('height', height);
var mapSvgGCountry = mapSvg.append('g').attr('id', 'nuts0');
countries = topojson.feature(nuts0, nuts0.objects.nuts0);
projectionCurrent.fitSize([width, height], countries);
var mapSvgGCountryPath = mapSvgGCountry.selectAll('path')
.data(countries.features)
.enter()
.append('path');
mapSvgGCountryPath.attr('class', 'country')
.attr('fill', 'tomato')
.style('stroke', 'white')
.style('stroke-width', 1)
.attr('d', path)
.attr('id', function(c) {
return 'country' + c.properties.nuts_id;
})
.on('click', clickOrDoubleCountry);
function clickOrDoubleCountry(d, i) {
if(dblclick_timer) { // double click
clearTimeout(dblclick_timer);
dblclick_timer = false;
my.countryDoubleClicked = d.country; // <-- variable to pass
if (my.doubleClickHandler) {
my.doubleClickHandler(d.country);
}
}
else { // single click
dblclick_timer = setTimeout(function() {
dblclick_timer = false;
}, 250)
}
}
return {
my: my
};
}());
If you don't want to use NAME.my for everything and want methods and variables accessible directly from NAME (e.g. NAME.countryDoubleClicked instead of NAME.my.countryDoubleClicked), you can use the original return statement return my;, just bear in mind that there will be no variable named NAME.my.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With