Dark Sky plot for GeekTool

Shortly after my post on combining the Dark Sky API with the Python matplotlib library to create rain intensity plots with confidence intervals, Jay Hickey and Barron Bichon both decided they wanted plots like that on their Desktops, so each of them adapted my script and turned it into GeekTool geeklet. Since this was a much better use of the script than I had ever put it to, I stole some of their ideas and made my own geeklet.

For years I’ve been putting textual weather information and a radar image on my Desktop via NerdTool, a GeekTool workalike that I switched to when I found GeekTool eating up a lot of RAM. Recently, though, I learned that NerdTool can’t display a PNG image as is—it always scales the image, even when you tell it not to. This was not something I wanted for my Dark Sky plot, so I switched back. I think Tynsoe has fixed GeekTool’s memory problem, but I’ll be keeping an eye on it.

Here’s what the plot looks like on my Desktop, which is the Solid Aqua Dark Blue color that OS X has had for years as one of its standard solid Desktop colors.

Dark Sky GeekTool plot

The horizontal gradations represent increasing rain intensity, the solid white line is the mean predicted intensity, and the faint band around it is the interquartile range (IQR).

Here’s the script that produces the plot:

python:
 1:  #!/usr/bin/python
 2:  
 3:  from __future__ import division
 4:  import json
 5:  import urllib
 6:  from os import environ
 7:  from sys import exit, argv
 8:  import matplotlib.pyplot as plt
 9:  from datetime import datetime, timedelta
10:  
11:  # Where to save the plot.
12:  plotfile = environ['HOME'] + '/Pictures/ds-rain.png'
13:  
14:  # The list of times on the x axis, 10 minutes apart.
15:  checktime = datetime.now()
16:  plotminutes = [10, 20, 30, 40, 50]
17:  plotlabels = [ (checktime + timedelta(minutes=m)).strftime('%-I:%M')
18:                  for m in plotminutes ]
19:  
20:  # The probability and number of standard deviations
21:  # associated with the upper and lower bounds.
22:  nUpper = .6745      # 75th percentile
23:  nLower = -.6745     # 25th percentile
24:  
25:  # Get the latitude and longitude from the command line
26:  # or use default values from downtown Naperville.
27:  try:
28:    lat = argv[1]
29:    lon = argv[2]
30:  except IndexError:
31:    lat = 41.772903
32:    lon = -88.150392
33:  
34:  # Get my API key and construct the URL
35:  try:
36:    with open(environ['HOME'] + '/.darksky') as rcfile:
37:      for line in rcfile:
38:        k, v = line.split(':')
39:        if k.strip() == 'APIkey':
40:          APIkey = v.strip()
41:      dsURL = 'https://api.darkskyapp.com/v1/forecast/%s/%s,%s' %
42:              (APIkey, lat, lon)
43:  except (IOError, NameError):
44:    print "Failed to get API key"
45:    exit()
46:  
47:  # Get the data from Dark Sky.
48:  try:
49:    jsonString = urllib.urlopen(dsURL).read()
50:    weather = json.loads(jsonString)
51:  except (IOError, ValueError):
52:    print "Connection failure to %s" % dsURL
53:    exit()
54:  
55:  # Pluck out the hourly rain forecast information.
56:  startTime = weather['hourPrecipitation'][0]['time']
57:  intensity = [ x['intensity'] for x in weather['hourPrecipitation'] ]
58:  upper = [ min(x['intensity'] + x['error']/3*nUpper, 75) for x in weather['hourPrecipitation'] ]
59:  lower = [ max(x['intensity'] + x['error']/3*nLower, 0) for x in weather['hourPrecipitation'] ]
60:  time = [ (x['time'] - startTime)/60 for x in weather['hourPrecipitation'] ]
61:  
62:  # Plot the intensity ranges.
63:  plt.fill_between([0, 59], [15, 15], [0, 0], color='#ffffff', alpha=.01, linewidth=0)
64:  plt.fill_between([0, 59], [30, 30], [15, 15], color='#ffffff', alpha=.02, linewidth=0)
65:  plt.fill_between([0, 59], [45, 45], [30, 30], color='#ffffff', alpha=.04, linewidth=0)
66:  plt.fill_between([0, 59], [75, 75], [45, 45], color='#ffffff', alpha=.08, linewidth=0)
67:  
68:  # Plot the values.
69:  plt.plot(time, intensity, color='#ffffff', linewidth=3)
70:  plt.fill_between(time, upper, lower, color='#ffffff', alpha=.05, linewidth=0)
71:  plt.box()
72:  plt.axis([0, 59, 0, 65])
73:  plt.xticks(plotminutes, plotlabels, color='#ffffff')
74:  plt.yticks([])
75:  plt.tick_params('y', length=0, color='#ffffff')
76:  plt.tick_params('x', color='#ffffff')
77:  
78:  plt.savefig(plotfile, dpi=50, transparent=True, bbox_inches='tight')

