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 = Nonedef 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:return self._formatter(x, pos)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)
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 (num2date(self.min).day == num2date(self.max).day):
if tick == self.mid: return True else: return False
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 (num2date(self.min).year == num2date(self.max).year):
if tick == self.mid: return True else: return False
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
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]
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:def suptitle(self, title): self.figure.suptitle(title, x=self.left_label_width, horizontalalignment='left')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 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):
if handles is None and labels is None:
handles, labels = self.axes.get_legend_handles_labels() if handles and labels:return self.figure.legend( handles, labels, bbox_to_anchor=(1 - self.legend_width,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'
0, # self.bottom_axis_location self.legend_width,def init_second_axes(self): init second axes1 = 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.
Output plot to png. Also calculates size of plot and put 'now'