I like freediving & spearfishing. I don't like hungry sharks. In Australia, there has been a large number of incidents over the years, but just how many? And where? Colin Ristig from yhat recently posted a cool (script) to parse and augment data from the Global Shark Attack File. I thought I'd add to his pipeline to generate an interactive web-based map.

Here is the final result. A map of shark attacks in Australia:





















First, I needed to clean the spreadsheet data. The geopy geocoders get confused with locations such as "In Swan River at Freshwater Bay, Claremont, 5 miles from river mouth". There were only a handful of these descriptions, so I did this manually. Ideally, one could try to use nltk or similar to extract the relevant information.

Next, the Rodeo_sharks_part2.py file needed to be customised for my location (Australia).

Once I had the relevant sharks_coords.csv file, it needed to be converted to geojson for injestion by the javascript. There didn't seem to be any standard tool to do this, so I just rolled my own using the geojson module. This also allowed me to add some extra information, like the map icons.

#!/bin/env python
# csv2geojson.py

import csv
import geojson
import random

id = 0
Feats = [ ]

with open('./sharks_coords.csv') as csvfile:
    reader = csv.DictReader(csvfile)
    for row in reader:
        # Add a title and description based on the relevant fields
        locstr = str(row['Location']) + ', ' + str(row['Area'])
        title = "{:s} ({:d})".format(locstr, int(float(row['year'])))
        description = "{:s}, {:s}, {:s}".format(str(row['Activity']), str(row['Species']), str(row['Injury']))
        row['title'] = title
        row['description'] = description

        # Add icon descriptor to indicate fatal attacks
        row['icon'] = { 'iconUrl': 'shark-blue.png',
                        'iconSize': [ 35, 90 ],
                        'iconAnchor': [ 17, 45 ],
                        'popupAnchor': [ 17, -45 ],
                        'className': 'dot' }
        if (row['Fatal'] == 'True'):
            row['icon']['iconUrl'] = 'shark-red.png'

        if (row['longitude'] and row['latitude']):
            id += 1
            # Add random jitter of up to approximately 10m (0.0001deg) to visually separate identical locations
            rx = 0.0001*random.random()
            ry = 0.0001*random.random()
            # Create a geojson Point & Feature
            point = geojson.Point((float(row['longitude']) + rx, float(row['latitude']) + ry))
            feat = geojson.Feature(id=id, geometry=point, properties=row)

featcoll = geojson.FeatureCollection(Feats)

with open('./sharks_coords.geojson', 'w') as outfile:
    geojson.dump(featcoll, outfile)

Initially, I started using plain Leaflet javascript library, but pretty soon graduated to using the Mapbox library, which had many more features, like featureLayers. Below is the the resulting sharks.js file, with marker clusters, custom icons and tooltips.

// sharkmap.js

L.mapbox.accessToken = 'pk.my.public.access.token';
var mapCluster = L.mapbox.map('map-cluster')
  .setView([-26, 133], 4)

var featureLayer = L.mapbox.featureLayer()
  .on('ready', function(e) {
    var clusterGroup = new L.markerClusterGroup({
        iconCreateFunction: function (cluster) {
            var childCount = cluster.getChildCount();
            var c = ' marker-cluster-';
            if (childCount < 10) {
                c += 'small';
            } else if (childCount < 100) {
                c += 'medium';
            } else {
                c += 'large';
            return new L.DivIcon({ html: '<div><span><b>' + childCount + '</b></span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });

    e.target.eachLayer(function(layer) {

// Set a custom icon on each marker based on feature properties.
featureLayer.on('layeradd', function(e) {
    var marker = e.layer,
        feature = marker.feature;
    feature.properties.icon.iconUrl = '/files/icons/' + feature.properties.icon.iconUrl;

// Add tooltips
featureLayer.on('mouseover', function(e) {
featureLayer.on('mouseout', function(e) {

Finally, the html to render. The div with id=map is where our map gets placed.

    <meta charset=utf-8 />
    <title>Shark Attacks Map</title>
    <script src='https://api.tiles.mapbox.com/mapbox.js/v2.2.2/mapbox.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox.js/v2.2.2/mapbox.css' rel='stylesheet' />
      body { margin:0; padding:0; }
      .map { position:absolute; top:0; bottom:0; width:100%; }
    <script src='https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v0.4.0/leaflet.markercluster.js'></script>
    <link href='https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v0.4.0/MarkerCluster.css' rel='stylesheet' />
    <link href='https://api.tiles.mapbox.com/mapbox.js/plugins/leaflet-markercluster/v0.4.0/MarkerCluster.Default.css' rel='stylesheet' />
    <div id='map' class='map-cluster'></div>
    <script src="sharkmap.js"></script>

The full code is here.