Tag Archives: ultrarunning

programming

UltraSignup Visualizer


Instructions

  • In the text box at the top of the graph, enter the full name of a runner whose results can be found on UltraSignup, then hit enter.
  • The points on the graph represent individual race results for the given runner. Move your mouse over a point to see details of that race.
  • The line represents the evolution of the runner’s UltraSignup rank.
  • Timed events (eg, 12-hour races, 24-hour races) appear as empty circles. It seems that as of mid-October, 2014, timed events are included in the ranking. However, it is not clear to me if that change is retroactive, and in some circumstances, I cannot get my calculation of the ranking to line up with their calculation of the ranking. So if you have a large number of timed events in your history, the line I’ve calculated might be e’er so slightly off. The ranking reported below the graph is the official number, provided by UltraSignup.

Background

[Update: The friendly folks at UltraSignup came across this, and they liked it. I worked with them to get it integrated into the official runner results page. So now you can click the “History” link just below a runner’s overall score on UltraSignup and see the plot on the results page. Though if you like the spanky transitions between runners, you still need to come here.]

In the world of ultrarunning, it seems that the ranking calculated by UltraSignup has become the de facto standard for ranking runners. I think that part of the reason for its acceptance is its simplicity. A runner’s rank in a single race is just the ratio of the winner’s finish time to the runner’s finish time. So if you win a race, you get a 100%; if you take twice as long as the winner, you get a 50%. The overall ranking is a single number that represents an average of all of a given runner’s race rankings. If you were to look up my results on UltraSignup, you would see that as of this moment of this blog post, my 10+ years of racing ultras has been boiled down to a ranking of 88.43% over 48 races.

Of course, with simplicity comes inflexibility. What that number doesn’t capture is change over time. By summing up my results as a single number, it’s hard to see how my last few years of Lyme-impaired running have affected my rank, or how my (hoped-for) return to form will affect it. I was curious to see how runners progress over time, and how it affects the UltraSignup rank. In looking at the details of how UltraSignup delivers their rank pages, I noticed that the results come as JSON strings. Therefore, I realized, I wouldn’t even have to do any parsing of irregular data. I could just pull the JSON, and use my handy D3 skillz to put the results in a scatter plot.

I won’t go into great depth about implementation details. If you happen to be interested, you can go to the source. A passing familiarity with D3 would be helpful, but familiarity with only vanilla Javascript should allow you to get the gist.

Oh, and be aware that since this pulls data from UltraSignup, it’s entirely possible that it will stop working someday, either because they change the way they deliver data, or because they don’t like third parties creating mashups with their data. Also, this doesn’t work on Internet Explorer 8, or earlier. Sorry ’bout that!

running

Hellgate Overview

[The following is an overview of the Hellgate 100k course. I originally wrote it in 2006, and I’ve amended it several times through the years. I’ve finished the race 11 times, so I don’t have much more to say about it, but I’ve decided to move the overview to this blog for the sake of content consolidation. D’I miss anything, or get it wrong? Feel free to append, extend, expand, propound, or offer your own observations in the comments.]

Hellgate 100k

Alrighty, folks. I was recently looking at a map of the Hellgate course to refresh my memory about how it goes. Then I realized that that was a terrible idea. I mean, after doing this race five times, the one thing you definitely don’t want to do is remember anything about it. But by the time I remembered that, it was too late. Yet the same desire that would make me say, “EWWW, taste this!” after drinking sour milk makes me want to share the memories. So here’s a handy little overview of Hellgate. (I should also note that Keith Knipling put together a far more high-tech overview of the 2007 race. Me, I use a highlighter and a map that I spread on my floor. Keith, he’s got heartrate data, GPS details and elevation profiles. How can I compete with that? I CAN’T, I TELL YOU! *sigh* So I just have to rely on my razor-sharp wit and boyish good looks to keep you interested in what I have to say.)

I’ll give you the full map immediately below. After that, I’ve broken it down, aid station to aid station. I’ll give you Horton’s description of each section, followed by the effluvia of my ruminations. In the map below, the race starts in the upper right, and follows the yellow highlighter generally toward the lower left. The start, finish and aid stations are marked with little red stars. The map I used for this little presentation is,

