adapter.py

#

Helper classes and functions for adapters

import datetime
import numpy

from dateutil.relativedelta import relativedelta
from dateutil.rrule import YEARLY, MONTHLY, DAILY, HOURLY, MINUTELY, SECONDLY
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.utils import simplejson as json
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
from matplotlib.dates import AutoDateFormatter
from matplotlib.dates import AutoDateLocator
from matplotlib.dates import DateFormatter
from matplotlib.dates import RRuleLocator
from matplotlib.dates import date2num
from matplotlib.dates import num2date
from matplotlib.dates import rrulewrapper
from matplotlib.figure import Figure
from matplotlib.ticker import MaxNLocator
from matplotlib.ticker import ScalarFormatter
from lizard_map.matplotlib_settings import FONT_SIZE
from lizard_map.matplotlib_settings import SCREEN_DPI

LEGEND_WIDTH = 200
LEFT_LABEL_WIDTH = 100
BOTTOM_LINE_HEIGHT = FONT_SIZE * 1.5
#

Return size in inches for matplotlib's benefit

def _inches_from_pixels(pixels):
#

return pixels / SCREEN_DPI

def parse_identifier_json(identifier_json): Return dict of parsed identifier_json.

    Converts keys to str.
    TODO: .replace('%22', '"') in a better way
#

identifier_json = identifier_json.replace('%22', '"').replace('%20', ' ') if not identifier_json:

return {}
result = {}
for k, v in json.loads(identifier_json).items():
result[str(k)] = v
return result

def workspace_item_image_url(workspace_item_id, identifiers,

strip_layout=False, session_graph_options=False):

    Returns image url

    Identifiers is a list of dicts
#

identifiers_copy = identifiers[:] if strip_layout:

for identifier in identifiers_copy:
if 'layout' in identifier:
del identifier['layout']
identifier_json_list = [json.dumps(identifier).replace('"', '%22') for 
identifier in identifiers_copy]
if session_graph_options:
img_url = reverse(
"lizard_map.workspace_item_image_session_graph_options",
kwargs={'workspace_item_id': workspace_item_id, })
else:
img_url = reverse(
"lizard_map.workspace_item_image",
kwargs={'workspace_item_id': workspace_item_id, })
img_url = img_url + '?' + '&'.join(['identifier=%s' % i for i in
identifier_json_list])
return img_url

class LessTicksAutoDateLocator(AutoDateLocator): Similar to matplotlib.date.AutoDateLocator, but with less ticks.

#

numticks = 5 Difference to original AutoDateLocator: less ticks

        numticks = self.numticks
#

