Jacob Wood
Build a functional end-to-end data recording, analytics, and visualization system for a real world activity.

Disc golf is fun. Visualizing repeated activities with real data is fun. Pulling out your phone while playing disc golf to record every shot is not fun.

This project attempts to solve that problem. Here are the results.

Table of Contents

  1. Perfect Solution
  2. Best Currently Available
  3. This Project
    1. Disc and Location Recording
      1. NFC Tags
    2. Shortcuts Integration
    3. Location Recording
    4. Automation Trigger
    5. Steps to Use
    6. Data Post-Processing
      1. Input Data
    7. Course Database
    8. Disc Database
    9. Played Round Inference
    10. make-round Command
    11. Statistics Generation
      1. make-stats Command
    12. Dashboard View
  4. Ingredients
    1. Hardware
    2. Software
    3. Data

Perfect Solution

My perfect answer to this problem has these qualities:

We probably can't get to the perfect solution, but we can get close!

Best Currently Available


UDisc is a fantastic application that is widely used for disc golf score keeping and tracking. It does almost everything described in the perfect solution, but comes up short in a few key places:

This Project

We should be able to create a near perfect solution if we can do three things:

  1. Record the location and disc used for each throw with minimal interruption

  2. Post-process the recorded data to provide an accurate depiction of the played round

  3. Produce desired per-round and lifetime data visualizations

Fortunately, we can do these three things pretty well!

Disc and Location Recording

The perfect interface here would be a quick tap on a disc that records the disc and the current location.

NFC Tags

This is a perfect use case for passive NFC stickers, which can weigh less than 0.2 grams and can be applied to a disc without affecting the flight.

The NFC stickers are applied to the disc and can be covered with a vinyl sticker to protect them when they are inevitably thrown into the water.

We need a way to read these NFC stickers, note the ID of the specific sticker, and record the ID and current location to a file. Fortunately, the iPhone has an NFC reader, GPS, and the Shortcuts app!

Shortcuts Integration

We will start each round by running a shortcut, Start DG Round, that:

  1. Gets the current date

  2. Makes a filename out of the current date

  3. Saves the filename into a global variable (available from the free Toolbox Pro app) that other shortcuts will be able to read