National Geographic Topographic Map #789
Lexington, Blue Ridge Mts
George Washington and Jefferson National Forests
Virginia, USA
Featuring: Glenwood / Pedlar Ranger District
ISBN: 1-56695-118-6
http://shop.nationalgeographic.com/product/615/803/246.html

I originally put together this overview before the 2006 race. During subsequent years, I realized that there were some sections that I needed to update because I had remembered some details incorrectly. But most of all, I realized that this sort of overview could be only marginally useful. Hellgate, more than any other race I’ve done, has a character that changes drastically from year to year. I’m not just saying that some years it’s chillier than other years. I’m saying that from year to year, this is a completely different race. One year, a certain section of the course might be particularly difficult, and the next year, that same section might be… less notable.

So far, we’ve had,

  • 2003 – The first year of the race, no one knew what to expect. The weather was cold, and there was a light fall of snow on the ground. The moon was full, and the sky was clear. With no leave on the trees, no clouds in the sky, and white snow on the ground, the moon lit up the trails like daylight. I turned on my flashlight for the more technical downhills, but I ran most of the way by the light of the moon. And the end of that first year, everyone knew we had been part of something special. And we were all amazed at just how difficult the race was.
  • 2004 – The “warm year” was different, in that there was no moon. I was quite comfortable in shorts. When I finished, I wondered how I could have forgotten just how difficult the race was.
  • 2005 – The “ice year” was just ridiculous. Several inches of snow fell early in the week. On friday, the temperature rose to the 60s, then fell at night to the 20s. Every road section was covered with glare ice, and every trail section had fluffy snow under a half-inch thick crust of ice. Staying upright was the name of the game. Just walking across the parking lot at Camp Bethel, from your car to race registration, was a harrowing experience. When I finished, I wondered how I could have forgotten just how difficult the race was.
  • 2006 – The “cold year” (or “the year of the leaves”) was when we learned that eyeballs can, in fact, freeze. With temperatures around 12°F at Headforemost mountain, and strong head winds, things got ugly. Four people ended up with severely impaired vision when their corneas froze later in the race. (After thawing out, everyone’s vision returned to normal.) Further, due to a lack of recent rain, leaves piled up as high as a foot and a half deep on many parts of the course. With uneven trail and loose rocks underneath, the leaves made footing extremely difficult. When I finished, I wondered how I could have forgotten just how difficult the race was.
  • 2007 – The “nice” year was probably as good as it gets. Most years, the 10 or 15 minutes before the race start, as we stand around in our Lycra® and our Polartec®, can be painfully cold. This year was rather nice. I was in shorts, and not particularly uncomfortable (which meant the temperature was in the upper 30s). There had been very little rain leading up to the race, so even the early creek crossing was a non-issue. There was a little bit of ice on some of the roads at higher elevations early in the race, and there were some deep leaves covering trails later in the course, but neither was as bad as previous years. We finally had a year when we could judge whether the race was difficult because of the weather of previous years, or because the course was just that hard. I’ll let you guess what the conclusion was. But I’ll give you a hint: about two seconds after I crossed the finish line, I was flat on the ground. Oh yeah, and when I finished, I wondered how I could have fotgotten just how difficult the race was. (Though I should mention that this year was a very special race for me. The full story is here.)

Are you picking up on the theme here?

Hellgate 100K Course

read more »

programming

Race Progress Visualization Using D3

