It's time to have some fun with maps in this #D3Thursday article. Let's use D3 with Leaflet, an open-source map library, to create an interactive map based visualization in SAS Visual Analytics (VA).
Leaflet Map
This post’s topics include:
Unlike previous examples we’ve seen as a part of this series, this example will not work out of the box without modification.
That is because you will need to provide your own access token for Leaflet to render map tiles from Mapbox.
Also unlike previous examples, this example is not generalizable to any data roles. This time we are creating a more specific visualization that expects the following data roles from the sashelp cars dataset.
DDC Roles
With the appropriate data roles set and your access token in hand, we can get down to the business of creating a Leaflet map.
function drawMap() {
// Initialize map
MAP = L.map("map", {
zoomControl: false
}).setView([LAT, LNG], ZOOM);
// Add tile layer to map
L.tileLayer(
"https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=" +
ACCESS_TOKEN,
{
maxZoom: 18,
attribution:
"Map data © <a href='https://www.openstreetmap.org/'>OpenStreetMap</a> contributors, " +
"<a href='https://creativecommons.org/licenses/by-sa/2.0/''>CC-BY-SA</a>, " +
"Imagery © <a href='https://www.mapbox.com/'>Mapbox</a>",
id: "mapbox.streets"
}
).addTo(MAP);
...
This code creates a map at a set latitude, longitude, and zoom level, and then adds map tiles using our Mapbox access token.
For more information on working with Leaflet see the links at the bottom of this post.
The true power of Leaflet comes from it’s ability to map SVG graphics to specific latitude and longitude coordinates.
...
// Add circles and popups to map for each type of car
for (let i = 0, length = DATA.length; i < length; i++) {
CIRCLES.push(
L.circle([42.3953, -94.6339] /*[35.818379, -78.757441]*/, {
color: STROKE[i % 8],
fillColor: FILL[i % 8],
fillOpacity: 0.3 * Math.pow(0.9, i),
radius: DATA[i].distMeter
}).addTo(MAP)
);
POPUPS.push(
L.popup()
.setLatLng(radiusLatLng(CIRCLES[i]))
.setContent(
"In " +
phrase(DATA[i].type) +
" you would need to live within " +
Math.round(DATA[i].distMile * 100) / 100 +
" miles to only consume 1 gallon of gas per week commuting to work."
)
.openOn(MAP)
);
}
// Open first map popup
MAP.openPopup(POPUPS[0]);
...
This code creates a series of concentric circles that visualize how far different vehicle types could travel on a set amount of gasoline. It also creates accompanying text popups that explain what the circles mean, and then opens the first of these text popups.
As mentioned earlier, this example is a specific visualization, but Leaflet’s SVG graphics are flexible enough to visualize any map data you can think of!
Up until now in this series we have always sized text statically. We have resized the elements that make up our chart dynamically, but the size of our text elements has always remained unchanged. Let’s look at how we can resize text as dynamically to fit within a dynamic container.
#key-value {
display: inline-block;
float: left;
width: 33%;
height: 100%;
}
...
// Get overall key value dimensions
KV_SVG_WIDTH = d3
.select("#" + KV_SVG_ID)
.node()
.getBoundingClientRect().width;
KV_SVG_HEIGHT = d3
.select("#" + KV_SVG_ID)
.node()
.getBoundingClientRect().height;
KV_RADIUS =
Math.min(
KV_SVG_WIDTH - 2 * KV_RING_PAD,
KV_SVG_HEIGHT - 2 * KV_RING_PAD
) / 2;
KV_INNER_RADIUS = (1 - KV_RING_WIDTH) * KV_RADIUS;
KV_EXTERNAL_PAD = KV_INNER_RADIUS * KV_EXTERNAL_PAD_PROP;
KV_CHORD =
2 *
KV_INNER_RADIUS *
Math.sin(Math.acos(1 - KV_EXTERNAL_PAD / KV_INNER_RADIUS));
...
// Create key value static
KV_STATIC = G_KV.selectAll("#key-value-static").data([DATA]);
KV_STATIC.enter()
.append("text")
...
.style("font-size", "1em")
.style("font-size", function() {
return (
(2 * KV_INNER_RADIUS - 2 * KV_STATIC_PADDING) /
this.getComputedTextLength() +
"em"
);
})
.each(function() {
KV_STATIC_HEIGHT = this.getBBox().height;
});
// Determine font size for key value type and cost text
G_KV.append("text")
.text(KV_TYPE_COST_MAX_LENGTH_STRING)
.style("font-size", "1em")
.each(function() {
const widthConstrained = KV_CHORD / this.getComputedTextLength();
const heightConstrained =
(KV_INNER_RADIUS -
KV_EXTERNAL_PAD -
KV_INTERNAL_MIN_PAD -
KV_STATIC_HEIGHT / 2) /
this.getBBox().height;
KV_TYPE_COST_FONT_SIZE =
Math.min(widthConstrained, heightConstrained) + "em";
d3.select(this).remove();
});
// Create key value type
KV_TYPE = G_KV.selectAll("#key-value-type").data([DATA]);
KV_TYPE.enter()
.append("text")
...
.attr("y", -KV_INNER_RADIUS + KV_EXTERNAL_PAD)
.style("font-size", KV_TYPE_COST_FONT_SIZE);
There’s a lot going on up there so let’s take some time and unpack it.
First, since we have both the map and key value in the same iFrame, we use CSS to allocate the appropriate amount of screen real estate to each object.
Secondly, like previous examples, we must then use the width and height of our SVG to calculate the constraining dimensions for our visualization.
Finally, we can then use these dimension variables to dynamically determine the font size for our text elements. The process for doing this follows these basic steps:
For the key-value-static text element, we know the text will always be width constrained since the text string never changes.
For the key-value-type and key-value-cost text elements, the text could be either width or height constrained. Also, we want the font size of both of these elements to be the same. To handle this situation, we find the longest string between both sets of elements and use this string to determine the font size.
The result of all this work is three text elements that scale with the size of their container and will never overlap!
In our previous examples, we allowed our transitions to overwrite each other and even used this feature to our advantage. This made sense when we were using transitions to handle enter, update, and exit events which could occur in rapid succession.
In this example, however, we aren’t handling any enter, update, or exit events. Instead, we are using selections to change what is displayed in our key value. In this situation, it doesn’t make sense to allow multiple rapid selections. Each selection should be allowed to finish than being overwritten.
To prevent any transitions from being overwritten, we can debounce our transitions with the simple addition of a Boolean variable.
let DEBOUNCE = false;
...
// Transition necessary elements on selection
function keyValSelect(index) {
// Prevent transition from being interrupted
if (DEBOUNCE) {
return;
}
...
// Transition key value ring fill and stroke color
G_KV.select("#key-value-ring")
.transition()
...
.on("start", function() {
DEBOUNCE = true;
});
...
// Transition key value cost text
G_KV.select("#key-value-cost")
.transition()
...
.on("end", function() {
DEBOUNCE = false;
});
...
}
There are four primary steps in the debouncing process shown above.
This simple process prevents our transitions from being interrupted. Note that if you choose to debounce a transition as shown above, you should ensure that your transition duration is as short as possible. Otherwise, a misclick could result in the user waiting for a transition they did not care about.
This post marks the end of the introductory section of this series. As we progress with new, more interesting visualizations in the coming weeks we will be spending less time explaining the nitty gritty of the code. Of course we will still touch on the interesting parts of each visualization, but expect much less code explanation and much more intriguing visualizations!
Next time we will create a radar chart that can have the data roles its visualizing changed from within SAS Report Viewer!
Remember to follow the D3 Thursday article series on the SAS Communities Library and the #D3Thursday Twitter hashtag. Comment below or Tweet your questions and comments!
hello!
which version of SAS is required to use this tool of interactive maps?
thanks!
The interactive maps in this post are created in SAS Visual Analytics (VA) using the data-driven content object, which is available in VA 8.2 and newer. For more information on getting started with the data-driven content object you might want to go back and check out my first post where I discuss how to setup this object in VA.
Best,
Ryan
Build your skills. Make connections. Enjoy creative freedom. Maybe change the world. Registration is now open through August 30th. Visit the SAS Hackathon homepage.
Register today!Data Literacy is for all, even absolute beginners. Jump on board with this free e-learning and boost your career prospects.