Skip to content

Building Infographics With Cloudinary and the National Park Service API

I read this fascinating article about how Cloudinary was used for a World of Warcraft (WOW) campaign, (How Cloudinary made World of Warcraft’s first-ever marketing campaign go viral). My wife is a huge WOW player, and I remember when this campaign went live. She shared the infographics that were created for her character. At the time, I thought it was impressive, but I honestly had no idea Cloudinary was used for it. I thought it would be cool to see if I could build something similar.

While not quite as exciting as a massive, online multiplayer role-playing game, I thought I’d create an example that used data from the National Park Service (NPS).

The NPS API is free with an API key and has a healthy rate limit of 1,000 calls per hour (some endpoints differ on their rates). Their documentation provides an incredible range of supported information.

For my demo, I’ll make use of the /parks endpoint, which as you can guess returns information about parks. The endpoint supports filtering parks by park code (a special identifier for parks) or state and supports pagination. You can do basic searches as well.

The information returned for a park is rather detailed, including:

  • The name as well as a description.
  • Location, both in plain text as well as in latitude and longitude.
  • A list of activities supported by the park as well as the educational topics that can be learned about by visiting the park.
  • Contact information.
  • Detailed information about when the park is open as well as exceptions for holidays.
  • Information about any required fees.
  • Important weather-related details that might impact the park.

Best of all, a set of pictures from the park are returned as well.

For our demo, we’ll get a set of parks based on a state, and for each park, use Cloudinary to generate an infographic based on a photo from the park. Let’s get started!

As mentioned above, working with the NPS API requires a free key, so be sure to register and get one first. Once we have a key, the endpoint to get parks for a particular state looks like so:

https://developer.nps.gov/api/v1/parks?stateCode=STATE&limit=100&fields=images&api_key=API_KEY

I’ve set the limit to 100, which in my testing is high enough to cover every state in America. Keep in mind though that if you need to, the API will return information about how to get the next page of results. Let’s look at a function that wraps this up nicely for us:

const NPS_KEY = process.env.NPS_KEY;

async function getParksForState(state) {

	let resp = await fetch(`https://developer.nps.gov/api/v1/parks?stateCode=${state}&limit=100&fields=images&api_key=${NPS_KEY}`);
	return (await resp.json()).data;
}
Code language: JavaScript (javascript)

This would be better with error handling, of course, but for our simple needs, it does the trick. By returning the .data key of the result, we strip out the pagination information and return an array of parks.

As mentioned above, the information returned for just one park is rather large, but if you’d like, you can see a complete park record below:

Have fun reading that if you wish, but in the next step, we’ll figure out what parts of that make sense for our infographic.

With this function in place, I then wrote some code to request the parks in Louisiana and loop over the results:

let laParks = await getParksForState('la');
console.log(`Creating infographics for ${laParks.length} parks.\n\n`);

laParks.forEach(async l => {

	// Stuff to do here!

});
Code language: JavaScript (javascript)

Alright, considering we’ve got a wealth of information coming from the API, what would make sense to include in our infographic? For this demo, I decided on the following:

  • The full name of the park.
  • Contact information for the park (phone and email).
  • The park address.

Here’s an example of one of the results:

To build this, I finally made use of the Cloudinary Node SDK. I’ve avoided it for a while now as I love how I can simply craft URLs by working with strings. I’ve avoided the JavaScript SDK because I felt like I could get by without adding that to the page load. This was less of a concern in a Node application. I also felt like my transformations will be more complex and I was hopeful the SDK could help with that.

While Cloudinary can work with remote images, my first step was to upload the park image. If you remember from the code above, I’m looping over each result from the API and assigning it to a variable, l. For my uploads, I named them according to their park code:

let options = {
	public_id: `parks/${l.parkCode}`
};
let img = await cloudinary.uploader.upload(l.images[0].url,options);
Code language: JavaScript (javascript)

Next, I worked on the content that’s displayed in the lower right of the infographic. Namely, the contact information and address. To do this, I’ll check for each part of the park record and build out a string.

let phone = '';
if(l.contacts.phoneNumbers) {
	l.contacts.phoneNumbers.forEach(p => {
		if(p.type === 'Voice') {
			phone = p.phoneNumber;
			if(p.extension !== '') phone += ` Ext: ${p.extension}`;
		}
	});
}
if(phone !== '') phone = `Phone: ${phone}`;

let email = '';
// unlike phone, always use first email
if(l.contacts.emailAddresses) {
	email = `Email: ${l.contacts.emailAddresses[0].emailAddress}`
}

