A Replacement for the Google Image Charts API (Open Source)

The Google Image Charts API shut down on March 14, 2019. Although it was deprecated since 2012, the shutdown has disappointed many developers.

Although Google expects developers to replace the Image Charts API with Google Charts, unfortunately this is not always possible. Google Charts requires Javascript, which means that it is incompatible with environments that cannot run Javascript, such as email, SMS, and chatbots.

QuickChart is an open-source project that provides an easy way to generate charts as PNGs. I built and released it in March 2019 and it has since served millions of chart images.

Charts are defined by a single URL. For example, this url:

https://quickchart.io/chart?c={type:'bar',data:{labels:['January','February','March','April','May'],datasets:[{label:'Dogs',data:[50,60,70,180,190]},{label:'Cats',data:[100,200,300,400,500]}]}}

Will produce this image:

Image Charts

QuickChart uses the popular Chart.js API, so if you're using Chart.js in your Javascript, copy over your chart definition to a QuickChart URL and things will just work. You can use the Chart.js documentation to customize your chart.

I open sourced the project because I wanted to avoid a repeat of the Google Image Charts scenario, in which people are dependent on proprietary image chart solutions that can be shut down at any time.

Head to QuickChart.io to try it out. Enjoy!

Getting out the Vote with a JS snippet

On October 11th I released Vote Banner, a JS snippet that helps people exercise their voting rights and access relevant voting information.

vote banner

In the 3.5 weeks leading up to the election, it was loaded 1,087,248 times and seen by about 257,000 unique users, far more than I would have expected for a simple project such as this.

The impact

What was the impact? Hard to say. A quarter million people viewed it. Maybe it helped remind some of them to make plans to vote. Maybe they reminded friends and family. We can't measure these things.

The CTR on the banner was quite low, 0.3%. That means about 678 people actually clicked through to register to vote or look up their polling place (clicks were evenly split between both options).

I'm not hung up on the low CTR, though. That's better than ad CTR on some websites. Blanket political calls to action don't perform that well, a phenomenon that I've noted before with the Call Congress hotline. But every little bit counts.

Next steps

Vote Banner will go on ice until the 2020 election season. Next time, I'd like to release it ahead of time, including perhaps for primaries, and have it point to state-level data based on the user's IP address.

I'd also like to make distribution easier by e.g. making it a Cloudflare application, which can be added to your site in one click.

Interested in contributing? It's open source here.

Using analytics.js as a standalone library

Segment.io has developed analytics.js, a handy tool that allows you to send events to Google Analytics, Mixpanel, and many other services using one standard API.

analytics.js is open source but the documentation assumes you're using Segment's paid service.

There's a helpful post from Pivotal that describes self-hosted analytics.js, but its code contains a bug that causes events to not be tracked until analytics.js is loaded. Depending on how you load dependencies, this introduces a race condition. About 40% of my .page and .track calls were dropped as a result.

The Pivotal code attempts to record a queue of method calls before analytics.js is loaded. According to Segment's cofounder, the analytics.js library assumes it is loaded synchronously. As a result, analytics.load overwrites the queue and we lose our events.

Code walkthrough

First, create a queue and a stubbed analytics object. I separated the queue from the analytics object because we don't want the queue overwritten when analytics.js is loaded.

If you put this chunk before the rest of your JS, you can call analytics.page(),analytics.track({...}) without worrying about load timing.

The fixed code is available here under MIT license.

// Create a queue to push events and stub all methods
window.analytics || (window.analytics = {});  
window.analytics_queue || (window.analytics_queue = []);  
(function() {
  var methods = ['identify', 'track', 'trackLink', 'trackForm', 'trackClick', 'trackSubmit', 'page', 'pageview', 'ab', 'alias', 'ready', 'group', 'on', 'once', 'off'];

  var factory = function(method) {
    return function () {
      var args = Array.prototype.slice.call(arguments);
      args.unshift(method);
      analytics_queue.push(args);
      return window.analytics;
    };
  };

  for (var i = 0; i < methods.length; i++) {
    var method = methods[i];
    window.analytics[method] = factory(method);
  }
})();

This next part loads the script asynchronously. If you don't want to host analytics.js yourself, you could use cdnjs:

// Load analytics.js after everything else
analytics.load = function(callback) {  
  var script = document.createElement('script');
  script.async = true;
  script.type = 'text/javascript';
  script.src = '/assets/js/analytics.min.js';     // <--- your url here
  if (script.addEventListener) {
    script.addEventListener('load', function(e) {
      if (typeof callback === 'function') {
        callback(e);
      }
    }, false);
  } else {  // IE8
    script.onreadystatechange = function () {
      if (this.readyState == 'complete' || this.readyState == 'loaded') {
        callback(window.event);
      }
    };
  }
  var firstScript = document.getElementsByTagName('script')[0];
  firstScript.parentNode.insertBefore(script, firstScript);
};

Finally, in the load step, process the queued method calls:

analytics.load(function() {  
  analytics.initialize({
    'Google Analytics': {
       trackingId: 'UA-XXX-1'
     },
     'Mixpanel': {
       token: 'XXX'
     }
  });

  // Loop through the interim analytics queue and reapply the calls to their
  // proper analytics.js method.
  while (window.analytics_queue.length > 0) {
    var item = window.analytics_queue.shift();
    var method = item.shift();
    if (analytics[method]) analytics[method].apply(analytics, item);
  }
});

I'm releasing the fixed code under the MIT license. Have a look at the full code on Github.

Resources for the 2017 solar eclipse

More for my benefit than anyone else's, I've compiled these resources on the August solar eclipse.

Location

The most detailed interactive map of the eclipse path is available here. NASA also has an official interactive map of the eclipse path.

Use Google Maps street view to figure out where exactly you're going to park and stand.

Time

The total solar eclipse will last just a few minutes. Enter the zip code of your observation site in this calculator to determine exactly when the moon will cover the sun (this information is also available on the map above).

Is it going to be cloudy?

Eyeball this map, which is colored by average cloud cover.

Then use Weatherspark to look at historical cloud cover for your particular location. Drill down into the cloud diagram and select August 21 to get hour-by-hour historical predictions (example: Depoe Bay, OR).

Less Important Questions from StackOverflow's Developer Survey

The StackOverflow post on tabs vs. spaces made quite a splash yesterday with the revelation that people who indent their code with spaces earn more, on average, than people use tabs.

They've made their dataset available for download so I wrote a quick script to see what else we can learn about developer salaries.

GIF vs. JIF

There's not much difference.

Choice of editor

This is the one we've all been waiting for.

Ship it?

Fortune favors the bold.

Clicky keys

You probably should just talk to your officemate instead of using this result.

Open source helps

Toward this end, I've open sourced this analysis. I still don't really know my way around pandas/Jupyter, so maybe you can do better.