“Katherine Dunham’s Global Travel, 1947-60 (Interactive Space-Time Aquarium)” is a web-based solution to visualize the manually-curated Everyday Itinerary Dataset (available from NADAC). Spatializing this data while maintaining chronology allows us to see the architecture of choreographer Katherine Dunham’s global travels, including the locations to which she regularly returned. Time is represented as elevation, with 1947 closest to the earth rising to 1960 farthest away.
This interactive tool is mainly composed of four types of elements: a globe, points, paths, and labels. A key initial decision was to choose how these components will be rendering the data. Points are cylindrical objects growing up perpendicularly from the surface of the globe; each one represents an individual vertical timeline for a visited place. Paths are displayed as floating lines connecting points at specific heights; they depict trips between two places. Labels provide contextual information, mainly city names and travel dates. Regarding interaction, the user can rotate the globe, zoom, hover to display information tooltips, and click to highlight elements, among others. Thanks to these sets of elements and interactive actions, this tool empowers geographical thinking as it helps to explore the globe in detail by regions and easily make connections between them.
The process of creating the 3D globe has two key steps: data wrangling and web-based visualization. First, the information is extracted from the Everyday Itinerary Dataset and then transformed to feed the globe. Second, a web interface is designed to visualize the data in the form of an interactive globe. The data required for the components are:
- For points: geolocation (latitude, longitude, and altitude) of visited places. The point’s altitude is proportional to the last visit date, measured in number of days elapsed since January 1st, 1947.
- For paths: pairs of places represented by their geolocation. The altitudes here are relative to when the places were specifically visited, again measured in number of days elapsed since January 1st, 1947.
- For labels: label geolocation and text to display.
The Everyday Itinerary is the source dataset that contains all the necessary information for points, paths, and labels.1 It is stored on Google Sheets, however, a local copy is downloaded for its analysis. I chose Python and, more specifically, Pandas for my data processing because it makes managing tabular data very simple. I wrote my scripts in Jupyter Notebooks for its ease of creating and sharing my codes and results. The notebooks with the source code and the data can be found on GitHub.
I started the data preparation by manually collecting the geolocation of every visited place through respective web searches and adding it to the itinerary dataset (see notebook 02, cell [9]). Then I compressed every consecutive row associated with the same place into a single row where I recorded the corresponding start and end dates and the stay length (roughly, the number of consecutive rows2) of that particular visit to that place (02 [11]). The following sample tables exemplify this preliminary data transformation:
| ⇒ |
|
The stay-length data now has a more convenient format to extract the information for the globe. There will be a point for every unique city (03 [14]). I found it most effective to give the points the minimum necessary height relative to the last visit, so that the altitude of each point is proportional to the duration, in number of days, between January 1st, 1947 and the final day of the last visit to the city represented by the point (03 [17]).
Every point has several labels associated with it. These labels are the city name and one or more labels indicating the years that place was visited. The point’s height (let us remember a point is rendered as a cylinder) along with the year labels represent a timeline for that place. As mentioned above, every point is geolocated by means of a triple (latitude, longitude, and altitude) and all its associated labels share the same latitude and longitude, but have different altitudes. The city name has an altitude of 0 (just on the ground, see (03 [20]) while years have an altitude proportional to the number of days elapsed since January 1st, 1947 (03 [23]), being this date on the ground (just like the city name) and December 31st, 1960 on the very top of the cylinder. Figure 1 shows the case of Havana. It has the name on the ground, the label “1947” very close to the ground (indicating a stay from March 28th to 30th, 1947), “1951” (for a stay on August 5-6, 1951), “1953” (October 7-10, 1953), “1954” (August 20-21, 1954), and the label “1956” (for the last visit to this place on January 1-3, 1956).
The paths are initially conceptualized as a succession of destinations on different dates. In this approach, a stay is considered a trip from a place to itself (to that same place) that took “stay-length” days (03 [28]). The following simplified table shows the path construction:
| # | Latitude | Longitude | Altitude | City | Date |
|---|---|---|---|---|---|
| 0 | 40.7127 | -74.0059 | 0 | New York City | Jan 1, 1947 |
| 1 | 40.7127 | -74.0059 | 20 | New York City | Jan 21, 1947 |
| 2 | 39.9528 | -75.1636 | 21 | Philadelphia | Jan 22, 1947 |
| 3 | 39.9528 | -75.1636 | 28 | Philadelphia | Jan 29, 1947 |
| 4 | 40.7127 | -74.0059 | 39 | New York City | Jan 30, 1947 |
The idea is to chain all the rows to form a continuous path with the following structure:
City #1 — stay length — City #1 — travel length — City #2 — stay length — City #2 — travel length — City #3 …
Then, cumulative stay and travel lengths are calculated to assign an altitude to each place every time it is visited. The following paths exemplify the information in Table 2:
New York City — 20 — New York City — 1 — Philadelphia — 7 — Philadelphia — 1 — New York City …
New York City, 0 ➤ New York City, 20 ➤ Philadelphia, 21 ➤ Philadelphia, 28 ➤ New York City, 29 …
Finally, I saved point, path, and label data to their respective CSV files for further processing on the web side.
In this process of web design, I used HTML, JavaScript, CSS, and the library jQuery, as well as the main specific library for this interactive visualization, GlobeGL, a web component that uses spherical projection to represent data visualization layers on a 3D globe. This library is largely built on ThreeJS, which is in turn based on WebGL, so make sure your browser supports it. Other dependencies are D3.js and Range Slider. D3.js is a JavaScript library for producing dynamic, interactive data visualizations in web browsers, but here it is only needed for loading the data. Range Slider is a simple JavaScript polyfill used here for controlling the timeline.
The first step is to “import” these libraries in the head of your HTML. I decided to download the latest versions of GlobeGL and ThreeJS and reference my local copies instead of linking them from CDNs. My head section looks like this:
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="dist/three.js"></script>
<script src="dist/globe.gl.js"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>Then create an empty div object in the body of your HTML and provide a value for its attribute id. This div will be the container of the globe. For example,
<div id="globeDiv"></div>Finally, create the Globe object in your JavaScript code, passing the above div to the initialization function, and assign it to a variable or constant. The following code creates and assigns the object to a constant:
const globe = Globe()(document.getElementById('globeDiv'))Configure the globe by setting up its image and its canvas’ width and height. The globe’s image must be an earth map image, whose map projection has to be equirectangular. I used a 5400x2700px PNG file for this purpose. The canvas’ width and height should be large enough to allow scrolling to the edges of the globe when zooming in. Use the designated globe’s methods and pass appropriate parameters to them. For example,
globe.globeImageUrl('earth-maps/grey.png')
globe.width(window.innerWidth * 2)
globe.height(window.innerHeight * 2)Now load the data (points, paths, and labels) onto the globe. The data was previously saved in CSV format, as explained above. There are many different ways to open a file in JavaScript, but I chose D3’s queue. This method can be leveraged to read data as deferred asynchronous tasks. When all the tasks complete, the queue passes the results to the await callback. For example,
d3.queue()
.defer(d3.csv, 'data/points.csv')
.defer(d3.csv, 'data/paths.csv')
.defer(d3.csv, 'data/labels.csv')
.await(ready)
function ready(error, pointData, pathData, labelData) {
…
}On data ready, it needs to be parsed in order to be provided to the globe object in the appropriate format. That is what the function ready in the example above does. Points, paths, and labels must be formatted as arrays of objects. Point objects contain a numerical value for the geo-coordinate attributes lat (latitude), lng (longitude), and altitude. Every path is in turn an array of two arrays, each of them containing three numerical values for the latitude, longitude, and altitude of the path’s origin and destination, respectively. Labels contain the geo-coordinate attributes plus the text to display. A fictitious code for this could be:
var pointArray = [
{ lat: lat1, lng: lng1, altitude: alt1 }, // pt1
{ lat: lat2, lng: lng2, altitude: alt2 }, // pt2
{ lat: lat3, lng: lng3, altitude: alt3 }, // pt3
…
]
var pathArray = [
[ [ lat1, lng1, alt1 ], [ lat2, lng2, alt2 ] ], // path1 = [pt1, pt2]
[ [ lat2, lng2, alt2 ], [ lat3, lng3, alt3 ] ], // path2 = [pt2, pt3]
[ [ lat3, lng3, alt3 ], [ lat4, lng4, alt4 ] ], // path3 = [pt3, pt4]
…
]
var labelArray = [
{ lat: lat1, lng: lng1, altitude: alt1, text: text1 }, // label1
{ lat: lat2, lng: lng2, altitude: alt2, text: text2 }, // label2
{ lat: lat3, lng: lng3, altitude: alt3, text: text3 }, // label3
…
]Once the data objects are created, load them onto the globe. Use the corresponding globe’s methods and pass the arrays of objects to them.
globe.pointsData(pointArray)
globe.pathsData(pathArray)
globe.labelsData(labelArray)Setting the points’, paths’, and labels’ altitudes require an additional step. In the code below, points’ and labels’ altitudes are taken from the value of the attribute altitude. For paths though, the altitude is taken from the last value of the origin and destination arrays.
globe.pointAltitude('altitude')
globe.pathPointAlt(d => d[2])
globe.labelAltitude('altitude')We implemented a color gradient for paths in order to make altitude and the passage of time more visible. Path coloring is, though, optional and user specific, so I purposely left the details out of this document.
Altogether, your web page code should look like the sketch in Appendix A. These are the basics for creating a 3D globe from itinerary data. Read GlobeGL documentation for more examples and detailed information.
Our code is shared as part of an exploratory, research-driven digital humanities project. While certain components may be technically reusable, this repository is intended as a starting point for thinking and experimenting with methods. It is not intended as a general-purpose visualization library or “drop-in” toolkit for replication.
This code is shared under an MIT License, which permits reuse and adaptation for any purpose, including commercial use, provided that attribution is included. We ask that users engage respectfully with the intellectual framing behind the project, especially in academic contexts. See the full license statement here.
Based on code and data developed by Antonio Jiménez-Mavillard for the AHRC-funded project Dunham’s Data: Katherine Dunham and Digital Methods for Dance Historical Inquiry (AHRC AH/R012989/1, 2018-2022), PIs Harmony Bench and Kate Elswit.
Harmony Bench and Kate Elswit (PIs). Dunham’s Data: Katherine Dunham and Digital Methods for Dance History (AHRC AH/R012989/1, 2018-2022). https://dunhamsdata.org
<!DOCTYPE html>
<head>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="dist/three.js"></script>
<script src="dist/globe.gl.js"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
…
</head>
<body>
<div id="globeDiv"></div>
…
<script>
const globe = Globe()(document.getElementById('globeDiv'))
globe.globeImageUrl('earth-maps/grey.png')
globe.width(window.innerWidth * 2)
globe.height(window.innerHeight * 2)
d3.queue()
.defer(d3.csv, 'data/points.csv')
.defer(d3.csv, 'data/paths.csv')
.defer(d3.csv, 'data/labels.csv')
.await(ready)
function ready(error, pointData, pathData, labelData) {
var pointArray = … // parse pointData
var pathArray = … // parse pathData
var labelArray = … // parse labelData
globe.pointsData(pointArray)
globe.pathsData(pathArray)
globe.labelsData(labelArray)
globe.pointAltitude('altitude')
globe.pathPointAlt(d => d[2])
globe.labelAltitude('altitude')
}
…
</script>
</body>Data preparation:
Input data:
- data/src/1947-60/Dunham 1947-60.xlsx
Notebooks and execution order:
- 01-everyday.ipynb
- 02-itinerary.ipynb
- 03-globe.ipynb
Web interface:
Source code: https://github.com/DunhamsData/Globe/tree/master/web
- Three.js (local copy)
- Globe.gl (modified local copy - licensed under the MIT License, https://github.com/vasturiano/globe.gl/blob/master/LICENSE)
- D3.js (only for loading data)
- jQuery
- Range Slider
Footnotes
-
For detailed information on this dataset, refer to the “Dunham's Data Everyday Itinerary Dataset User Guide”. ↩
-
We formally defined the stay length as the number of consecutive nights spent in a place. It usually agrees with the number of successive rows in the dataset, but there are cases in which it does not. Read my blog post “First Things First: Days and Nights” for more information. ↩
