{
 "metadata": {
  "name": "plotly_polar_vortex"
 },
 "nbformat": 3,
 "nbformat_minor": 0,
 "worksheets": [
  {
   "cells": [
    {
     "cell_type": "heading",
     "level": 1,
     "metadata": {},
     "source": [
      "How cold has this winter been?"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Let's check using IPython and plot.ly!\n",
      "\n",
      "I will plot a world map of the average daily temperature anomalies (i.e. deviation from climatology) for December 2013 and Februray 2014.\n",
      "\n",
      "This notebook will use:\n",
      "\n",
      "* plot.ly's 'heatmap' plot type for colored contour maps compatible with plot.ly [see https://plot.ly/api/python/docs/heatmaps ]\n",
      "* NetCDF data imports which are very common in Atmospheric/Oceanic sciences [ref. http://docs.scipy.org/doc/scipy/reference/generated/scipy.io.netcdf.netcdf_file.html ]\n",
      "* Basemap module for coastlines and countries data [ref. http://matplotlib.org/basemap/ ]\n",
      "\n",
      "\n",
      "Note that, I would have loved to use Basemap also to project the data to a curvilinear grid (e.g. the Robinson projection for aesthestics). <br>\n",
      "However, plot.ly's 'heatmap' plot type currently only accepts 1-dimensional 'x' and 'y' coordinate vectors. <br>\n",
      "So, my plot will use the simplest of all cylindrical projections (called the equirectangular projection) where all meridians have constant spacing.\n",
      "\n",
      "I divided the tasks as:\n",
      "\n",
      "- Setup plot.ly\n",
      "- Import the temperature data\n",
      "- Get the coastline and country boundary data\n",
      "- Setup plot layout and color map\n",
      "- Plot!"
     ]
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "1) Setup plot.ly"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Import plot.ly with python api [see https://plot.ly/api/python/getting-started ], \n",
      "sign in, create shortcut to plot.ly object and check version:"
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "import plotly\n",
      "\n",
      "username='etpinard'       # my username\n",
      "api_key = 'a35m7g6el5'    # my api key\n",
      "\n",
      "py = plotly.plotly(username,api_key)  # shortcut to plot.ly object\n",
      "plotly.__version__                    # check ploty.ly version"
     ],
     "language": "python",
     "metadata": {},
     "outputs": [
      {
       "output_type": "pyout",
       "prompt_number": 1,
       "text": [
        "'0.5.7'"
       ]
      }
     ],
     "prompt_number": 1
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Define a function to embed plot.ly graph in IPython notebook [ref. http://nbviewer.ipython.org/github/plotly/IPython-plotly/blob/master/Plotly%20Quickstart.ipynb ]:"
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "from IPython.display import HTML\n",
      "\n",
      "def show_plot(url, width=1000, height=500):\n",
      "    s = '<iframe height=\"%s\" id=\"igraph\" scrolling=\"no\" seamless=\"seamless\" src=\"%s\" width=\"%s\"></iframe>' %\\\n",
      "    (height+50, \"/\".join(map(str,[url, width, height])), width+50)\n",
      "    return HTML(s)"
     ],
     "language": "python",
     "metadata": {},
     "outputs": [],
     "prompt_number": 2
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Import all other packages needed for executing this notebook:"
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "import numpy as np                         # numpy ... of course\n",
      "from scipy.io import netcdf                # scipy NetCDF i/o module\n",
      "from mpl_toolkits.basemap import Basemap   # Basemap model of the matplotlib toolkit"
     ],
     "language": "python",
     "metadata": {},
     "outputs": [],
     "prompt_number": 3
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "2) Import temperature data"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "I got data on this http://www.esrl.noaa.gov/psd/data/composites/day/ .\n",
      "\n",
      "This website does not allow to *code* your output demand and/or use `wget` to download the data. <br>\n",
      "However, the data I used can be found in a only a few clicks:\n",
      "\n",
      "- Select `Air Temperature` in `Varaibles`\n",
      "- Select `Surface` in `Analysis level?`\n",
      "- Select `Dec` | `1` and `Jan` | `31` \n",
      "- Enter `2014` in the `Enter Year of last day of range` prompt\n",
      "- Select `Anomaly` in `Plot type?`\n",
      "- Select `All` in `Region of globe`\n",
      "- Click on `Create Plot` and a temporary NetCDf file can be downloaded on that new."
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "The data represents the Dec2013-Jan2014 average daily surface air temperature anomaly (in deg. C) with respect to 1981-2010 climatology.\n",
      "\n",
      "That is, our temperature variable of interest $T$ is\n",
      "\n",
      "$$ T = \\frac{1}{62 \\; \\text{days}} \\sum_{i=1}^{62} \\bigg( T_i - \\bar{\\hskip2pt T}_i \\bigg) $$\n",
      "\n",
      "where $\\bar{\\hskip2pt T}_i$ is daily surface air temepature 1981-2010 climatological mean\n",
      "and $i$ is the day index starting from 1 December 2013.\n",
      "\n",
      "In the NetCDF file, $T$ is refered to as `air`. <br>\n",
      "The NetCDF file also includes a longitude (`lon`), latitude (`lat`) and time singleton (`time`). <br>\n",
      "Note also that this dataset is of quite low resolution 2.5 deg x 2.5 deg (about 300 km) resolution.\n",
      "\n",
      "Now, let's extract these arrays [inspired by http://earthpy.org/interpolation_between_grids_with_basemap.html ]:"
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "f_tas_path = 'compday.qQbv_4_mY1.nc'         # file must be in working directory\n",
      "f_tas = netcdf.netcdf_file(f_tas_path, 'r')  # will most likely have a different name if you downloaded it yourself\n",
      "\n",
      "lon = f_tas.variables['lon'][::]             # copy as an array\n",
      "lat = f_tas.variables['lat'][::-1]           # invert the latitude vector, now South to North\n",
      "air = f_tas.variables['air'][0,::-1,:]       # squeeze out the time dimension, invert latitude index\n",
      "\n",
      "# shift 'lon' from [0,360] to [-180,180] for better-looking plots\n",
      "lon_tmp = lon.copy()\n",
      "for n, l in enumerate(lon_tmp):\n",
      "    if l >= 180:\n",
      "        lon_tmp[n] = lon_tmp[n] - 360. \n",
      "lon = lon_tmp                         # gives [0,180]U[-180,-2.5]\n",
      "mid_lon = lon.shape[0]/2              # index of second of the -180 lon\n",
      "lon_east = lon[0:mid_lon]             # [0,180]\n",
      "lon_west = lon[mid_lon:]              # [-180,0]\n",
      "lon = np.hstack((lon_west, lon_east)) # stack the 2 halves\n",
      "\n",
      "# correspondingly shift the 'air' array\n",
      "air_east = air[:,0:mid_lon]\n",
      "air_west = air[:,mid_lon:]\n",
      "air = np.hstack((air_west, air_east))\n",
      "\n",
      "f_tas.close()   # close NetCDF file\n",
      "\n",
      "#print lon\n",
      "#print lat"
     ],
     "language": "python",
     "metadata": {},
     "outputs": [],
     "prompt_number": 4
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "3) Get coastline and country boundary data"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "The Basemap module includes data for drawing coastlines and country boundaries onto world maps. <br>\n",
      "Adding coastlines and/or country boundaries on a regular Python (i.e, non-plot.ly) figure can easily be done\n",
      "with the `.drawcoaslines()` or `.drawcountries()` Basemap methods. \n",
      "\n",
      "Here, I retrive the Basemap plotting data (or polygons) and convert them to longitude/latitude arrays compatible with plot.ly <br>\n",
      "[inspired by http://stackoverflow.com/questions/14280312/world-map-without-rivers-with-matplotlib-basemap ]\n",
      "\n",
      "In other words, the goal is to plot each *continuous* coastline and country boundraies as 1 plot.ly *trace*.\n",
      "\n",
      "Note that there are other ways to this, such as import shapefile file off the web [e.g. http://www.naturalearthdata.com/downloads/110m-cultural-vectors/ ]."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "m = Basemap()     # shortcut to Basemap object (no need to specify projection type)\n",
      "\n",
      "# Function assigning plot.ly plotting options for each coastline/country trace\n",
      "def MakePlotData(lon_cc,lat_cc):\n",
      "    ''' lon_cc: lon. of coastline/country, lat_cc: lat of coastline/country '''\n",
      "    return {\n",
      "            'x': lon_cc,\n",
      "            'y': lat_cc,  \n",
      "            'mode': 'lines',\n",
      "            'line': {'color': 'rgb(0,0,0)'},\n",
      "            'name': ''\n",
      "           }\n",
      "\n",
      "# Functions converting coastline/country plot polygons to lon/lat arrays\n",
      "def polygons_to_lonlat(N_poly,poly_paths):\n",
      "    ''' N_poly: number of polygon to convert, poly_paths: paths to polygons '''\n",
      "    data_tmp = []  # init. plotting list \n",
      "    for i_poly in range(N_poly):\n",
      "        poly_path = poly_paths[i_poly]\n",
      "        \n",
      "        # get the Basemap coordinates of each segment\n",
      "        coords_cc = np.array([(vertex[0],vertex[1]) \n",
      "                              for (vertex,code) in poly_path.iter_segments(simplify=False)])\n",
      "        \n",
      "        # convert coordinates to lon/lat by 'inverting' the Basemap projection\n",
      "        lon_cc, lat_cc = m(coords_cc[:,0],coords_cc[:,1], inverse=True)\n",
      "        \n",
      "        # add plot.ly plotting options\n",
      "        data_tmp.append(MakePlotData(lon_cc,lat_cc))\n",
      "        \n",
      "    return data_tmp\n",
      "        \n",
      "# 1) 'draw' coastlines using the Basemap built-in methods\n",
      "m_draw = m.drawcoastlines()\n",
      "poly_paths = m_draw.get_paths()  # get coastline polygon paths\n",
      "N_poly = 91                      # use of the 91 biggest coastlines (i.e. don't show rivers)\n",
      "data_cc1 = polygons_to_lonlat(N_poly,poly_paths)\n",
      "\n",
      "# 2) Similarly 'draw' countries boundaries\n",
      "m_draw = m.drawcountries()\n",
      "poly_paths = m_draw.get_paths() # get country boundaries polygon paths\n",
      "N_poly = len(poly_paths)     # use all countries boundaries\n",
      "data_cc2 = polygons_to_lonlat(N_poly,poly_paths)\n",
      "\n",
      "# Concatenate coastlines and country boundaries list\n",
      "data_cc = data_cc1+data_cc2\n",
      "\n",
      "#print data_cc\n",
      "#print len(data_cc), len(data_cc1), len(data_cc2)"
     ],
     "language": "python",
     "metadata": {},
     "outputs": [],
     "prompt_number": 5
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "4) Setup plot layout and color map"
     ]
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "Layout options:"
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "layout_style = {\n",
      "                'autosize': False,\n",
      "                'width': 1000,\n",
      "                'height': 500,\n",
      "                'margin': {'t':100, 'b':50, 'l':50, 'r':0, 'pad':4},\n",
      "                \"plot_bgcolor\": \"rgb(255,255,255)\",                     # white plot bg\n",
      "                \"paper_bgcolor\": \"rgb(222,222,222)\",                    # gray frame bg\n",
      "                'showlegend': False,\n",
      "                'hovermode': 'closest',\n",
      "                'title': \"Average daily surface air temperature anomalies [in deg. C] <br>\\\n",
      "                          from 2013-12-01 to 2014-01-31 with respect to 1981-2010 climatology\",   \n",
      "                'titlefont': {'color':'rgb(0,0,0)', 'size':18},\n",
      "                'xaxis': {\n",
      "                          #'title': 'longitude',\n",
      "                          'range': [lon[0],lon[-1]],       # show the whole globe\n",
      "                          'showgrid': False,\n",
      "                          'zeroline': None,\n",
      "                          'ticks': None,\n",
      "                          'showticklabels': False\n",
      "                         },\n",
      "                'yaxis': {\n",
      "                          #'title': 'latitude',\n",
      "                          'range': [lat[0],lat[-1]],        # show the whole globe\n",
      "                          'showgrid': True,\n",
      "                          'zeroline': False,\n",
      "                          'ticks': None,\n",
      "                          'showticklabels': False\n",
      "                         },\n",
      "                'annotations': [{\n",
      "                                'text': 'Data courtesy of <a href=\"http://www.esrl.noaa.gov/psd/data/composites/day/\">NOAA Earth System Research Laboratory </a>',\n",
      "                                'xref': 'paper',\n",
      "                                'yref': 'paper',\n",
      "                                'x': 0,\n",
      "                                'y': -0.1,\n",
      "                                'align': 'left',\n",
      "                                'showarrow': False \n",
      "                                }]\n",
      "                                \n",
      "                }"
     ],
     "language": "python",
     "metadata": {},
     "outputs": [],
     "prompt_number": 6
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "By default, plot.ly linearly maps [`min(z)`, `max(z)`] to [0,1] to build its color map. <br>\n",
      "Instead, let's build a symmetric color map (and color bar) around zero, \n",
      "for a more elegant plot.\n",
      "\n",
      "So, first find the smaller possible symmetric range with integer bounds \n",
      "that includes all `z` values. <br>\n",
      "Then, using the default 'scl' map (coded below as `default_scl_map()`), \n",
      "map this range for our plot.ly call.\n",
      "\n",
      "To put emphasis on the temperature anomalies, \n",
      "let's use a variant of ColorBrewer2.0's divergent 'RdBu' color scheme <br>\n",
      "with 10 levels [ref. http://colorbrewer2.org ], with the 2 centermost levels in white."
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "# The default 'scl' map that plot.ly utilizes\n",
      "def default_scl_map(x):\n",
      "    \"\"\" x: some vector to be map \"\"\"\n",
      "    a = 1./(air.max() - air.min())     # the slope \n",
      "    return a*(x - air.min())           # the image \n",
      "\n",
      "N_scl = 10   # number of colors \n",
      "lwb_scl = np.min([np.floor(air.min()),-np.ceil(air.max())])  # lower bound\n",
      "upb_scl = np.max([-np.floor(air.min()),np.ceil(air.max())])  # upper bound\n",
      "i_scl = default_scl_map(np.linspace(lwb_scl,upb_scl,N_scl))  # scaled levels to be used\n",
      "#print lwb_scl, upb_scl\n",
      "\n",
      "# colorbrewer2.0 RdBu \n",
      "cmap = np.array([ [103, 0, 31], [178, 24, 43], [214, 96, 77], [244, 165, 130],\n",
      "                  [253, 219, 199], [209, 229, 240], [146, 197, 222], \n",
      "                  [67, 147, 195], [33, 102, 172], [5, 48, 9] ])\n",
      "\n",
      "# invert colormap so that negative temperature anomalies are in blue  \n",
      "cmap = cmap[::-1,:]\n",
      "\n",
      "# with white at 2 centermost positions\n",
      "cmap[4,:] = [255,255,255]\n",
      "cmap[5,:] = [255,255,255]\n",
      "\n",
      "# now we are ready to make a plot.ly dictionary for our temperature variable\n",
      "data_air = {\n",
      "             'x': lon,\n",
      "             'y': lat,\n",
      "             'z': air, \n",
      "             'type': 'heatmap',\n",
      "             'scl': # scaled level, rgb('cmap') , add ',' in-between 'cmap' elements\n",
      "                    [[i_scl[i], \"rgb(\"+','.join(map(str,cmap[i,:]))+\")\"] for i in range(N_scl)], \n",
      "            }\n",
      "# 'text' option does not work when using the 'heatmap' type."
     ],
     "language": "python",
     "metadata": {},
     "outputs": [],
     "prompt_number": 7
    },
    {
     "cell_type": "heading",
     "level": 2,
     "metadata": {},
     "source": [
      "5) Plot the results!"
     ]
    },
    {
     "cell_type": "code",
     "collapsed": false,
     "input": [
      "# Concatenate the coastline/country list (above) and temperature list (below)\n",
      "data = data_cc+[data_air]\n",
      "    \n",
      "py.ioff()         # turn off new tab pop-up in browser\n",
      "    \n",
      "# call plot.ly, save fig as 'polar_vortex'\n",
      "plot_out = py.plot(\n",
      "                   data,\n",
      "                   layout=layout_style,\n",
      "                   filename='polar_vortex',\n",
      "                   fileopt='overwrite'\n",
      "                  )\n",
      "    \n",
      "# show plot in notebook using 'plot_out'\n",
      "print('\\n This plot is available at '+plot_out['url'])\n",
      "show_plot(plot_out['url'])"
     ],
     "language": "python",
     "metadata": {},
     "outputs": [
      {
       "output_type": "stream",
       "stream": "stdout",
       "text": [
        "\n",
        " This plot is available at https://plot.ly/~etpinard/25\n"
       ]
      },
      {
       "html": [
        "<iframe height=\"550\" id=\"igraph\" scrolling=\"no\" seamless=\"seamless\" src=\"https://plot.ly/~etpinard/25/1000/500\" width=\"1050\"></iframe>"
       ],
       "output_type": "pyout",
       "prompt_number": 8,
       "text": [
        "<IPython.core.display.HTML at 0x3c0c2d0>"
       ]
      }
     ],
     "prompt_number": 8
    },
    {
     "cell_type": "markdown",
     "metadata": {},
     "source": [
      "**Conclusion:** Cold in Eastern North America, quite warm in Alaska, Greenland and most Europe.  "
     ]
    }
   ],
   "metadata": {}
  }
 ]
}