Offline Web-Maps

Recently I said something silly to a client:

...and because we're not using a database, you'll be able to run this map offline on your local computer.

Great! They give lots of presentations where they want to show off the map and this means they wouldn't have to rely on conference wifi, the most overstretched resource on the planet. We delivered the completed map to be deployed on their site along with the instructions:

Just open index.html in your web-browser and you'll be up and running.

Nope.

Did you try Chrome instead of IE?

That ol' chestnut? Still nope.

Why was it working for me but not for them? We developed this map running the files locally so what was the difference between our machines and theirs?

The Development Webserver

Everyone doesn't use one of these? If you develop stuff for the web, you most likely have some kind of webserver running on your computer (MAMP, WAMP, LAMP, et al) so when you view a project in the browser, the URL looks like:

http://localhost/...

The page is loaded over the same protocol used when it is on the web, it's just not looking outside your actual computer to do so. However, if you just open any old HTML file in the browser, you'd get something like:

file:///Users/...

This local file protocol means your browser is loading your file without it being initially processed by a webserver, which opens it up to some potential security issues that make the browser say "nah... not loading that guy". If it's a really simple page (static HTML, CSS), no problem. It is a problem, however, if you're running any XMLHttpRequests to asynchronously load data. In D3, these requests look something like:

d3.json()

or any of these. In jQuery, this is the guy you're looking for:

$.ajax()

The Fix

Once I've located all the XMLHttpRequests on the page, it's a relatively simple job to replace them. In this map, I have this block of code that loads a topojson file:

d3.json("json/states.json", function(error, us) {
   map.selectAll("path")
      .data(topojson.feature(us, us.objects.states).features)
      .enter()
      .append("path")

This loads the states.json file and as soon as it is received, runs the function that takes an error and a variable called us. The second variable contains the contents of the loaded file and that's the important one for what we're doing.

There's two changes to make. The first is to open states.json and at the beginning of the file, add:

var us =

This takes the proceeding JSON object and declares it as the variable us, the same variable used in the success function for the d3.json request. Now, delete the first line of the request (and the closure at the end) so you're code becomes:

map.selectAll("path")
   .data(topojson.feature(us, us.objects.states).features)
   .enter()
   .append("path")

The new code is still looking for the us variable, but now it isn't wrapped in the d3.json function that waits for the XMLHttpRequest to finish before executing. It will just run as soon as it gets to that line in the flow of the code. Because of that, you need to make sure that data is available when it gets there.

In your index.html file add the modified states.json file like it was a regular javascript file:

<script src="json/states.json"></script>

before the javascript file that executes the code. Just put it at the top of the <head>, below your CSS files. You don't need to worry about it slowing down the load time of the page because none of these files are being transmitted over the web.

You now have a set of files that will work wherever you send them. Anyone can open your index.html file in a browser and see your map as you intended.

Some Caveats

  1. Watch out for convenience functions like d3.csv() that have conversions built into them. This method only works for files that are already Javascript objects and can be declared as variables. If you must have your data stored as CSV, you'll need to alter the file to become one long string and parse it yourself.
  2. The variable you use in the external file will be global. Be careful with that. You can't reuse any of those variables in your code so it's worth a quick search to make sure the names don't come up again.
  3. There's a reason we don't load our data like this all the time. It will be slow and gross.
  4. The latest version of Firefox appears to be cool with XMLHttpRequests over file:/// so if you're lazy, just tell your clients to use that instead.