In today's data-driven landscape, effective visualization plays a crucial role in making complex data accessible and informative. In this blog post, we will explore how to harness the power of Globe.gl , coupled with some fundamental Three.js knowledge, to transform raw AWS User Groups data into an engaging and interactive globe-spanning visualization. we'll explore how AWS services can significantly expedite the deployment of our global visualization, making it accessible to audiences worldwide within minutes. Throughout this blog post, our goal is to provide you with a clear, step-by-step guide to help you effortlessly navigate this process. Below is a quick outline:
Unveiling the Data: Understanding AWS User Groups:
Our expedition starts with an exploration of the AWS User Groups dataset, shedding light on its structure. This dataset represents a global community of AWS cloud enthusiasts. Understanding its format is foundational to our visualization endeavor. We will also modify the JSON file to include latitude, longitude, description, and images for each User Group (where available) using Python and Google Maps API.Setting the Stage: Preparing for the Journey
In this section, we prepare our workspace by integrating essential tools such as Vite, http://Globe.gl, and Three.js. These foundational elements empower us to craft a visually compelling and technically robust globe visualization.Building the Globe: A Comprehensive Guide
The heart of our project unfolds as we embark on the step-by-step construction of our globe visualization. Starting from the basic HTML and CSS components and culminating in the JavaScript functions, we will build an interactive globe that showcases the global presence of AWS User Groups.Going Global in Minutes: Leveraging AWS Services
This section showcases the strategic utilization of AWS services. With Amazon S3, Lightsail, ACM, and CloudFront at our disposal, we expedite the deployment of our globe visualization to a global audience. Furthermore, we explore the integration of this visualization as a subdomain within an existing website, ensuring accessibility to a worldwide audience.
Without further ado, let’s get started!
Step 1 - Unveiling the Data: Understanding AWS User Groups:
AWS User Groups stand as vibrant local communities, bringing together AWS enthusiasts and developers from around the globe. These groups hold a wealth of information, organized within a JSON file, providing a comprehensive repository of AWS User Group details worldwide. Within this JSON file, each entry boasts essential information, including ugName, ugLocation, ugURL, and ugCategory, organized to represent the essence of each User Group. Let's take a closer look at the structure with a couple of examples:
[
{
"ugName": " AWS User Group Timisoara",
"ugLocation": "Timisoara, Romania",
"ugURL": "https://www.meetup.com/aws-timisoara ",
"ugCategory": "General AWS User Groups"
},
{
"ugName": "AWS Abidjan User Group",
"ugLocation": "Abidjan, Ivory Coast",
"ugURL": "https://www.meetup.com/groupe-meetup-abidjan-amazon-web-services/ ",
"ugCategory": "General AWS User Groups"
},
Yet, we need to go a step further by adding more information such as the latitude and longitude information for each User Group. This data is crucial for plotting locations on our globe visualization. To achieve this, we've created a Python function that extracts, processes, and enhances the dataset.
Imports & Initialization
The journey begins by ensuring that User Group names are clean and uniform. We remove any unnecessary white spaces, ensuring consistency in our data. Do not forget to import the following:
import requests
from bs4 import BeautifulSoup
import json
import os
import re
from geopy.geocoders import GoogleV3
import time
extracted_detailed_data = []
for item in extracted_data:
url = item['ugURL']
ugName = item['ugName']
ugLocation = item['ugLocation']
ugCategory = item['ugCategory']
#Clean the UG Name
ugName = re.sub("^\s+", "", ugName)
ugName = re.sub("\s{2,}", " ", ugName)
ugNameClean = ugName.replace(" ", "_")
Geocode User Group Locations
Next, we initialize variables for latitude and longitude, initially set to "NA" (Not Available). These variables will soon be populated with geographical coordinates. To determine the latitude and longitude of each User Group's location, we leverage the power of geocoding. Using the Google Maps Geocoding API, we convert User Group locations into precise coordinates.
# Initialize Latitude and Longitude Variables
ugLatitude = "NA"
ugLongitude = "NA"
api_key = "YOUR_GOOGLE_API_KEY_HERE"
geolocator = GoogleV3(api_key=api_key)
location = geolocator.geocode(ugLocation)
if location:
ugLatitude = location.latitude
ugLongitude = location.longitude
Note: you can obtain Google API Keys for free within a certain usage limit. Other geocoding libraries are also available in which you can explore.
Retrieve Additional Information
Our function doesn't stop at geographical data. It goes above and beyond, scraping additional details from the User Group's Meetup page. We retrieve:
Member Count: The number of members in the User Group.
Description: A brief description of the User Group.
Since the majority of AWS User Groups are hosted on Meetup.com, our function is finely tuned to collect information from this platform. For a data retrieval experience, we identified the "member-count-link" as the unique identifier within the HTML structure of the requested webpage (you can refer to BeautifulSoup documentation if this concept is new to you).
To ensure data retrieval and robust exception handling, our function is designed to handle unexpected obstacles that may arise during data extraction. In such situations, we gracefully fill in these details with "NA" (Not Available), ensuring a continuous and uninterrupted data enrichment process. This comprehensive approach guarantees that you receive all the data and enables you to interpret it effectively, even when faced with challenges presented by the webpage.
try:
response = requests.get(url)
if response.status_code == 200:
time.sleep(1)
soup = BeautifulSoup(response.text, "html.parser")
#Get UG Members Number
try:
member_count_element = soup.find('a', id='member-count-link')
member_count_text = member_count_element.get_text(strip=True)
ugMembersNumber = member_count_text.split()[0]
except:
ugMembersNumber = "NA"
#Get UG Description
try:
description_element = soup.find('p', class_='mb-4')
ugDescription = description_element.get_text(strip=True)
except:
ugDescription = "NA"
Retrieve User Group Images and Save Them
Visual aesthetics play a pivotal role in our project, and these images may find valuable applications in our future expansions. In a similar effort to our previous data extraction processes, our function adeptly retrieves these images from meetup.com and handles exceptions gracefully. After successfully locating and preparing the image for download, we establish a dedicated directory named "UGImages" and organize the images within it. To maintain consistency and clarity, we utilize the cleaned User Group name and the appropriate file extension as the new name for each image.
# Find the image element by its class attribute with error handling
try:
img = soup.find("img", class_="md:rounded-lg object-cover object-center aspect-video w-full")
#Extract the image source URL
img_src = img.get("src")
#Check if the image source URL is valid
if img_src and img_src.startswith("http"):
#Extract the file extension from the URL
file_extension = os.path.splitext(img_src)[1]
#Construct the filename using ugNameClean
filename = f"{ugNameClean}{file_extension}"
#Download the image
img_response = requests.get(img_src)
#Define the folder name
folder_name = "UGImages"
#Make sure the folder exists, create it if not
if not os.path.exists(folder_name):
os.makedirs(folder_name)
#Check if the image download was successful
if img_response.status_code == 200:
# Specify the full path to the file
file_path = os.path.join(folder_name, filename)
# Open and write the image content to the specified file
with open(file_path, "wb") as f:
f.write(img_response.content)
#Set ugImage to ugNameClean when an image is available
ugImage = ugNameClean
else:
# If no image found, set img_src and ugImage to "NA"
img_src = "NA"
ugImage = "NA"
except:
#If an exception occurs while finding the image, set img_src and ugImage to "NA"
img_src = "NA"
ugImage = "NA"
Data Structuring and Function Return
Finally, the extracted data is structured into a dictionary for each User Group, containing essential information like name, location (with coordinates), URL, category, member count, description, and an image (if available). These structured data entries are then appended to a list, creating a comprehensive dataset. Then, once everything is set we return the new JSON file.
#Store extracted data in a dictionary
data = {
'ugName': ugName,
'ugLocation': {
'ugLocation': ugLocation,
'ugLatitude': ugLatitude,
'ugLongitude': ugLongitude
},
'ugURL': url,
'ugCategory': ugCategory,
'ugMembersNumber': ugMembersNumber,
'ugDescription': ugDescription,
'ugImage': ugImage
}
extracted_detailed_data.append(data)
except:
data = {
'ugName': ugName,
'ugLocation': {
'ugLocation': ugLocation,
'ugLatitude': ugLatitude,
'ugLongitude': ugLongitude
},
'ugURL': url,
'ugCategory': ugCategory,
'ugMembersNumber': "NA",
'ugDescription': "NA",
'ugImage': "NA"
}
extracted_detailed_data.append(data)
return extracted_detailed_data
File Creation
Once the dataset is ready, we store it in a JSON file named 'detail_UGInfo.json,' ensuring that the data is well-preserved for our visualization project.
detail_UGInfo = extract_UGInfo(extracted_data)
if detail_UGInfo:
with open('detail_UGInfo.json', 'w', encoding='utf-8') as json_file:
json.dump(detail_UGInfo, json_file, ensure_ascii=False, indent=4)
Step 2 - Setting the Stage: Preparing for the Journey
With our data ready, it's time to lay the groundwork for our project. Begin by ensuring you have the necessary tools at your disposal: npm and npx. These tools will facilitate the installation and execution of Vite, our chosen local development tool for this venture.
To get started, open your terminal and initiate the following steps:
Create a directory named "AWSUserGroups-Globe."
Within this newly created directory, generate three subfolders: "css" with a “style.css” file, "js" with a “script.js” file, and images with your choice of a 2D earth map and sky background picture*.
Crucially, craft an "index.html" file.
Equally important, place the "detail_UGInfo.json" file—created earlier—within the same directory as the "index.html" file.
The structure for your folder should something like this:
AWSUserGroups-Globe ------ css ---- style.css
------- js ---- script.js
------- images ----- earth-night.jpg (ex)
----- sky.jpg (ex)
------- index.html
------- detail_UGInfo
*Note that we will include the Digico Solutions branding and logos within our project, please make sure you modify your code accordingly.
To get started, you can two options for acquiring the necessary libraries. The first involves importing "Globe.GL " with the commandimport Globe from 'globe.gl'; and "three.js" with import * as THREE from 'three';*, and subsequently downloading their package dependencies. Alternatively, you can opt for the second which requests these libraries directly within your HTML using script tags. For our tutorial, we will adopt the latter approach.
Here's the desired folder structure to ensure your project is properly organized:
Importing Three.js is optional when you don't require additional components to customize your Globe.GL visualization.
Step 3 - Building the Globe: A Comprehensive Guide
Three.js: Three.js is a powerful JavaScript library that simplifies 3D graphics rendering in web applications. It provides a versatile and intuitive platform for creating interactive 3D scenes, making it an ideal choice for developing immersive web-based visualizations.
Globe.GL: Globe.GL, built on top of Three.js, specializes in simplifying the creation of globe visualizations. It streamlines the process of rendering Earth-like globes with geographic data and offers numerous features and customization options. By integrating Globe.GL into our project, we leverage its capabilities to craft an engaging and interactive globe-spanning visualization.
Initialization:
After creating the project tree, navigate to it on the terminal and write the command npx vite which might prompt you to install the configuration files, enter y to continue. If everything works correctly, you should have your localhost:5173 open from your browser.
In our index.html file we will set up the document as such:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Cairo&display=swap" rel="stylesheet">
<link rel="icon" href="./images/digico-icon.ico" type="image/x-icon">
<link rel="stylesheet" type="text/css" href="./css/style.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<title>AWS User Groups</title>
</head>
<body>
<div class="loader-wrapper">
<img class="loader-img" src="./images/digico-loader-icon.png" alt="Loading...">
</div>
<div id="globeContainer">
<div id="globeViz">
</div>
<script src="//unpkg.com/globe.gl"></script>
<script src="./js/script.js"></script>
</body>
</html>
In the
section, we've configured the website for enhanced usability and aesthetics:Responsive Design: We've included the tag to ensure that the web page seamlessly adapts to various screen sizes. This is crucial for creating a mobile-friendly browsing experience.
-
Page Title: The
AWS User Groups tag sets the title of your web page, which appears in the browser tab. It serves as an identifier and provides users with context about the content they're viewing. Favicon: The website icon, stored in our "images" folder (noted as ./images/), is set up here. It's the small icon you see in your browser tab, making your site easily recognizable.
Font Selection: We've opted for the Cairo Font and imported it from Google Fonts using the appropriate link. This choice enhances the visual appeal of our web content.
CSS Styling: The page's style and layout are defined by an external CSS stylesheet linked with . This separation of styles from content ensures clean and maintainable code.
JavaScript Library: To enhance interactivity and create a loading screen that improves user experience during the initial loading process, we've imported the jQuery library with . This library streamlines DOM manipulation and facilitates smooth loading of other resources like libraries and images.
In the
Section:-
Loading Screen: We've incorporated a loading screen within a
element with the class "loader-wrapper." Inside, you'll find an image tagged with . This loading screen element serves an important purpose: it provides visual feedback to users while the web page is fetching necessary resources. This helps maintain a smooth and responsive user experience, especially when loading libraries and images. You can modify the image by replacing the path found above. -
Main Content Area: The primary section where our project unfolds is encapsulated within
. This designated space is crucial because it's where our interactive globe visualization takes center stage. The nested is where the Globe.GL library will render the 3D globe that showcases AWS User Groups around the world. Think of this as the canvas upon which our data comes to life. Library Inclusion: Right within this section, we import an essential library with . This library is responsible for rendering and animating the Globe.GL visualization on our web page. It essentially provides the necessary scripts that runs our interactive globe.
Script Integration: To complete the setup, we load our custom JavaScript script from . This script acts as the engine behind our project, orchestrating data handling, animation, and user interactions. It integrates with the Globe.GL library to create the immersive AWS User Groups visualization you'll explore.
To verify that your script is functioning as expected within the HTML, you can include a simple test message like console.log("Hello World!"); in your script.js file. Then, open your browser's developer tools (usually accessible via Command ⌘ + Option ⌥ + I on MacOS X) and navigate to the console tab.
If your script is running correctly, you should see a message logged in the console, similar to this:
Functionality 1 - Loader Screen:
In the script.js file, we establish a loading screen effect with the following code:
$(window).on("load", function () {
$(".loader-wrapper").fadeOut("slow");
});
This code ensures that when the web page loads, the loading screen (defined by the .loader-wrapper class in our HTML) smoothly fades out.
To style the loading screen as a full-screen element with a rotating animation, we've applied CSS rules in the style.css file. These rules are designed to create an engaging loading experience:
.loader-wrapper {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: #000000;
display: flex;
justify-content: center;
align-items: center;
}
.loader-img {
display: inline-block;
width: auto;
height: auto;
position: relative;
animation: rotateImage 3s infinite ease-out;
}
You should see some similar to this:
Once the page loads, this screen should disappear.
Functionality 2 - Creating the Globe:
The heart of our project is the globe visualization created using the globe.gl library. This dynamic globe serves as a canvas for displaying AWS User Groups worldwide. We configure various aspects of the globe, such as point colors, labels, and animations. Additionally, we load custom globe and background images to enhance the visual experience.
We want to first create the globe with the following code:
let globe = Globe()
.globeImageUrl('./images/earth-night.jpg')
.backgroundImageUrl('./images/starry-sky.png')
(document.getElementById('globeViz'));
We kick off the process by fetching details about AWS User Groups from the 'detail_UGInfo.json' file using the 'fetch' function. This dataset contains essential information about each user group, such as their name, location, URL, category, membership count, and descriptions. This data serves as the foundation for creating our interactive globe visualization.
To properly display these groups on the globe, we first create 'pinsData.' This configuration extracts latitude and longitude coordinates from the JSON dataset. Additionally, it assigns attributes such as size, color, and information about each group. These attributes determine how the pins appear and behave on the globe.
const pinsData = jsonData.map(item => ({
lat: item.ugLocation.ugLatitude,
lng: item.ugLocation.ugLongitude,
size: 0.025,
color: 'gold',
info: item
}));
Next, we instruct our globe visualization to utilize the 'pinsData' dataset. We configure the globe with specific attributes, defining its appearance and interactivity. These attributes include the globe image, background image, hover precision for lines, and the behavior of the pins based on their size and color.
let p_data;
let globe;
fetch('detail_UGInfo.json')
.then(response => response.json())
.then(jsonData => {
p_data = jsonData;
//Pin config
//Extract latitudes and longitudes from JSON data
const pinsData = jsonData.map(item => ({
lat: item.ugLocation.ugLatitude,
lng: item.ugLocation.ugLongitude,
size: 0.025,
color: 'gold',
info: item
}));
globe = Globe()
.globeImageUrl('./images/earth-night.jpg')
.backgroundImageUrl('./images/starry-sky.png')
.lineHoverPrecision(0)
.pointsData(pinsData)
.pointAltitude('size')
.pointColor('color')
(document.getElementById('globeViz'));
})
.catch(error => {
console.error('Error loading JSON:', error);
});
But what if we want to enhance user interaction by allowing them to hover over a location marker and view detailed information about each AWS User Group? We can achieve this by customizing the globe to include a '.pointLabel' feature, which facilitates on-hover actions and data display using HTML. This feature draws upon the 'info' attribute derived from our data-fetching process.
.pointLabel(d => `<div id="info-box"><h3>${d.info.ugName}</h3>
<p><strong>Location:</strong> ${d.info.ugLocation.ugLocation}</p>
<p><strong>Members:</strong> ${d.info.ugMembersNumber}</p>
<p><strong>URL:</strong> <a href="${d.info.ugURL}" target="_blank">${d.info.ugURL}</a></p></div>`)
Also, with the '.onPointClick' function, we can enable users to open the URL associated with an AWS User Group in a new browser tab when they click on a group's location marker.
.onPointClick(d => window.open(d.info.ugURL, '_blank'))
Now, the full code for our globe function looks like this:
let p_data;
let globe;
fetch('detail_UGInfo.json')
.then(response => response.json())
.then(jsonData => {
p_data = jsonData;
//Pin config
//Extract latitudes and longitudes from JSON data
const pinsData = jsonData.map(item => ({
lat: item.ugLocation.ugLatitude,
lng: item.ugLocation.ugLongitude,
size: 0.025,
color: 'gold',
info: item
}));
globe = Globe()
.globeImageUrl('./images/earth-night.jpg')
.backgroundImageUrl('./images/starry-sky.png')
.lineHoverPrecision(0)
.pointsData(pinsData)
.pointAltitude('size')
.pointColor('color')
.pointLabel(d => `<div id="info-box"><h3>${d.info.ugName}</h3>
<p><strong>Location:</strong> ${d.info.ugLocation.ugLocation}</p>
<p><strong>Members:</strong> ${d.info.ugMembersNumber}</p>
<p><strong>URL:</strong> <a href="${d.info.ugURL}" target="_blank">${d.info.ugURL}</a></p></div>`)
.onPointClick(d => window.open(d.info.ugURL, '_blank'))
(document.getElementById('globeViz'));
})
.catch(error => {
console.error('Error loading JSON:', error);
});
You might have noticed that the visualization is not quite as appealing as we'd like it to be. Don't worry; we can fix that! In our style.css file, we have the power to enhance the aesthetics to perfectly match our preferences.
body {
margin: auto;
background-color: #000000;
}
#info-box {
padding-top: 5px;
padding-bottom: 10px;
padding-left: 20px;
padding-right: 20px;
background-color: rgba(0, 0, 0, 0.4);
color: #ffffff;
}
h3 {
font-size: 25px;
margin-bottom: 10px;
}
p+p {
margin-top: 5px;
}
a {
color: #3498db;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
Notice: our #info-box is the hovering card in our globe visualization, you can further modify as needed.
Functionality 3 - Arc Data Handling:
One of the nice features we can add to our project is the dynamic arcs that connect different AWS User Groups on the globe. To achieve this, we have two functions: initArcDataSet and updateArcDataSet. The former lays the foundation by generating a set of random arcs based on our loaded data. It's the initial step in creating the connections you'll witness. We pick random start and end points for each arc, ensuring they fall within the data's geographical bounds and avoiding duplicates. This gives us a diverse and engaging display of arcs., while the latter build upon the arcs created by initArcDataSet, this function keeps our visualization dynamic and fresh. Here's the neat part: we begin by loading a specific number of arcs using initArcDataSet. Then, we take it up a notch by introducing new random arc locations, sourced from the latitude and longitude data in our JSON file. These new arcs join the existing ones, creating a constantly evolving visual experience. We rely on the numLocations variable to generate random indices within the length of our JSON data, ensuring no duplicate indices exist too.
For this we need to initialize new variables:
let a_data; //Data to alternate the arcs
let numLocations; //JSON data length
const numArcs = 7; //Number of arcs to draw
let arcsData = []; //Array to hold the arcs data
a. initArcDataSet
function initArcDataSet(jsonData = p_data) {
//Arc config
numLocations = jsonData.length;
for (let i = 0; i < numArcs; i++) {
//Generate random indices within the bounds of the data
let randomStartIndex = Math.floor(Math.random() * numLocations);
let randomEndIndex = Math.floor(Math.random() * numLocations);
//Ensure the randomEndIndex is different from randomStartIndex
if (randomStartIndex === randomEndIndex) {
randomEndIndex = Math.floor(Math.random() * numLocations);
}
//Ensure that the generated indices are within bounds
if (randomStartIndex >= 0 && randomStartIndex < numLocations &&
randomEndIndex >= 0 && randomEndIndex < numLocations) {
arcsData.push({
startLat: jsonData[randomStartIndex].ugLocation.ugLatitude,
startLng: jsonData[randomStartIndex].ugLocation.ugLongitude,
endLat: jsonData[randomEndIndex].ugLocation.ugLatitude,
endLng: jsonData[randomEndIndex].ugLocation.ugLongitude,
color: ['gold', 'blue']
});
} else {
console.warn('Generated indices out of bounds:', randomStartIndex, randomEndIndex);
}
}
console.log("End first time: jsondata", arcsData)
return arcsData;
}
b. updateArcDataSet
let arc_change = 2;
function updateArcDataSet(jsonData = a_data) {
//Arcs to modify
for (let i = 0; i < arc_change; i++) {
let k = Math.floor(Math.random() * numArcs);
//Generate random indices within the bounds of the data
let randomStartIndex = Math.floor(Math.random() * numLocations);
let randomEndIndex = Math.floor(Math.random() * numLocations);
//Ensure the randomEndIndex is different from randomStartIndex
if (randomStartIndex === randomEndIndex) {
randomEndIndex = Math.floor(Math.random() * numLocations);
}
//Ensure that the generated indices are within bounds
if (randomStartIndex >= 0 && randomStartIndex < numLocations &&
randomEndIndex >= 0 && randomEndIndex < numLocations) {
arcsData[k] = {
startLat: p_data[randomStartIndex].ugLocation.ugLatitude,
startLng: p_data[randomStartIndex].ugLocation.ugLongitude,
endLat: p_data[randomEndIndex].ugLocation.ugLatitude,
endLng: p_data[randomEndIndex].ugLocation.ugLongitude,
color: ['gold', 'blue']
};
} else {
console.warn('Generated indices out of bounds:', randomStartIndex, randomEndIndex);
}
}
a_data = arcsData;
globe.arcsData(a_data);
}
Tip: easily adjust the number of arcs to change by modifying the 'for' loop or by creating a variable for better control e.g. arc_change.
- Bonus challenge: consider implementing a mechanism to gracefully handle exceptions when dealing with 'Not Available' (NA) locations during arc regeneration.
c. Updating the Globe to Display Arcs
To update the globe and display the arcs, add the following configurations to the 'Globe' variable:
.arcsData(a_data)
.arcColor('color')
.arcDashLength(1)
.arcDashGap(1)
.arcDashInitialGap(1)
.arcsTransitionDuration(0)
.arcDashAnimateTime(3500)
.arcLabel(d => d.info.ugLocation.ugLocation)
Additionally, set a window interval to ensure continuous arc updates within a 3500ms timeframe:
window.setInterval(updateArcDataSet, 3500);
Functionality 4 - Responsive Design:
Our project offers a responsive design that effortlessly adjusts to different devices. In our script.js, we utilize window.innerWidth to detect the user's device and dynamically adjust the globe's interaction attributes, such as OnPointClick. This means whether you're using a desktop, tablet, or mobile device, our globe provides an engaging and informative experience tailored to your screen.
Complemented with media queries (@media) in our style.css, we ensure that the globe visualization and content are presented optimally on screens of varying sizes.
For script.js, we employ device size detection to tailor the user experience. Specifically, we customize the pointLabel for tablets and mobile phones to include an 'Open URL' button. In contrast, for desktop users, we enable onPointClick to open URLs in new windows. To ensure optimal positioning of the globe on various screen sizes, we access the Three.js components of our Globe.GL globe and adjust the camera position. This is achieved through
const cam = globe.camera();
if (isMobile) {
cam.position.z = 600; //Positioned for mobile devices
} else if (isTablet) {
cam.position.z = 500; //Positioned for tablet devices
}
Here's the corresponding compatibility code:
//Device detection
const isMobile = window.innerWidth <= 767;
const isTablet = window.innerWidth <= 1024;
const cam = globe.camera();
if (isMobile) {
cam.position.z = 600;
globe.pointLabel(d => `<div id="info-box"><h3>${d.info.ugName}</h3>
<p><strong>Location:</strong> ${d.info.ugLocation.ugLocation}</p>
<p><strong>Members:</strong> ${d.info.ugMembersNumber}</p>
<p><strong>URL:</strong> <a href="${d.info.ugURL}" target="_blank">${d.info.ugURL}</a></p> <button id="url-button" onclick="window.location.href='${d.info.ugURL}"';"> Open URL</button></div>`)
}
else if (isTablet) {
cam.position.z = 500;
globe.pointLabel(d => `<div id="info-box"><h3>${d.info.ugName}</h3>
<p><strong>Location:</strong> ${d.info.ugLocation.ugLocation}</p>
<p><strong>Members:</strong> ${d.info.ugMembersNumber}</p>
<p><strong>URL:</strong> <a href="${d.info.ugURL}" target="_blank">${d.info.ugURL}</a></p>button id="url-button" onclick="window.location.href='${d.info.ugURL}"';"> Open URL</button>
</div>`)
} else { //Desktop
globe.onPointClick(d => window.open(d.info.ugURL, '_blank'))
}
Globe on mobile (e.g. iPhone 12 Pro):
For Tablets (e.g. iPad Air):
More Elements (Text + Logo):
To elevate the visual appeal of our project, we're introducing two key elements: a concise text, 'Find an AWS Group,' and Digico's logo. We've decided to position these elements – for desktop and tablet devices, they appear on the bottom right, and for mobile devices, they are centered.
Adding Elements to index.html:
<div id="text-overlay">
<p id="custom-inner-text"></p>
</div>
<div id="image-container"><a href="https://digico.solutions/" target="_blank">
<img id="logo" src="./images/digico-logo.png" alt="digicon">
</a></div>
Notice the 'id' attributes assigned to these elements, which we'll reference in script.js and style.css.
Creating Responsive Designs in style.css:
Desktop CSS (Screens ≥ 1024px):
/* CSS for Desktop */
@media (min-width: 1024px) {
#info-box {
padding-top: 5px;
padding-bottom: 10px;
padding-left: 20px;
padding-right: 20px;
background-color: rgba(0, 0, 0, 0.4);
color: #ffffff;
}
#text-overlay {
position: absolute;
top: 8%;
left: 3%;
}
#custom-inner-text {
width: 100%;
/* padding: 10px; */
color: white;
font-size: 40px;
font-family: 'Cairo', sans-serif;
font-weight: 300;
}
#logo {
width: 20%;
padding-right: 35px;
margin: auto;
display: none;
position: absolute;
bottom: 0;
right: 0;
}
p {
font-size: 15px;
line-height: 1.5;
font-family: 'Cairo', sans-serif;
margin: 0;
}
}
Tablet CSS (Screens ≤ 1024px):
/* CSS for screens with a maximum width of 1024px (tablets) */
@media (max-width: 1024px) {
#text-overlay {
position: absolute;
top: 2%;
left: 3%;
}
#custom-inner-text {
width: 100%;
margin: 0;
color: white;
font-size: 40px;
font-family: 'Cairo', sans-serif;
font-weight: 300;
}
#logo {
width: 35%;
margin: auto;
display: none;
position: absolute;
bottom: 0;
right: 0;
}
}
Mobile CSS (Screens ≤ 767px):
/* CSS for screens with a maximum width of 767px (mobile screens) */
@media (max-width: 767px) {
#info-box {
padding: 10px;
background-color: rgba(0, 0, 0, 0.4);
color: #ffffff;
}
#text-overlay {
width: 100%;
margin: 0;
}
#custom-inner-text {
text-align: center;
margin: auto;
color: white;
margin-left: -10px;
font-size: 30px;
font-family: 'Cairo', sans-serif;
font-weight: 300;
}
#logo {
width: 60%;
margin-right: 9vh;
display: none;
}
h3 {
font-size: 18px;
margin-bottom: 10px;
}
p {
font-size: 14px;
line-height: 1.5;
font-family: 'Cairo', sans-serif;
margin: 0;
}
}
This code ensures that the text and logo are displayed optimally on various devices, enhancing the overall visual experience of our project.
Adding Text After Loading:To ensure a seamless loading experience without prematurely displaying text, we've implemented the following code to manipulate the HTML elements. This enables us to insert the desired text while maintaining proper formatting. Additionally, we make the previously positioned logo visible by changing its display property to 'block.'
//Accessing the custom inner text element
const customInnerText = document.getElementById('custom-inner-text');
//Creating text nodes for the lines
const line1 = document.createTextNode('Find an ');
const lineBreak = document.createElement('br');
const line2 = document.createTextNode('AWS User Group');
//Appending the lines and line break
customInnerText.appendChild(line1);
customInnerText.appendChild(lineBreak);
customInnerText.appendChild(line2);
//Making the logo visible
document.getElementById('logo').style.display = 'block';
Final Visualization Images:
Desktop (MacBook Pro M1)
Tablet (iPad Air)
Mobile (iPhone 12)
Step 4 - Going Global in Minutes: Leveraging AWS Services
In this step, we'll dive into the world of AWS (Amazon Web Services) and explore how we can rapidly expand the project's global reach. AWS offers a plethora of services and tools that can help your project scale effortlessly, and we'll guide you through the key steps to harness this power effectively. Get ready to take your project to new heights with AWS!
Before getting started make sure you have an AWS account to follow through on the tutorial.
Deploying on S3
In this section, we'll set up a static website hosting environment using Amazon S3, a highly scalable and cost-effective AWS service.
Sign in to Your AWS Account
Begin by signing in to your AWS (Amazon Web Services) account. If you don't have an AWS account, you can create one by visiting the AWS Console at https://console.aws.amazon.com/. Ensure that you are logged in with the necessary permissions to create and manage S3 buckets.
Create an S3 Bucket
In the AWS Console, locate the search box at the top center and search for "S3."
Click on "S3" to access the Amazon S3 service.
Now, let's create a new S3 bucket to host your static website. Click on the "Create Bucket" button.
Setting up the S3 Bucket
Choosing the right name for your S3 bucket is crucial, especially if you plan to use a custom subdomain or domain for your website. Here are some key considerations:
Bucket Name: This should match the subdomain or domain you intend to use. For example, if you want your website to be accessible at http://www.example.com, your bucket name must be www.example.com.
Bucket Naming Rules: Bucket names must be globally unique across all of AWS. So, make sure your chosen name is not already in use by another AWS user.
In our example, we'll name the bucket "usergroups.digico.solutions." Here, "usergroups" becomes a subdomain within our domain "digico.solutions."
Note: the naming is now currently awsusergroups.digico.solutions
Then, navigate down to untick the “Block all public access” option to allow public access to your website files. Acknowledge the implications by ticking the acknowledgment box.
While configuring your S3 bucket, you can adjust other settings as needed including the Object Ownership and Encryption, the default is satisfactory in our project. However, it's advisable to enable versioning for your bucket. Versioning keeps multiple versions of an object, allowing you to recover from accidental deletions or overwrites. Please note that each versioned object is treated as a separate object, which may incur additional storage costs.
Finally, click on "Create bucket" to save your bucket configuration. After successfully creating the bucket, you should see a confirmation message like "Successfully created bucket 'usergroups.digico.solutions'."
Now that your bucket is set up, you need to upload your project files into it before deploying it as a static website. Follow these steps:
Navigate to your bucket by searching for it in the search box under the "Buckets" section.
Click on the bucket name to open the bucket objects page.
Uploading Files and Folders:
Since your bucket is empty, you need to add the objects to it. Start by clicking the "Upload" button.
In the upload page, use both the "Add Files" and "Add Folders" options. Note that you can't select multiple folders for batch upload; you need to iteratively click "Upload" for each file or folder.
Upload all the folders and files from your local device into the S3 bucket. Pay attention to how the paths are formed when you upload a folder.
Click "Upload" to transfer the items to the S3 bucket. Wait for a green banner to appear with the message "Upload Succeeded," then click "Close."
Enabling Static Website Hosting:
Now that your project files are in the bucket, you need to enable static website hosting. Follow these steps:
Go to the bucket's "Properties" section.
Scroll down to the "Static website hosting" section and click "Edit."
In the "Edit" page, enable static website hosting by ticking the "Enable" option.
Ensure that the "Hosting type" is set to "Host a static website."
Specify the name of your index document, which serves as the default page for your website. Typically, this is "index.html" as we sat up previously.
Scroll to the end of the page and click "Save changes."
Once completed, you should see “Successfully edited static website hosting.”
By this step, you should have configured your S3 bucket to host your static website, uploaded your project files, and enabled static website hosting. The website should now be accessible through the S3 bucket URL. In our case the S3 endpoint is accessible and can be found if we scroll down to the end of the properties page at: http://usergroups.digico.solutions.s3-website-eu-west-1.amazonaws.com
However, if you attempt to access the URL to view the objects, you may encounter an "Access Denied" error. But don't worry; this is a common issue, and it happens because we haven't set up a Bucket Policy yet.
Bucket policies are essential as they define the access rules for our S3 bucket. They determine who can access and interact with the objects stored within the bucket. To make our S3 bucket publicly accessible, we need to create a Bucket Policy that grants public read access to our objects.
To customize the bucket's policy, follow these steps:
Navigate to the "Permissions" tab within your previously named bucket. In our case, the "usergroups.digico.solutions" bucket.
Scroll down to the "Bucket policy" section and click "Edit."
There are different ways to create policies. However, to simplify the policy creation process, we'll use the Policy generator.
On this page we should several steps in order to create a correct and sound policy:
- Step 1: Select Policy Type
Choose "S3 Bucket Policy."
- Step 2: Define Effect and Principal
For "Effect," select "Allow" (this determines whether the action is allowed or denied).
For "Principal," choose "*" (which means all users).
- Step 3: Set Service and Actions
Service should be set to "Amazon S3."
From the "Action" dropdown menu, select "GetObject" (allowing users to retrieve objects).
- Step 4: Define Resource ARN
To specify the ARN (Amazon Resource Name), go back to the "Edit bucket policy" page and copy the ARN displayed there.
Paste this ARN into the "Generate Policy" page.
Important: Append "/" to the bucket ARN, making it in the format "arn:aws:s3:::YOUR_BUCKET_NAME/" This ensures the policy applies to both the bucket itself and its contents.
- Step 5: Add and Generate Policy Statement
Click "Add Statement" to include the policy statement you've designed.
As this is the only statement needed, scroll down and click "Generate Policy."
- Step 6: Apply the Generated Policy
Copy the generated policy.
Return to the "Edit bucket policy" page.
Paste the generated policy into the designated field.
Scroll down and click "Save changes."
Let’s try again now our S3 endpoint to see if our website is displaying correctly: http://usergroups.digico.solutions.s3-website-eu-west-1.amazonaws.com.
The result:
Step 1: Securing and Deploying the Globe on your Domain **
In this section, we'll guide you through the process of deploying your project while ensuring it follows best practices for security and accessibility. We'll start by addressing the need for secure deployment and discuss the steps involved.
Issuing an SSL Certificate
Our journey begins with securing your website through the issuance of an SSL (Secure Sockets Layer) certificate via the AWS Certificate Manager (ACM). This crucial certificate ensures the encryption and security of data in transit, safeguarding the integrity of your website.
Accessing AWS Certificate Manager (ACM)
Start by accessing the AWS Certificate Manager from the AWS Console. You can easily find it by typing "Certificate Manager" in the search box.
Selecting the Right Region
For compatibility with CloudFront, it's essential to request or import the certificate in the US East (N. Virginia) Region (us-east-1). This region ensures integration with the AWS services we'll use later in the deployment process. In our case, we will proceed with requesting the certificate.
Requesting Your SSL Certificate
In the AWS Certificate Manager, click on "Request" to initiate the certificate issuance process.
Ensure that you select "Request a public certificate" under the Certificate Type and then click "Next."
Specify the fully qualified domain name (FQDN) or the subdomain/domain URL you plan to use for your website. This is a crucial step, as it helps the certificate to be correctly associated with your domain.
For validation, choose the DNS records option. This is the method we'll use to verify ownership of the domain.
Scroll down and select "RSA 2048" as the key algorithm. While there are other options like "ECSDA 256" and "ECSDA P 384," "RSA 2048" is a commonly chosen option due to its compatibility and robust encryption.
Finally, click on "Request" located at the bottom left of the page.
Viewing Certificate Request Status
If your certificate request has been successfully submitted, you'll see a banner confirming it with a unique ID (XXXX...). Click "View certificate" from the banner.
Scroll down to the "Domains" section of the certificate details. Here, you'll find the necessary values required for setting up DNS records. Make sure to copy both the "CNAME Name" and "CNAME Value" as we'll utilize them in the upcoming sections.
Next, we'll proceed to set up DNS records to complete the verification process and enable HTTPS for Globe website.
Step 2: Managing DNS Records
Accessing and managing DNS (Domain Name System) records for your domain is a crucial part of our deployment process. These records are the key to configuring your domain settings effectively. In our demonstration, we utilize Lightsail to handle DNS records. If you're new to this process, we highly recommend referring to the Lightsail DNS Entry Guide for detailed instructions.
Accessing Lightsail Domains & DNS
Begin by accessing Lightsail Domains & DNS and set up a DNS Zone if you haven't already. This zone will serve as the foundation for configuring your DNS records.
Adding a DNS Record
Navigate to the DNS records within Lightsail and look for the option to "Add record." This action will allow you to create a new DNS record necessary for our setup.
Configuring the DNS Record
In the record type, select "CNAME." This type of record is suitable for our purposes and aligns with our certificate setup.
Under "Record name," paste the CNAME name obtained from the AWS Certificate Manager. Please note that depending on your hosting service's DNS record input, your domain may already be present. If this is the case, ensure that the CNAME name matches the text from the AWS Certificate Manager.
In the "Route traffic to" field, enter the CNAME value that you previously copied from the certificate details.
Finally, click "Save" to confirm the addition of the CNAME record. It's essential to verify that the CNAME record has been successfully added under the CNAME Records section, taking into account your specific hosting service's requirements.
Typically, you can expect the SSL certificate to be issued by AWS Certificate Manager within approximately 30 minutes. To confirm, revisit the ACM page and click on 'List certificates'.
Step 3: Configuring CloudFront
Now that we have secured our SSL certificate and managed DNS records, let's move on to configuring Amazon CloudFront, AWS's content delivery network service. CloudFront will serve as the entry point for our website, ensuring faster content delivery to users. In this step, we will set up CloudFront to use the S3 bucket as its origin, associate it with the SSL certificate we obtained earlier, and implement a crucial HTTP to HTTPS redirect for added security.
- Access CloudFront: Begin by accessing CloudFront through the AWS Console. If you're starting fresh, select "Create a CloudFront distribution." If you've previously set up a distribution, choose the "Create" option.
- Select Origin: Choose an appropriate name for your origin. From the dropdown menu, select the AWS origin, which corresponds to the S3 bucket we've created earlier. Opt for "Use website endpoint" for consistency, as it simplifies routing.
- Configure Default Cache Behavior and WAF: Scroll down while retaining most of the default configuration. However, under "Default cache behavior," go to "Viewer" and then "Viewer protocol policy." Opt for "Redirect HTTP to HTTPS" to ensure all traffic is secured via HTTPS. This step is crucial for data protection. Also, under Web Application Firewall (WAF), choose Do not enable security protections as we are only having this as a simple project.
Fine-Tune Settings: Continue scrolling down until you reach the "Settings" section. Here, select "Use all edge locations (best performance)" under "Pricing class." This choice optimizes the delivery of our globe visualization project.
Configure Alternate Domain (CNAME): In this step, add the same fully qualified domain name for which we issued the SSL certificate under the "Alternate domain name (CNAME)" section. In our case, this is "usergroups.digico.solutions." This step is crucial for correct configuration to avoid potential ERROR 403 issues.
Custom SSL Certificate: In the section near the bottom, choose the previously issued SSL certificate from the dropdown menu. This certificate is essential for securing your custom domain.
- Create Distribution: Finally, scroll to the bottom and click "Create distribution" to complete the CloudFront setup.
Upon successful creation, you should see a green banner with “Successfully created new distribution”, signaling that the process is underway. Keep in mind that the status under "Last modified" may initially show "deploying." This status change is normal and occurs because CloudFront can take up to 25 minutes to fully deploy your configuration.
Step 4: Updating DNS Records for CloudFront
Finally, we'll configure a CNAME record for the subdomain "usergroups.digico.solutions" to direct it to the CloudFront distribution. This step marks the culmination of the process, enabling secure access to the globe visualization project via HTTPS, using our designated subdomain. To accomplish this:
Copy the Distribution Domain Name from your CloudFront setup.
Navigate back to the DNS Zone of your hosting service, just like in the previous steps where we managed DNS records.
Click on "Add record" to create a new DNS record.
Choose "CNAME" as the Record type.
In the "Record name" field, type the subdomain, which, in our case, is "usergroups."
In the "Record value" field, paste the copied Distribution Domain Name from CloudFront. Make sure to remove the "https://" part, as we only need the domain name.
Click "Save" to confirm the addition of the CNAME record.
Finally, make sure that the CNAME record has been added.
Optional: Use Only CloudFront to Display the Website
Despite our deployment being complete, we still would like to restrict access to our S3 bucket and keep our CloudFront distribution as the only accessible point for our Globe.
To do this, will have a simple alternative route in setting up both our S3 Bucket and our CloudFront distribution.
With this step performed, our deployment is now complete with the URL usergroups.digico.solutions ready using several AWS services:
- Amazon S3
- AWS Certificate Manager
- Amazon Lightsail
- Amazon CloudFront
To try it out the AWS User Group Globe, please visit awsusergroups.digico.solutions
I hope you enjoyed the reading, see you in the next one!
Top comments (0)