self._freq = YEARLY

        interval = 1
        bymonth = 1
        bymonthday = 1
        byhour = 0
        byminute = 0
        bysecond = 0
        if (numYears >= numticks):
            self._freq = YEARLY
            interval = int(numYears // numticks)
        elif (numMonths >= numticks):
            self._freq = MONTHLY
            bymonth = range(1, 13)
            interval = int(numMonths // numticks)
        elif (numDays >= numticks):
            self._freq = DAILY
            bymonth = None
            bymonthday = range(1, 32)
            interval = int(numDays // numticks)
        elif (numHours >= numticks):
            self._freq = HOURLY
            bymonth = None
            bymonthday = None
            byhour = range(0, 24)      # show every hour
            interval = int(numHours // numticks)
        elif (numMinutes >= numticks):
            self._freq = MINUTELY
            bymonth = None
            bymonthday = None
            byhour = None
            byminute = range(0, 60)
            interval = int(numMinutes // numticks)
#

end if

        elif (numSeconds >= numticks):
            self._freq = SECONDLY
            bymonth = None
            bymonthday = None
            byhour = None
            byminute = None
            bysecond = range(0, 60)
            interval = int(numSeconds // numticks)
#

end if

        else:
#

do what? microseconds as floats, but floats from what reference point?

            pass

        rrule = rrulewrapper(self._freq, interval=interval,
                             dtstart=dmin, until=dmax,
                             bymonth=bymonth, bymonthday=bymonthday,
                             byhour=byhour, byminute=byminute,
                             bysecond=bysecond)

        locator = RRuleLocator(rrule, self.tz)
        locator.set_axis(self.axis)

        locator.set_view_interval(*self.axis.get_view_interval())
        locator.set_data_interval(*self.axis.get_data_interval())
        return locator
#

Multiline version of AutoDateFormatter.

class MultilineAutoDateFormatter(AutoDateFormatter):
#

This class needs the axes to be able to initialize. When called, the ticks need to be known as well. For some scales, instead of showing a predetermined date label at any tick, the labels are chosen dependent of the tick position. Note that some labels are multiline, so make sure there is space for them in your figure."""

def init(self, locator, axes, tz=None):

self._locator = locator
self._formatter = DateFormatter("%b %d %Y %H:%M:%S %Z", tz)
self._tz = tz
self.axes = axes
self.tickinfo = None
def call(self, x, pos=0):

scale = float(self._locator._get_unit()) if not self.tickinfo:

self.tickinfo = self.Tickinfo(self.axes.get_xticks())
if (scale == 365.0): self._formatter = DateFormatter("%Y", self._tz) elif (scale == 30.0): if self.tickinfo.show_year(x):
self._formatter = DateFormatter("%bn%Y", self._tz)
else:
self._formatter = DateFormatter("%b", self._tz)
elif ((scale == 1.0) or (scale == 7.0)):
if self.tickinfo.show_month(x):
self._formatter = DateFormatter("%dn%b %Y", self._tz)
else:
self._formatter = DateFormatter("%d", self._tz)
elif (scale == (1.0 / 24.0)):
if x == self.tickinfo.max:

don't show

self._formatter = DateFormatter("%H", self._tz) elif self.tickinfo.show_day(x): self._formatter = DateFormatter("%Hn%d %b %Y", self._tz) else: self._formatter = DateFormatter("%H", self._tz) elif (scale == (1.0 / (24 * 60))): self._formatter = DateFormatter("%H:%M:%S %Z", self._tz) elif (scale == (1.0 / (24 * 3600))): self._formatter = DateFormatter("%H:%M:%S %Z", self._tz) else: self._formatter = DateFormatter("%b %d %Y %H:%M:%S %Z", self._tz)

return self._formatter(x, pos)

class Tickinfo(object): Class with tick information.

        The methods are used to determine what kind of label to put at a
        particular tick."""


#DIVIDER
        def show_day(self, tick):
#DIVIDER


#DIVIDER
            if (num2date(self.min).month == num2date(self.max).month):
                if tick == self.mid:
                    return True
                else:
                    return False


#DIVIDER
            else:
                middle_of_month = self.middle_of_month(tick)
                if (abs(tick - middle_of_month) < self.step / 2 or
                    (middle_of_month < self.min and tick == self.min) or
                    (middle_of_month > self.max) and tick == self.max):
                    return True
                else:
                    return False


#DIVIDER
        def show_year(self, tick):
#DIVIDER
            dt = num2date(tick)
            middle_of_day = datetime.datetime(dt.year, dt.month, dt.day, 12)
            return date2num(middle_of_day)


#DIVIDER
        def middle_of_month(self, tick):
#DIVIDER
            dt = num2date(tick)
            middle_of_year = datetime.datetime(dt.year, 7, 1)
            return date2num(middle_of_year)



#DIVIDER
class Graph(object):
#DIVIDER
        self.figure.set_facecolor('white')

#DIVIDER
        self.legend_width = 0.08

#DIVIDER
        self.left_label_width = LEFT_LABEL_WIDTH / self.width
        self.bottom_axis_location = BOTTOM_LINE_HEIGHT / self.height
        self.x_label_height = 0.08
        self.legend_on_bottom_height = 0.0
        self.axes = self.figure.add_subplot(111)
        self.axes.grid(True)


#DIVIDER
        self.fixup_axes()


#DIVIDER
        self.ax2 = None


#DIVIDER
        self.axes.axvline(self.today, color='orange', lw=1, ls='--')


#DIVIDER
    def set_ylim_margin(self, top=0.1, bottom=0.0):
#DIVIDER

        axes_to_change = self.axes
        if second:
            if self.ax2 is None:
                return
            else:
                axes_to_change = self.ax2


#DIVIDER
        if not self.restrict_to_month:
            major_locator = LessTicksAutoDateLocator()
            axes_to_change.xaxis.set_major_locator(major_locator)

            major_formatter = MultilineAutoDateFormatter(
                major_locator, axes_to_change)
            axes_to_change.xaxis.set_major_formatter(major_formatter)

        available_height = (self.height -
                            BOTTOM_LINE_HEIGHT -
                            self.x_label_height -
                            self.legend_on_bottom_height)
        approximate_lines = int(available_height / (FONT_SIZE * 1.5))
        max_number_of_ticks = approximate_lines
        if max_number_of_ticks < 2:
            max_number_of_ticks = 2
        locator = MaxNLocator(nbins=max_number_of_ticks - 1)
        if not second:
            axes_to_change.yaxis.set_major_locator(locator)
            axes_to_change.yaxis.set_major_formatter(
                ScalarFormatter(useOffset=False))


#DIVIDER
    def legend_space(self):
#DIVIDER
        Displays legend. Default is right side, but if the width is
        too small, it will display under the graph.

        handles is list of matplotlib objects (e.g. matplotlib.lines.Line2D)
        labels is list of strings
#DIVIDER
        self.ax2 = self.axes.twinx()
        self.fixup_axes(second=True)


#DIVIDER
    def http_png(self):

#

Return true or false to show day at this tick.

#

If there is only one day in the ticks, show it at the center

if (num2date(self.min).day == num2date(self.max).day):

if tick == self.mid:
return True
else:
return False

If there are more days in the ticks, show a label for that

tick that is closest to the center of their day.

else: middle_of_day = self.middle_of_day(tick) if (abs(tick - middle_of_day) < self.step / 2 or (middle_of_day < self.min and tick == self.min) or (middle_of_day > self.max) and tick == self.max): return True else: return False

def show_month(self, tick): Return true or false to show month at this tick.

#

If there is only one month in the ticks, show it at the center

#

If there are more months in the ticks, show a label for that tick that is closest to the center of their month.

#

Return true or false to show year at this tick.

#

If there is only one year in the ticks, show it at the center

if (num2date(self.min).year == num2date(self.max).year):

if tick == self.mid:
return True
else:
return False

If there are more years in the ticks, show a label for that

tick that is closest to the center of their year.

else: middle_of_year = self.middle_of_year(tick) if (abs(tick - middle_of_year) < self.step / 2 or (middle_of_year < self.min and tick == self.min) or (middle_of_year > self.max) and tick == self.max): return True else: return False

def middle_of_day(self, tick): Return the middle of the day as matplotlib number.

#

Return the middle of the month as matplotlib number.

#

dt = num2date(tick) middle_of_month = datetime.datetime(dt.year, dt.month, 16) return date2num(middle_of_month)

def middle_of_year(self, tick): Return the middle of the year as matplotlib number.

#

Class for matplotlib graphs, i.e. for popups, krw graphs

  • calculates correct size
  • horizontal axis = dates
  • vertical axis = user defined
  • outputs httpresponse for png
#

Figure color

#

Axes and legend location: full width is "1".

#

^^^ No legend by default, but we do allow a little space to the right of the graph to prevent the rightmost label from being cut off (at least, in a reasonable percentage of the cases).

#

Fixup_axes in init, so axes can be customised (for example set_ylim).

#

deze kan je zelf zetten

#

Show line for today.

#

Adjust y-margin of axes.

#

The standard margin is sometimes zero. This method sets the margin based on already present data in the visible part of the plot, so call it after plotting and before http_png().

Note that it is assumed here that the y-axis is not reversed.

From matplotlib 1.0 on there is a set_ymargin method like this already."""

lines = self.axes.lines arrays = [numpy.array(l.get_data()) for l in lines]

axhline and axvline give trouble - remove short lines from list

big_arrays = [a for a in arrays if a.size > 4] if len(big_arrays) > 0:

data = numpy.concatenate(big_arrays, axis=1)
if len(data[0]) > 0:

Datatimes from database may have timezone information.

In that case, start_date and end_date cannot be naive.

Assume all datetimes do have the same timezone, so we

can do the comparison.

start_date_tz = self.start_date.replace(tzinfo=data[0][0].tzinfo) end_date_tz = self.end_date.replace(tzinfo=data[0][0].tzinfo) index_in_daterange = ((data[0] < end_date_tz) & (data[0] > start_date_tz)) if index_in_daterange.any(): data_low = numpy.min(data[1, index_in_daterange]) data_high = numpy.max(data[1, index_in_daterange]) data_span = data_high - data_low view_low = data_low - data_span * bottom view_high = data_high + data_span * top self.axes.set_ylim(view_low, view_high) return None

def suptitle(self, title): self.figure.suptitle(title, x=self.left_label_width, horizontalalignment='left')

def set_xlabel(self, xlabel): self.axes.set_xlabel(xlabel) self.x_label_height = BOTTOM_LINE_HEIGHT / self.height

def fixup_axes(self, second=False): Fix up the axes by limiting the amount of items.

#
   available_width = self.width - LEFT_LABEL_WIDTH - LEGEND_WIDTH
   approximate_characters = int(available_width / (FONT_SIZE / 2))
   max_number_of_ticks = approximate_characters // 20
   if max_number_of_ticks < 2:
       max_number_of_ticks = 2
#

reserve space for legend (on the right side). even when

#

there is no legend displayed""" self.legend_width = LEGEND_WIDTH / self.width

def legend(self, handles=None, labels=None, ncol=1):

#

experimental update: do not reserve space for legend by

default, just place over graph. use legend_space to manually

add space

if handles is None and labels is None:

handles, labels = self.axes.get_legend_handles_labels()
if handles and labels:

Determine 'small' or 'large'

if self.width < 500: legend_loc = 4 # lower right

approximation of legend height

self.legend_on_bottom_height = min( (len(labels) / ncol + 2) * BOTTOM_LINE_HEIGHT / self.height, 0.5) else: legend_loc = 1 # Upper right'

return self.figure.legend( handles, labels, bbox_to_anchor=(1 - self.legend_width,
0,  # self.bottom_axis_location
self.legend_width,

1 = Upper right of above bbox. Use 0 for

'best'

1), loc=legend_loc, ncol=ncol, fancybox=True, shadow=True,)

legend.set_size('medium')

TODO: get rid of the border around the legend.

def init_second_axes(self): init second axes

#

Output plot to png. Also calculates size of plot and put 'now'