Very little of the script has changed from the original. The main differences are:

Two geeklets are needed: one that runs the script and another that displays the resulting plot. I have them set to refresh every five minutes; by starting them on a minute that’s a multiple of 5, I get nice round numbers in the times along the x-axis.

One thing I didn’t steal was Barron’s spline smoothing code. I don’t use smoothing in the plotting I do for work, so I wasn’t inclined to dig into his code to see how the smoothing worked. I may change my mind if my plots look too jagged.

One last thing: If you try to use my script, or Jay’s or Barron’s, you may find that a little rocket icon appears momentarily in your Dock whenever the script that produces the plot runs.

Python rocket

If you’re annoyed by that (as I am), you can stop it from happening by following this tip to edit the Info.plist file of the Python app associated with that icon. Be aware that you’ll have to make the edit with administrator privileges and a future update by Apple may wipe out your changes, forcing you to do it again. To me, it’s worth it—I hate that little rocket.


7 Responses to “Dark Sky plot for GeekTool”

  1. Seth Brown says:

    This is great. I’m definitely going to add this to my desktop. Thanks for hacking on the Dark Sky API.

    I know nothing about weather, but using the standard deviation as a measure of dispersion is interesting. The standard deviation is sensitive to outliers and is usually used for symmetrical distributions. Does the weather data meet these assumptions?

  2. Dr. Drang says:

    Not only is it unlikely that the distribution is symmetrical (I’ve seen examples where the standard deviation is nearly equal to the mean), it’s a conditional standard deviation. From the docs:

    The 3-sigma error of the intensity value, conditional on precipitation. In other words, if there is precipitation, then 99.7% of the time the intensity value will be between intensity ± error.

    So I’m playing a bit fast and loose when calling the range I plot the IQR, but it’s close enough to the truth for my purposes.

  3. Barron Bichon says:

    Doesn’t the second sentence in your quote of the docs imply symmetry? If they only provide one error value and intensity ± error provides their 2-sided confidence bounds, could it be asymmetric? It’s the same movement from the mean (intensity) in both directions. Am I missing something?

    That said, the fact that you have to check for values < 0 (line 59) says that maybe it shouldn’t be symmetric (negative intensity doesn’t make sense), but it does sound like that’s how they are modeling it (or at least what they are making available via the API).

  4. Dr. Drang says:

    They’re definitely making an assumption of normality in that second sentence, but results with a coefficient of variation close to 1 are pretty clear evidence that that assumption isn’t always reasonable.

  5. Jay Hickey says:

    Great plot! Looks fantastic. And good call with the plist change to hide the rocket icon. My dock is hidden so I don’t often see it, but when I do it’s pretty distracting.

    Have you noticed any text aliasing issues with your switch to GeekTool? It seems like NerdTool does a better (but still not perfect) job at antialiasing text.

  6. Dr. Drang says:

    Jay, I used to see a big difference in font rendering between the two, but not so much now. Either GeekTool has gotten better or my eyes have gotten so bad I can’t see the difference anymore.

  7. Mike says:

    any chance of you posting the geeklet? this is seriously useful, but i have no coding skills/background