let address = '';
if(l.addresses && l.addresses[0]) {
	address = l.addresses[0].line1;
	if(l.addresses[0].line2 !== '') address += `\n${l.addresses[0].line2}`;
	if(l.addresses[0].line3 !== '') address += `\n${l.addresses[0].line3}`;
	address += `\n${l.addresses[0].city} ${l.addresses[0].stateCode} ${l.addresses[0].postalCode}`;
}

let details = 
`
${phone}
${email} 

${address}
`;
Code language: JavaScript (javascript)

The net result here is a string that may contain a phone, an email, and an address. In general, these values seemed to be consistently available in the data so this always “just worked.”

Now it’s time to actually do the transformations. The general form of how this is done in the SDK is like so:

let result = cloudinary.url(img.public_id, {
	transformation: [
		// a set of transforms
	]
});

console.log(result);
Code language: JavaScript (javascript)

I’ll use the url method as all I want is the URL of the image, not an HTML image tag. I’ll then add my transformations, and as it’s an array, it was simple to add, test, and repeat.

My first transformation did a resize:

{ width: 800, height: 600, crop: 'fit' },
Code language: CSS (css)

Next, I’ll add the park name to the top left of the image:

{overlay: {font_family: 'NationalPark-VariableVF.ttf', font_size: 26, font_weight: 'bold', text: l.fullName}, color: 'white', gravity:'north_west', background:'black', border:'10px_solid_black'},
Code language: JavaScript (javascript)

Note that I’ll use the custom fonts feature with a cool, free font called National Park Typeface. Also, note the use of a border to add a black box around the text. This ensures the white text is readable.

Finally, I’ll add the large details string:

{overlay: {font_family: 'Arial', font_size: 10, font_weight: 'bold', text: details}, color: 'white', gravity:'south_east', background:'black', border:'10px_solid_black'} 
Code language: JavaScript (javascript)

For this, I switched back to Arial and a smaller font. Design is not my strongest skill, so blame any unpleasantness on me, not Cloudinary!

And that’s it! Here’s one beautiful example:

The complete source code may be found below:

const NPS_KEY = process.env.NPS_KEY;

const cloudinary = require('cloudinary').v2;
const API_KEY = process.env.API_KEY;
const API_SECRET = process.env.API_SECRET;
const CLOUD_NAME = 'raymondcamden';

cloudinary.config({
	cloud_name: CLOUD_NAME,
	api_key: API_KEY, 
	api_secret: API_SECRET, 
	secure: true
});

async function getParksForState(state) {

	let resp = await fetch(`https://developer.nps.gov/api/v1/parks?stateCode=${state}&limit=100&fields=images&api_key=${NPS_KEY}`);
	return (await resp.json()).data;
}

(async () => {

	let laParks = await getParksForState('la');
	console.log(`Creating infographics for ${laParks.length} parks.\n\n`);

	laParks.forEach(async l => {

		let options = {
			public_id: `parks/${l.parkCode}`
		};
		let img = await cloudinary.uploader.upload(l.images[0].url,options);

		let phone = '';
		if(l.contacts.phoneNumbers) {
			l.contacts.phoneNumbers.forEach(p => {
				if(p.type === 'Voice') {
					phone = p.phoneNumber;
					if(p.extension !== '') phone += ` Ext: ${p.extension}`;
				}
			});
		}
		if(phone !== '') phone = `Phone: ${phone}`;

		let email = '';
		// unlike phone, always use first email
		if(l.contacts.emailAddresses) {
			email = `Email: ${l.contacts.emailAddresses[0].emailAddress}`
		}

		let address = '';
		if(l.addresses && l.addresses[0]) {
			address = l.addresses[0].line1;
			if(l.addresses[0].line2 !== '') address += `\n${l.addresses[0].line2}`;
			if(l.addresses[0].line3 !== '') address += `\n${l.addresses[0].line3}`;
			address += `\n${l.addresses[0].city} ${l.addresses[0].stateCode} ${l.addresses[0].postalCode}`;
		}

		let details = 
`
${phone}
${email} 

${address}
`;

		/*
		Transformations:
		1) Resize
		2) Add name to top left
		3) Add details to bottom right
		*/
		
		let result = cloudinary.url(img.public_id, {
			transformation: [
				{ width: 800, height: 600, crop: 'fit' },
				{overlay: {font_family: 'NationalPark-VariableVF.ttf', font_size: 26, font_weight: 'bold', text: l.fullName}, color: 'white', gravity:'north_west', background:'black', border:'10px_solid_black'},
				{overlay: {font_family: 'Arial', font_size: 10, font_weight: 'bold', text: details}, color: 'white', gravity:'south_east', background:'black', border:'10px_solid_black'} 
			]
		});

		console.log(result);
	});


})();
Code language: JavaScript (javascript)

Have a question or want to discuss the topic of this blog? Head over to the Cloudinary Community forums or Discord and ask away!

Back to top

Featured Post