We will make use of a Timestamp shortcut to:

  1. Generate a timestamp

  2. Collect an input string (the disc's ID)

  3. Append both to a newline in the .csv file specified by Start DG Round

Location Recording

Ideally, the Timestamp shortcut would directly read and record the GPS location. This approach, however, forces the GPS fix to be lost and re-acquired at each read, which can take a few seconds. To remedy this we will instead keep a record of our position throughout the round using Open GPX Tracker. We can then correlate our timestamps with our position after the round. This is a far more robust solution.

Automation Trigger

The Timestamp shortcut is wrapped in an Automation that is triggered when a known NFC tag is detected. The automation will:

  1. Play a tink sound to let you know the disc has been read

  2. Send the name of the triggering disc to Timestamp to be recorded

Unfortunately, the iPhone will only detect NFC when the screen is on. This means we need to do something to keep the screen alive during the entire round. The workaround solution for now is to run an application that keeps the screen going. I use a free minimal clock app that keeps the screen almost entirely black and then set the brightness to a minimum - this results in ~30% battery drain after 4 hours of playing. I start the app with the Start DG Round shortcut and then disable the entire screen with Guided Access so nothing gets pressed in my pocket.

Steps to Use

  1. Use Open GPX Tracker to record your second-by-second location while you play

  2. Run Start DG Round when you are ready to begin a round

    • Leave the clock app open (and the screen disabled with Guided Access) to keep the screen alive

  3. Tap the disc you are about to throw to your phone before each shot and at each basket

It takes a bit of practice to tap in the right spot, but I get it on the first try most of the time after playing one or two rounds. I keep my phone upright in my back pocket and try to touch the bottom of the phone with the rim of the disc - that puts the sticker very close to the top of the phone where the NFC reader is located. It looks something like:

Data Post-Processing

The data post processing was originally all done in Julia, which is my language of choice these days. You can find that code here.

I ended up going down a large rabbit hole when trying to implement a browser interface to round and course editing. As a result, I learned and implemented the post-processing toolchain in Go. This project was a great learning experience in Go, JavaScript, HTML, and general web infrastructure.

Input Data

After the round is complete we will have two files, a timestamp .csv file and a .gpx file recording our location throughout the round.

The timestamp file should look like:


And the gpx file:

<?xml version="1.0" encoding="UTF-8"?>
<gpx xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" version="1.1" creator="Open GPX Tracker for iOS">
			<trkpt lat="33.07901132857842" lon="-117.05936818536237">
			<trkpt lat="33.079017415810696" lon="-117.05934814846991">

Those files are parsed and interpolated to produce a raw recording of the round that internally is represented rather simply:


We now should have a full description of our round. We just need to place it in the context of a course.

Course Database

In order to track consistent stats we would like to be able to determine the exact course that was played. This is best achieved by mapping the marked locations to a course database. The course database should contain the following information:

Additionally, future installments could contain geoJSON polygon information outlining fairways and OB (which can change based on the pin and tee locations). Maybe one day...

The course database is well suited for JSON due to its hierarchical nature. Here is the implementation of the course we commonly play at:

	"id": "kit_carson",
	"name": "Kit Carson",
	"loc": [33.079323,-117.058426],
	"holes": [
			"id": "1",
			"tees": [
					"id": "reg",
					"loc": [33.079341451548174,-117.05858254892743]
			"pins": [
					"id": "A",
					"loc": [33.07994748732632,-117.05780006680942]
					"id": "B",
					"loc": [33.08009547439126,-117.05760343347208]
					"id": "C",
					"loc": [33.07967526332277,-117.05775281076058]
			"pars": [
					"tee": "reg",
					"pin": "A",
					"par": 3
					"tee": "reg",
					"pin": "B",
					"par": 3
					"tee": "reg",
					"pin": "C",
					"par": 3

The creation and editing of a course can all be done with a graphical interface provided by the make-course and edit-course commands. Here is a screenshot of the editing interface:

Disc Database

We should also keep track of the parameters that define a disc. For ease of use we can maintain a database of "molds" that define all the common parameters for each type of disc, and a database of named user discs that record the values specific to the actual discs the user owns.

The molds database is populated with data from alldiscs.com. It is easily implemented as a .csv and looks like:

LATITUDE_64_MISSILENLatitude 64MissilenDistance153-0.54.5
LATITUDE_64_RAKETENLatitude 64RaketenDistance154-23
AXIOM_EXCITEAxiom DiscsExciteDistance14.55.5-22
AXIOM_TANTRUMAxiom DiscsTantrumDistance14.55-1.53
MVP_DIMENSIONMVP Disc SportsDimensionDistance14.5503

The personal disc database is also implemented as a .csv file that references the id column of the molds database. The file referenced in the image column will be used as symbols denoting the use of that disc on the map visualization of the played round.


At this point we have defined all the relevant information to process our round. Let's get going!

Played Round Inference

The main function the software should provide is inference. We want to take care of all the heavy lifting with the pre-defined databases so the per-round data collection can be as easy and seamless as possible. There are a few things we need to infer about the round:

With that all determined we will be able to appropriately score the round and provide accurate visualizations.

Most of these are straightforward. The played course will just be the nearest course to the first marked location. The hole that was played will be the hole that owns the inferred teebox. The pin/tee that was played on each hole will be the nearest pin/tee to the marked location. The only difficult problem will be to determine which taps mark a completed hole and then a tee on the next hole. We need to be careful because we may be putting from another basket location or from near the teebox on the next hole. We also may not be playing the holes in order, so we can't assume that after hole 3 we should look at the teebox for hole 4. As of now, the best algorithm I could think of uses the following criteria to determine if a tap is marking the end of a hole:

This algoithm is not perfect (maybe you hit a tree <20m from the teebox) but it provides a solid start to processing the round if you remember to tap before each shot.

We are not going to assume that the data collection or inference is perfect, however. To remedy this we will provide the user with a visualization of the round as it is interpreted. The user will then be able to edit the data through a map GUI until the visualization accurately reflects the played round.

make-round Command

The make-round command wraps all the above functionality. It:

Below is an example use of make-round. The rudimentary inference was not perfect, the disc was tapped too far away (>10m) from the 14th teebox when recording. After dragging the stamped location towards the teebox the inference is updated to correctly reflect the round as played. When the "Save" button is clicked the icons are updated in two ways:

The saved round is recorded in a tidy-ish .csv file with some round metadata in the header:

RoundID: 2021-07-26-08-19-29_-_kit_carson
CourseID: kit_carson
CourseName: Kit Carson


Statistics Generation

make-stats Command

Finally, we need a way to put together the recorded rounds and generate some data visuals. The make-stats command reads in all the played rounds and generates csv files for further analysis at different levels of detail:

Dashboard View

Additionally, make-stats breaks out a few interesting statistics into a dash.json file. That file is displayed on a public dashboard based on the AdminLTE bootstrap template. You can click the rows in the "Rounds" table to view them!