[The project referred to in this post can be found at http://vestigial.org/MMT/ ]

I’ve been looking for some better tools to produce interactive, data driven, visually appealing web content. In the past couple of years, I’ve become enamored with R for analysis and visualization, but the graphic results are static. (Sure, there are tricks to create animations, but I’m not looking for workarounds.) I occasionally use Google Charts when I need to put together a quick visualization, but they don’t provide quite the level of flexibility I’d like. I started looking at either working directly with SVG or Canvas DOM elements, or using a Javascript SVG library that would allow me to avoid the low-level details.

The most interesting possibility was the D3 framework. D3 — for Data-Driven Documents — is an entire framework for DOM manipulation in data-driven sites. Browsing through the examples on the D3 site, I recognized several memorable visualizations that have appeared on one of my favorite blogs through the years, Flowing Data. It is possible to use D3 for SVG construction and manipulation while non-data-driven portions of the site are handled by, eg, jQuery or standard Javascript. But as long as you’re already using the bandwidth to load the framework, you might as well drop other frameworks, and use the tools that D3 provides.

I was keen to get some experience with D3. When learning a new technology, I prefer to dive straight in — come up with a short, but non-trivial project that I can build. In this case, I came up with a project that melds technology, data visualization, and ultrarunning. The Massanutten Mountain 100 Mile Trail Run (or MMT) is in a few weeks. In such a long race, runners and crews like to have some idea when they’ll arrive at intermediate points along the course if they’re aiming for some given finish time. Conversely, knowing when they’ve arrived at points along the course can help to predict what sort of finish time to expect. While I’m not the first person to provide a visualization, or some tool to correlate aid station splits with finish times, it’s fun to put together something that’s visually appealing and useful.

Showing data from 2011 and 2013 for finishers who finished between 20:59 and 25:55, race time. The horizontal axis is time and the vertical axis is distance, labeled on the left with mileage at each aid station, and on the right with the aid station name. Each diagonal line represents a single racer. Intermediate times on the graph show first and last racer times of arrival at each aid station (for racers in the result set).

Showing data from 2011 and 2013 for finishers who finished between 20:59 and 25:55, race time. The horizontal axis is time and the vertical axis is distance, labeled on the left with mileage at each aid station, and on the right with the aid station name. Each diagonal line, or “track”, represents a single racer. Intermediate times on the graph show first and last racer times of arrival at each aid station (for racers in the result set). Tufte would be proud.

 

There are several interactive components that I think are noteworthy. First, I provide on-demand data loading. When the page loads, none of the race results is loaded. When a year is selected, the page checks whether the data have been downloaded. If not, it fires an AJAX request, and saves the data so the results can be turned on and off.

The page also provides sliders to limit the result set based on finish time. Each limiter consists of three components: a triangular slider widget (represented by an SVG path element), a time display (represented by an SVG text element), and a vertical guide line (represented by an SVG line element). When the widget is slid, all three elements should move in unison, and the time display should update with the time value at the current point. As a bonus, the vertical guide gets brighter. So I needed to be able to address each element individually, but move them in unison. To build that, first I needed to define the shape for my widget (note that in SVG coordinates, the top left is [0,0]):

var limpolygon = [{x: 0, y: 0}, {x: 10, y: 0}, {x: 5, y: 10}, {x: 0, y: 0}];

I also need to define a function to tell D3 how to interpret the data above. I can use d3.svg.line() to return a function for this purpose. Since I’ve built the object with straight-forward X and Y coordinates, I just need to build a simple function based on those values:

var limline = d3.svg.line()
  .interpolate("linear")
  .x(function(d) { return d.x; })
  .y(function(d) { return d.y; });

Finally, I put the group together. I define a group element (“g”), and append the widget, which I construct in place. I then use the D3 selector to reselect the group, and add the line, then the text:

svg.append("g")  // Create the group, append it to the svg object
  .attr("id", "lim1")
  .attr("transform", "translate("+lim1x+","+limy+")")  // Put it into position
  .append("path")  // Create "path" element for widget, and append it to group
    .attr("id", "lim1_point")
    .attr("d", limline(limpolygon))  // A path has a "d" attribute which gives
                                     // instructions for drawing. Our limline()
                                     // translates raw data into path data
    .attr("fill", "white")
    .on("mousedown", function() {
      capt = "lim1";
      d3.select("#lim1_line").style("stroke-opacity", "1");
    });

d3.select("#lim1").append("svg:line")   // Create line element, append to group
  .attr("x1", limhalfw)
  .attr("y1", ex_pad.top)
  .attr("x2", limhalfw)
  .attr("y2", height - ex_pad.bottom)
  .attr("id", "lim1_line");

d3.select("#lim1").append("svg:text")   // Create text element, append to group
  .attr("id", "lim1_time")
  .text("00:00")
  .style("text-anchor", "end")
  .attr("transform", "translate(-2)");  // Push it 2px to left, for a nice gap

In my view, the coolest trick is making the data respond to the sliders. Whereas showing or hiding the individual years relies on a small number (3) of discrete values, I need to show or hide individual race results based on what is essentially a continuous scale. This involves several steps. First, when adding each track to the graph, I need to attach the finish time to it. Fortunately, HTML5 provides the ability to specify arbitrary data attributes with the data-* construct.

lineset.enter()
  .append("path")
  .attr("data-finish", function(d) {  // Add the data-finish attribute
    return d.finish;
   })
  .style("stroke-opacity", function(d) {
    if (d.finish > finScale(lim2x) || d.finish < finScale(lim1x)) return "0";
    else return ".3";
   })
  .datum(function(d) { return d.splits; })
  .attr("class", "rtrack line " + iden)  // Classes to use later in selectors
  .attr("d", line);

Above is the code to add the tracks. While it might not make much sense if you are not familiar with D3, the key point is the third line. The object has a data object, d, applied to it, and on that line, we set the data-finish attribute to the value of d.finish. (Directly below that, we set the opacity of the line to 0 (making it invisible) if it falls outside of our specified range, or .3 if it is inside the range. But we’re getting ahead of ourselves.)

The next thing we need to a way to translate the location of a slider into a finish time. D3 provides “scales” for just such a purpose. Usually, D3 scales are used to translate some real world value to a pixel position. In this case, we want to do the reverse. I want to build a function that will translate an input domain of a pixel position into the output range of a race time, which in this case is between 0 and 36 hours.

var finScale = d3.scale.linear()
  .domain([lim1x, lim2x])
  .range([0, 36]);

(An astute reader who is familiar with D3 might note that somewhere else, I must have defined a scale to translate from times to pixel values. In that case, someone might wonder why I don’t just use linear.invert() to translate a range value into its corresponding domain value. The answer is that the scale that translates from time to position uses a domain defined by the time of day as a date object, whereas in this case, I want to translate between position and a floating point number representing the finish time in hours (with minutes represented in the fractional portion of the number). Hence the need to define a new scale.)

In this case, lim1x is the initial pixel position of the lower limit slider, and lim2x is the pixel position of the upper limit slider. That produces a function that can be called as finScale(px_pos) to return a corresponding race time. I can then use that in the function that is called when a slider is released.

function updateRange() {
  var fin1 = finScale(lim1x);  // Translate pixel positions to finish times
  var fin2 = finScale(lim2x);
  d3.selectAll(".rtrack").transition(500).style("stroke-opacity", function(d) {
    if (this.getAttribute("data-finish") > fin2 ||
        this.getAttribute("data-finish") < fin1) return "0";
    else return ".3";
  });

  updateAidStationTimes();
}

That function translates the current pixel positions of the sliders into race times (fin1 and fin2). Then it uses d3.selectAll to get every item with the class “rtrack” (which is every race line displayed on the graph), applies a 500ms transition time to the following step, then sets the stroke-opacity style based on a function that checks whether the custom attribute data-finish is in the range defined by the limiters. Finally, it calls updateAidStationTimes(), which I won’t explain in detail here, but it uses d3.extent() with a custom accessor function to find the first and last arrival time of racers in the result set at each aid station. (If you’re particularly interested, you can always dig it out of the source.) It then updates the times displayed on the graph, and moves them into the proper positions.

I started the project on Saturday morning with no experience in D3 (or with SVG graphics), and I finished Sunday evening. I even had time to get out for a bike ride, a run, and a trip to the library to get a movie (which I also watched over the weekend). In the course of this project, I came to appreciate just how massive D3 is. I’m starting to get a feel for it, but this project just scratched the tip of the D3 iceberg (though I’m not sure one would really scratch an iceberg, the tip or otherwise).

[The project referred to in this post can be found at http://vestigial.org/MMT/ ]