# @GPL_HEADER_START
# Use of the RAVE and related source code is subject to the terms
# of the following licenses:
# 
# GNU Public License (GPL) Rights pursuant to Version 2, June 1991
# Government Purpose License Rights (GPLR) pursuant to DFARS 252.225-7013
# 
# NO WARRANTY
# 
# ANY INFORMATION, MATERIALS, SERVICES, INTELLECTUAL PROPERTY OR OTHER
# PROPERTY OR RIGHTS GRANTED OR PROVIDED BY CARNEGIE MELLON UNIVERSITY
# PURSUANT TO THIS LICENSE (HEREINAFTER THE "DELIVERABLES") ARE ON AN
# "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
# KIND, EITHER EXPRESS OR IMPLIED AS TO ANY MATTER INCLUDING, BUT NOT
# LIMITED TO, WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE,
# MERCHANTABILITY, INFORMATIONAL CONTENT, NONINFRINGEMENT, OR ERROR-FREE
# OPERATION. CARNEGIE MELLON UNIVERSITY SHALL NOT BE LIABLE FOR INDIRECT,
# SPECIAL OR CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF PROFITS OR INABILITY
# TO USE SAID INTELLECTUAL PROPERTY, UNDER THIS LICENSE, REGARDLESS OF
# WHETHER SUCH PARTY WAS AWARE OF THE POSSIBILITY OF SUCH DAMAGES.
# LICENSEE AGREES THAT IT WILL NOT MAKE ANY WARRANTY ON BEHALF OF
# CARNEGIE MELLON UNIVERSITY, EXPRESS OR IMPLIED, TO ANY PERSON
# CONCERNING THE APPLICATION OF OR THE RESULTS TO BE OBTAINED WITH THE
# DELIVERABLES UNDER THIS LICENSE.
# 
# Licensee hereby agrees to defend, indemnify, and hold harmless Carnegie
# Mellon University, its trustees, officers, employees, and agents from
# all claims or demands made against them (and any related losses,
# expenses, or attorney's fees) arising out of, or relating to Licensee's
# and/or its sub licensees' negligent use or willful misuse of or
# negligent conduct or willful misconduct regarding the Software,
# facilities, or other rights or assistance granted by Carnegie Mellon
# University under this License, including, but not limited to, any
# claims of product liability, personal injury, death, damage to
# property, or violation of any laws or regulations.
# 
# Carnegie Mellon University Software Engineering Institute authored
# documents are sponsored by the U.S. Department of Defense under
# Contract F19628-00-C-0003. Carnegie Mellon University retains
# copyrights in all material produced under this contract. The U.S.
# Government retains a non-exclusive, royalty-free license to publish or
# reproduce these documents, or allow others to do so, for U.S.
# Government purposes only pursuant to the copyright license under the
# contract clause at 252.227.7013.
# 
# @GPL_HEADER_END
"""
Matplotlib visualizations
"""

__version__ = "$Rev$"

# vvv THIS IS FIRST SO IT CAN BE SURE TO PATCH THE font_manager FIRST

# HACK
# Make font_manager.FontProperties do something sane.

font_filename = None
font_small_size = 10
font_large_size = 20

import matplotlib.font_manager
OldFontProperties = matplotlib.font_manager.FontProperties
class NewFontProperties(OldFontProperties):
    def __init__(self, *args, **kwargs):
        if ('fname' not in kwargs) and (font_filename is not None):
            kwargs['fname'] = font_filename
        OldFontProperties.__init__(self, *args, **kwargs)
matplotlib.font_manager.FontProperties = NewFontProperties

def set_font_filename(filename):
    global font_filename
    font_filename = filename

# ^^^ THIS IS FIRST SO IT CAN BE SURE TO PATCH THE font_manager FIRST

import sys

from rave.plugins.decorators import *
from rave.plugins.dataset import *
from rave.plugins.shell import *
from rave.plugins.times import *

from datetime import datetime, timedelta
from calendar import timegm

from threading import Lock

from matplotlib.ticker import Formatter, LogLocator, MaxNLocator, \
                              ScalarFormatter, FuncFormatter
from matplotlib.figure import Figure
from matplotlib.axes import Axes
from matplotlib.dates import AutoDateLocator, date2num, num2date
from matplotlib.numerix import take, nonzero, absolute
from matplotlib.font_manager import FontProperties
from matplotlib.artist import setp
from matplotlib.text import Text
from matplotlib.lines import Line2D
from matplotlib.patches import Polygon, Rectangle

fig_canvas = None
try:
    from rave.plugins.mpl_kludge_cairo import FigureCanvasCairo
    fig_canvas = FigureCanvasCairo
except:
    try:
        from matplotlib.backends.backend_agg import FigureCanvasAgg
        fig_canvas = FigureCanvasAgg
    except:
        from matplotlib.backends.backend_cairo import FigureCanvasCairo
        fig_canvas = FigureCanvasCairo

import re, locale, time, math

renderer_lock = Lock()

strftime_dt_illegal_s = re.compile(r"((^|[^%])(%%)*%s)")

def write_fig(fig, out_file_name, bgcolor="#ffffff"):
    fig_canvas = make_canvas(fig)
    out_file = file(out_file_name, 'w')
    try:
        fig.savefig(out_file, dpi=dpi, facecolor=bgcolor)
    finally:
        out_file.close()



def findall(self, text, substr):
    # Also finds overlaps
    sites = []
    i = 0
    while 1:
        j = text.find(substr, i)
        if j == -1:
            break
        sites.append(j)
        i=j+1
    return sites

def combine_styles(a, b):
    if a == None or a == 'None':
        return b
    if b == None or b == 'None':
        return a
    c = dict(a)
    c.update(b)
    return c

# Works for wider range of dates than normal
def strftime_dt(dt, fmt):
    fmt = strftime_dt_illegal_s.sub(r"\1", fmt)
    fmt = fmt.replace("%s", "s")
    if dt.year > 1900:
            return unicode(time.strftime(fmt, dt.timetuple()), locale.getpreferredencoding())

    year = dt.year
    # For every non-leap year century, advance by
    # 6 years to get into the 28-year repeat cycle
    delta = 2000 - year
    off = 6*(delta // 100 + delta // 400)
    year = year + off

    # Move to around the year 2000
    year = year + ((2000 - year)//28)*28
    timetuple = dt.timetuple()
    s1 = time.strftime(fmt, (year,) + timetuple[1:])
    sites1 = findall(s1, str(year))

    s2 = time.strftime(fmt, (year+28,) + timetuple[1:])
    sites2 = findall(s2, str(year+28))

    sites = []
    for site in sites1:
        if site in sites2:
            sites.append(site)

    s = s1
    syear = "%4d" % (dt.year,)
    for site in sites:
        s = s[:site] + syear + s[site+4:]

    return unicode(s, locale.getpreferredencoding())

class RaveAutoDateFormatter(Formatter):
    def __init__(self, locator):
        self.locator = locator

    def __call__(self, x, pos=0):
        scale = float(self.locator._get_unit())
        (dmin, dmax) = self.locator.datalim_to_dt()
        dt = num2date(x)
        dmin_n = date2num(dmin)
        dmax_n = date2num(dmax)

        # Format for top line: shown for every tick
        fmt1 = "%H:%M"                  # 1:00

        # Format for bottom line: shown only when the value changes
        fmt2 = "%b %e, %Y"              # Jan 1, 1970

        if scale == 365.0:              # Tick on years
            fmt1 = "%Y"
            fmt2 = ""
        elif scale == 30.0:             # Tick on months
            fmt1 = "%B"
            fmt2 = "%Y"
        elif scale == 1.0 or scale == 7.0: # Weeks or days
            fmt1 = "%e"
            fmt2 = "%b %Y"
        elif scale == 1.0/24.0:         # Hours
            fmt1 = "%H:%M"
            fmt2 = "%b %e, %Y"

        # Lesser time scales handled by the defaults

        dmin_s = strftime_dt(dmin, fmt2)
        dmax_s = strftime_dt(dmax, fmt2)
        if dmin_s == dmax_s:
            # No change in display range:
            if x < dmin_n + scale or x > dmax_n - scale:
                # Display at start and end of range
                return strftime_dt(dt, fmt1 + "\n" + fmt2)
            else:
                return strftime_dt(dt, fmt1)
        else:
            # Change occurs within display range:
            dt_pre_s = strftime_dt(num2date(x - scale), fmt2)
            dt_s = strftime_dt(dt, fmt2)
            if dt_pre_s <> dt_s:
                # Display at points of change
                return strftime_dt(dt, fmt1 + "\n" + fmt2)
            else:
                return strftime_dt(dt, fmt1)

# Log Locator that does not display a tick at 10^0 (1)
class RaveLogLocator(LogLocator):
    def __call__(self):
        ticklocs = LogLocator.__call__(self)
        return take(ticklocs, nonzero(ticklocs<>1.0))

class RaveFormatter(ScalarFormatter):
    def __init__(self, y_units=''):
        ScalarFormatter.__init__(self)
        self.y_units = y_units
    def get_offset(self):
        return ''
    def pprint_val(self, x):
        if x == 0.0:
            return ""
        if self.orderOfMagnitude >= 12:
            return ("%0.1f T%s" % (x/10**12, self.y_units))
        elif self.orderOfMagnitude >= 9:
            return ("%0.1f G%s" % (x/10**9, self.y_units))
        elif self.orderOfMagnitude >= 6:
            return ("%0.1f M%s" % (x/10**6, self.y_units))
        elif self.orderOfMagnitude >= 3:
            return ("%0.1f k%s" % (x/10**3, self.y_units))
        elif self.orderOfMagnitude >= 0:
            if x >= 1:
                return ("%d %s" % (int(x), self.y_units))
            else:
                return ("%0.2f %s" % (x, self.y_units))
        else:
            return ("%0.2g %s" % (x, self.y_units))
    def _set_orderOfMagnitude(self,range):
        # if scientific notation is to be used, find the appropriate exponent
        # if using an numerical offset, find the exponent after applying the offset
        locs = absolute(self.locs)
        if self.offset: oom = math.floor(math.log10(range))
        else:
            if locs[0] > locs[-1]: val = locs[0]
            else: val = locs[-1]
            if val == 0: oom = 0
            else: oom = math.floor(math.log10(val))
        if oom <= -2:
            self.orderOfMagnitude = oom
        elif oom >= 3:
            self.orderOfMagnitude = oom
        else:
            self.orderOfMagnitude = 0

# For working with "zero" on log scales.
def zeros_to_ones(x):
    if x == 0.0:
        return 1.0
    return x

# For abbreviating raw counts
# TODO: this probably belongs somewhere else....
def format_count(count):
    kb = 1024.0
    mb = 1024.0 * kb
    gb = 1024.0 * mb
    tb = 1024.0 * gb

    if count >= tb:
        return "%.2fT" % (count/tb)
    if count >= gb:
        return "%.2fG" % (count/gb)
    if count >= mb:
        return "%.2fM" % (count/mb)
    if count >= kb:
        return "%.2fk" % (count/kb)
    return "%.2fb" % count

    

dpi = 75.0

def make_figure(width, height, **kwargs):
    in_w = width / dpi
    in_h = height / dpi
    return Figure(figsize=(in_w, in_h), dpi=dpi, **kwargs)

def make_axes(fig,
              margin_top=32, margin_left=48,
              margin_bottom=32, margin_right=48, **kwargs):
    (in_w, in_h) = fig.get_size_inches()
    (w, h) = (int(in_w * dpi), int(in_h * dpi))
    mx = 1.0 / w
    my = 1.0 / h
    width = (w - margin_left - margin_right)
    height = (h - margin_top - margin_bottom)
    as = ((margin_left + 0.5) * mx,
          (margin_bottom + 0.5) * my,
          width * mx, height * my)
    ax = fig.add_axes(as, **kwargs)
    if ax.get_frame_on():
        ax.axesPatch = Rectangle((0.0, -1.0/height), 1.0, 1.0 + 1.0/height,
                                 facecolor='#f8f8f8', edgecolor='#cccccc',
                                 transform=ax.transAxes)
        ax.axesPatch.set_figure(ax.figure)
        ax.axesFrame = Rectangle((-1.0, -1.0), 0.5, 0.5)
    return ax

def make_text(fig, x, y, s, *args, **kwargs):
    (in_w, in_h) = fig.get_size_inches()
    (w, h) = (int(in_w * dpi), int(in_h * dpi))
    mx = 1.0 / w
    my = 1.0 / h
    rel_x = x * mx
    rel_y = y * my
    return fig.text(rel_x, rel_y, s, *args, **kwargs)

def make_canvas(fig):
    return fig_canvas(fig)

def make_legend_axes(fig, margin_left=48, margin_bottom=0,
                     margin_right=48, height=16):
    (in_w, in_h) = fig.get_size_inches()
    (w, h) = (int(in_w * dpi), int(in_h * dpi))
    axes = make_axes(fig, h - margin_bottom - height - 1,
                     margin_left, margin_bottom, margin_right)
    axes.get_xaxis().set_visible(False)
    axes.get_yaxis().set_visible(False)
    axes.set_frame_on(False)
    return axes

def draw_legend(fig, bin_size, window_size):
    bin_size = bin_size.seconds
    window_size = window_size.seconds
    if (bin_size % 86400) == 0:
        bin_size_text = '%d day' % (bin_size / 86400)
    elif (bin_size % 3600) == 0:
        bin_size_text = '%d hour' % (bin_size / 3600)
    elif (bin_size % 60) == 0:
        bin_size_text = '%d min' % (bin_size / 60)
    else:
        bin_size_text = '%d sec' % bin_size
    if (window_size % 86400) == 0:
        window_size_text = '%d day' % (window_size / 86400)
    elif (window_size % 3600) == 0:
        window_size_text = '%d hour' % (window_size / 3600)
    elif (window_size % 60) == 0:
        window_size_text = '%d min' % (window_size / 60)
    else:
        window_size_text = '%d sec' % window_size
    leg_axes = make_legend_axes(fig)
    leg_axes.text(5.25, 0.5, 'All times are UTC',
                  horizontalalignment='right', verticalalignment='center',
                  size=font_small_size)
    leg_axes.plot([0.0], [0.65], marker='.', linestyle='None', color='k',
                  markeredgecolor='k', markerfacecolor='k', markersize=4.0)
    leg_axes.text(0.1, 0.5, bin_size_text + ' data point',
                  horizontalalignment='left', verticalalignment='center',
                  size=font_small_size)
    leg_axes.plot([1.05], [0.65], marker='|', linestyle='None', color='r',
                  markeredgecolor='r', markerfacecolor='r',
                  markeredgewidth=2.0, markersize=6.0)
    leg_axes.text(1.15, 0.5, 'data > 95%-ile',
                  horizontalalignment='left', verticalalignment='center',
                  size=font_small_size)
    leg_axes.plot([1.925, 2.0], [0.65, 0.65], marker='None', linestyle='-',
                  color='b')
    leg_axes.text(2.05, 0.5, window_size_text + ' moving avg',
                  horizontalalignment='left', verticalalignment='center',
                  size=font_small_size)
    leg_axes.plot([3.0, 3.075], [0.65, 0.65], marker='None', linestyle=':',
                  color='b')
    leg_axes.text(3.125, 0.5, window_size_text + ' moving variance',
                  horizontalalignment='left', verticalalignment='center',
                  size=font_small_size)
    leg_axes.set_xlim(0, 5)
    leg_axes.set_ylim(0, 1)    

def make_vertical_stripchart(fig, n,
                             margin_top=32, margin_left=48,
                             margin_bottom=32, margin_right=48,
                             row_spacing=8,
                             label_left_space=16, label_bottom_space=4,
                             use_common_y=False, title=None, ylabel=None,
                             xlabel=None, show_x_ticks=True):
    (in_w, in_h) = fig.get_size_inches()
    (w, h) = (int(in_w * dpi), int(in_h * dpi))
    box_height = h - margin_top - margin_bottom - row_spacing * (n-1)
    box_height = box_height / n
    rem_height = h - margin_top - margin_bottom - row_spacing * (n-1) - \
                 box_height * n
    axes_list = []
    old_axes = None
    for i in xrange(n):
        neg_i = n - i - 1
        if use_common_y:
            sharey=old_axes
        else:
            sharey=None
        axes = make_axes(fig,
                         margin_top + i * (box_height + row_spacing),
                         margin_left, margin_bottom + rem_height
                         + neg_i * (box_height + row_spacing),
                         margin_right, sharex=old_axes, sharey=sharey)
        axes.grid(alpha=0.2, color='k', linestyle='-')
        # Y: No tick marks, label left side only
        for t in axes.yaxis.get_major_ticks():
            t.tick1On = False
            t.tick2On = False
            t.label1On = True
            t.label2On = False
        # X: No tick marks (grid instead), no labels
        for t in axes.xaxis.get_major_ticks():
            t.tick1On = False
            t.tick2On = False
            t.label1On = False
            t.label2On = False
        axes_list.append(axes)
        old_axes = axes
    kwargs = {}
    if xlabel <> None: kwargs['xlabel'] = xlabel
    if title <> None: kwargs['title'] = title
    if not show_x_ticks:
        old_axes = None
    title_axes = make_axes(fig, margin_top, margin_left,
                           margin_bottom + rem_height - label_bottom_space,
                           margin_right,
                           frameon=False, sharex=old_axes, **kwargs)
    if not show_x_ticks:
        title_axes.xaxis.set_ticks([])
    title_axes.yaxis.set_ticks([])
    # No tick marks
    for t in title_axes.xaxis.get_major_ticks():
        t.tick1On = False
        t.tick2On = False
    kwargs = {}
    if ylabel <> None: kwargs['ylabel'] = ylabel
    ylabel_axes = make_axes(fig, margin_top, margin_left - label_left_space,
                            margin_bottom + rem_height,
                            margin_right, frameon=False,
                            **kwargs)
    ylabel_axes.xaxis.set_ticks([])
    ylabel_axes.yaxis.set_ticks([])
    fig._rem_height = rem_height
    return axes_list

def make_horizontal_stripchart(fig, n,
                               margin_top=48, margin_left=48,
                               margin_bottom=32, margin_right=48,
                               column_spacing=8,
                               label_left_space=16, label_bottom_space=0,
                               use_common_x=False, title=None, ylabel=None,
                               xlabel=None, show_y_ticks=True):
    (in_w, in_h) = fig.get_size_inches()
    (w, h) = (int(in_w * dpi), int(in_h * dpi))
    box_width = w - margin_left - margin_right
    box_width = box_width / n
    rem_width = w - margin_left - margin_right - box_width * n
    axes_list = []
    old_axes = None
    for i in xrange(n):
        neg_i = n - i - 1
        if use_common_x:
            sharex=old_axes
        else:
            sharex=None
        axes = make_axes(fig,
                         margin_top, margin_left + i * box_width,
                         margin_bottom,
                         margin_right + rem_width + column_spacing
                         + neg_i * box_width,
                         sharey=old_axes, sharex=sharex)
        axes.grid(alpha=0.2, color='k', linestyle='-')
        # X: No tick marks, label bottom side only
        for t in axes.xaxis.get_major_ticks():
            t.tick1On = False
            t.tick2On = False
            t.label1On = True
            t.label2On = False
        # Y: No tick marks (grid instead), no labels
        for t in axes.yaxis.get_major_ticks():
            t.tick1On = False
            t.tick2On = False
            t.label1On = False
            t.label2On = False
        axes_list.append(axes)
        old_axes = axes
    kwargs = {}
    if xlabel <> None: kwargs['ylabel'] = ylabel
    if title <> None: kwargs['title'] = title
    if not show_y_ticks:
        old_axes = None
    title_axes = make_axes(fig, margin_top, margin_left,
                           margin_bottom - label_bottom_space,
                           margin_right + rem_width,
                           frameon=False, sharey=old_axes, **kwargs)
    if not show_y_ticks:
        title_axes.yaxis.set_ticks([])
    title_axes.xaxis.set_ticks([])
    # No tick marks
    for t in title_axes.yaxis.get_major_ticks():
        t.tick1On = False
        t.tick2On = False
    kwargs = {}
    if xlabel <> None: kwargs['xlabel'] = xlabel
    xlabel_axes = make_axes(fig, margin_top, margin_left - label_left_space,
                            margin_bottom,
                            margin_right + rem_width, frameon=False,
                            **kwargs)
    xlabel_axes.xaxis.set_ticks([])
    xlabel_axes.yaxis.set_ticks([])
    return axes_list

def plot_single_timeseries(axes,
                           t, y,
                           window_duration=None,
                           max_proportion=0.95,
                           min_proportion=None,
                           data_line_style={ 'marker': '.',
                                             'linestyle': 'None',
                                             'color': 'k',
                                             'markeredgecolor': 'k',
                                             'markerfacecolor': 'k',
                                             'markersize': 4.0 },
                           out_of_range_line_style={ 'marker': '|',
                                                     'linestyle': 'None',
                                                     'color': 'r',
                                                     'markerfacecolor': 'r',
                                                     'markeredgecolor': 'r',
                                                     'markeredgewidth': 2.0,
                                                     'markersize': 6.0 },
                           out_of_range_max_line_style='None',
                           out_of_range_min_line_style='None',
                           average_line_style={ 'marker': 'None',
                                                'linestyle': '-',
                                                'color': '#0066cc' },
                           deviation_line_style={ 'linestyle': ':' },
                           deviation_max_line_style='None',
                           deviation_min_line_style='None'):

    """
    Plot a single time-series plot on an axis, including optionally
    generating a moving average and distribution lines, and scaling to
    include only some of the data.

    Required arguments:
      t - a sequence of datetime objects representing the times
      y - a sequence of numbers representing the values

    Optional arguments:

      window_duration - A timedelta object representing the time span
          shown in a moving average.  If window_duration is None (the
          default), then no average is generated.

      max_proportion - Instead of choosing a maximum value for the y
          range that includes all elements, choose a value that
          includes this proportion of elements.  (For example, if
          max_proportion is the default value of 0.95, a maximum y
          value is chosen that includes at least 95% of the data
          points.)  This is used to prevent spiky data from blowing
          out the range on the majority of data points.

      min_proportion - Instead of choosing a minimum value for the y
          range that includes all elements, choose a value that
          includes this proportion of elements.  See max_proportion
          above.  The default for min_proportion is None, which means
          that 0.0 should be the minimum y value.

      data_line_style - A dictionary of properties to be set on the
         datapoint line.  See the definition of the
         matplotlib.lines.Line2D class __init__ function.  This line
         represents the raw data points.  If data_line_style is set to
         None, this line will not be displayed.

      out_of_range_line_style - Properties for the out-of-range line.
         This line represents all data values that are not in the y
         range of the display.  For example, if the data series
         includes a point that is above the max y of the plot, then
         this line will include a point with that x value and a y
         value of the max y of the plot.  Likewise, if a value is
         below the min y of the plot, this line will include a point
         with that x value and y value equal to the min y of the plot.

      average_line_style - Properties for the moving average line.

      deviation_line_style - Properties for the +1sigma and -1sigma
         moving average lines.  (See also deviation_max_line_style and
         deviation_min_line_style.)  This line style inherits any
         unset attributes from average_line_style.

      deviation_max_line_style - Properties for the +1sigma line.  If
         both this and deviation_line_style are given, this version is
         used for any property that occurs in both.

      deviation_min_line_style - Properties for the -1sigma line.  If
         both this and deviation_line_style are given, this version is
         used for any property that occurs in both.
    """

    out_of_range_max_line_style = combine_styles(out_of_range_line_style,
                                                 out_of_range_max_line_style)
    out_of_range_min_line_style = combine_styles(out_of_range_line_style,
                                                 out_of_range_min_line_style)
    deviation_line_style = combine_styles(average_line_style,
                                          deviation_line_style)
    deviation_max_line_style = combine_styles(deviation_line_style,
                                              deviation_max_line_style)
    deviation_min_line_style = combine_styles(deviation_line_style,
                                              deviation_max_line_style)
    
    
    # Find max and min caps.
    if not min_proportion:
        # Throw out zeros if we've pegged ymin already
        sorted_y = sorted([d for d in y if d <> 0])
    else:
        sorted_y = sorted(y)

    range_ymin = 0.0
    if len(sorted_y) > 0:
        range_ymax = sorted_y[-1]
    else:
        range_ymax = 1
    if min_proportion:
        range_ymin = sorted_y[-int(len(sorted_y)*min_proportion)]
    if max_proportion:
        if len(sorted_y) > 0:
            range_ymax = sorted_y[int(len(sorted_y)*max_proportion) - 1]
        else:
            range_ymax = 1
    del sorted_y

    # Sanitize the limits
    if range_ymax < range_ymin:
        (range_ymin, range_ymax) = (range_ymax, range_ymin)
    if range_ymin == range_ymax:
        range_ymin -= 1
        range_ymax += 1
    exponent, remainder = divmod(math.log10(range_ymax-range_ymin), 1)
    if remainder < 0.5:
        exponent -= 1
    scale = 10**(-exponent)
    range_ymin = math.floor(scale*range_ymin)/scale
    range_ymax = math.ceil(scale*range_ymax)/scale

    if range_ymin == range_ymax:
        if range_ymax <> 0:
            range_ymax = range_ymax * 2
        else:
            range_ymax = 1

    if window_duration:
        (avg_t, avg_y, avg_y_dev) = moving_average(window_duration, t, y)
        max_y = [(avg_y[i] + avg_y_dev[i]) for i in xrange(len(avg_y))]
        min_y = [(avg_y[i] - avg_y_dev[i]) for i in xrange(len(avg_y))]
        avg_t = map(date2num, avg_t)

    t = map(date2num, t)

    over_i = [i for i in xrange(len(y)) if y[i] > range_ymax]
    over_t = [t[i] for i in over_i]
    over_y = [range_ymax for i in over_i]

    under_i = [i for i in xrange(len(y)) if y[i] < range_ymin]
    under_t = [t[i] for i in under_i]
    under_y = [range_ymin for i in under_i]

    # Peg to limits
    axes.set_ylim(range_ymin, range_ymax)

    if data_line_style <> None and data_line_style <> 'None':
        try:
            line = axes.plot(t, y, **data_line_style)
        except ZeroDivisionError:
            pass

    if window_duration:
        if average_line_style not in (None, 'None'):
            try:
                avg_line = axes.plot(avg_t, avg_y, **average_line_style)
            except ZeroDivisionError:
                pass

        try:
            avg_t_r = list(avg_t)
            avg_t_r.reverse()
            min_y_r = list(min_y)
            min_y_r.reverse()
            min_max_poly = axes.fill(avg_t + avg_t_r, max_y + min_y_r,
                                     facecolor='#dfe9f3', ec='#dfe9f3')
        except ZeroDivisionError:
            pass

        if deviation_max_line_style not in (None, 'None'):
            try:
                max_line = axes.plot(avg_t, max_y, **deviation_max_line_style)
            except ZeroDivisionError:
                pass

        if deviation_min_line_style not in (None, 'None'):
            try:
                min_line = axes.plot(avg_t, min_y, **deviation_min_line_style)
            except ZeroDivisionError:
                pass

    if out_of_range_max_line_style not in (None, 'None'):
        try:
            oor_max_line = axes.plot(over_t, over_y, '|',
                                     **out_of_range_max_line_style)
        except ZeroDivisionError:
            pass
        
    if out_of_range_min_line_style not in (None, 'None'):
        try:
            oor_min_line = axes.plot(under_t, under_y,
                                     **out_of_range_min_line_style)
        except ZeroDivisionError:
            pass

    axes.set_ylim(range_ymin, range_ymax)


def render_timeseries_multi(out_file_name, label_data, time_data, value_data,
                            width, height, sdate, edate, by,
                            log=None, ylim=None, ylab='', title='',
                            bgcolor='#ffffff'):
  if len(label_data) == 0:
      render_blank(out_file_name, width, height, bgcolor)
      return
  renderer_lock.acquire()
  try:
    yscale = 'linear'
    if 'y' in log:
        yscale = 'log'

    if yscale == 'log':
        val_default = 1.0
        value_data = (zeros_to_ones(float(x)) for x in value_data)
    else:
        val_default = 0.0

    sdate = datetime_obj(bin_datetime(by, sdate, ceil=True))
    edate = datetime_obj(bin_datetime(by, edate))
    by = timedelta(seconds=by)
    d = Dataset(label=label_data,
                time=(datetime_obj(x) for x in time_data),
                value=value_data)

    times = []
    t = sdate
    while t <= edate:
        times.append(t)
        t = t + by
    d = d.cross_tab(['time'], 'label', 'value',
                    key_list=((x,) for x in times),
                    value_default=val_default)
    d = d.sort_col('time')
    labels = [c for c in d.columns if c <> 'time']
    label_sums = {}
    for l in labels:
        sum = 0
        for x in d[l]:
            sum = sum + x
        label_sums[l] = sum
    labels.sort(key=(lambda x: label_sums[x]), reverse=True)

    # Data munging complete

    fig = make_figure(width, height)
    axes = make_axes(fig, 32, 48, 32, 128, title=title, ylabel=ylab,
                     yscale=yscale)
    axes.grid(alpha=0.2, color='k', linestyle='-')
    x_locator = AutoDateLocator()
    x_formatter = RaveAutoDateFormatter(x_locator)
    if yscale == 'log':
        y_locator = RaveLogLocator()
    else:
        y_locator = MaxNLocator(nbins=4)
    axes.yaxis.set_major_locator(y_locator)
    axes.xaxis.set_major_locator(x_locator)
    axes.xaxis.set_major_formatter(x_formatter)
    
    chart_lines = []

    line_types = ['b-', 'g-', 'r-', 'c-', 'm-', 'y-', 'k-']

    x = date2num(map(datetime_obj, d['time']))
    sym = 0
    for l in labels:
        y = d[l]
        try:
            chart_line = axes.plot(x, y, line_types[sym], label=l)
            chart_lines = chart_lines + chart_line
            sym = (sym + 1) % len(line_types)
        except ZeroDivisionError:
            pass

    axes.set_xlim(xmin=date2num(sdate), xmax=date2num(edate))
    if yscale == 'log':
        axes.set_ylim(ymin=1.0)
    else:
        axes.set_ylim(ymin=0.0)

    p = FontProperties(size=font_small_size)

    leg = fig.legend(chart_lines, map(lambda x: x.get_label(), chart_lines),
                     'center right', prop=p, handlelen=0.015, markerscale=1.0)
    # setp(leg.get_frame(), facecolor='k', alpha=0.1)

    write_fig(fig, out_file_name, bgcolor=bgcolor)
  finally:
    renderer_lock.release()

def render_timeseries_ind(out_file_name, label_data, time_data, value_data,
                          width, height, sdate, edate, by, secondary=None,
                          ylab='', title='', average_by=300, labels=None,
                          min_proportion=None, max_proportion=0.95,
                          bgcolor='#ffffff', legend=True, y_units=''):
  if len(label_data) == 0:
    render_blank(out_file_name, width, height, bgcolor)
    return
  renderer_lock.acquire()
  try:
    sdate = datetime_obj(bin_datetime(by, sdate, ceil=True))
    edate = datetime_obj(bin_datetime(by, edate))
    by = timedelta(seconds=by)
    average_by = timedelta(seconds=average_by)

    times = []
    t = sdate
    while t <= edate:
        times.append(t)
        t = t + by

    d = Dataset(label=label_data, time=time_data, value=value_data)

    if not labels:
        labels = list(set(label_data))
        def sort_key(l):
            if isinstance(l, (str, unicode)):
                l = l.lower()
                if l == 'other' or l == 'unknown': return ()
            return l
        labels.sort(key=sort_key)

    fig = make_figure(width, height)

    if legend:
        axes_list = make_vertical_stripchart(fig, len(labels),
                                             title=title, ylabel=ylab,
                                             margin_right=64,
                                             margin_bottom=48,
                                             margin_left=96)
        draw_legend(fig, by, average_by)
    else:
        axes_list = make_vertical_stripchart(fig, len(labels),
                                             title=title, ylabel=ylab,
                                             margin_right=64,
                                             margin_left=96)

    axes_scales = []

    for i in xrange(len(labels)):
        label = labels[i]
        axes = axes_list[i]

        # Format Y using metric prefixes
        y_formatter = RaveFormatter(y_units=y_units)
        axes.yaxis.set_major_formatter(y_formatter)

        x_locator = AutoDateLocator()
        axes.xaxis.set_major_locator(x_locator)

        x_formatter = RaveAutoDateFormatter(x_locator)
        axes.xaxis.set_major_formatter(x_formatter)

        l_indices = [i for i in xrange(len(d)) if d['label'][i] == label]
        l_times = [d['time'][i] for i in l_indices]
        l_values = [d['value'][i] for i in l_indices]

        y = [0.0] * len(times)
        seq_set(y, seq_find(times, l_times), l_values)

        plot_single_timeseries(axes, times, y, average_by,
                               min_proportion=min_proportion,
                               max_proportion=max_proportion)
        axes.set_xlim(xmin=date2num(sdate), xmax=date2num(edate))

        (ymin, ymax) = axes.get_ylim()
        axes.yaxis.set_ticks([ymax])
        axes_scales.append(ymax)

        axes.text(1.005, 1.0, label, horizontalalignment='left',
                  verticalalignment='top', transform=axes.transAxes,
                  rotation=0.0, size=font_small_size)

        axes.set_ylim(ymin=0.0)
        setp(axes.get_yticklabels(), rotation=0.0, verticalalignment='top',
             size=font_small_size)

    if legend:
        legend_height = 16
    else:
        legend_height = 0

    bar_pixels = height - 64 - fig._rem_height - legend_height
    bar_pixels_remaining = bar_pixels

    scale_sum = 0.0
    part_height = [None] * len(axes_scales)
    part_bottom = [None] * len(axes_scales)
    #
    for i in xrange(len(axes_scales)):
        scale_sum = scale_sum + axes_scales[i]
    changed = True
    while changed:
        changed = False
        for i in xrange(len(axes_scales)):
            if part_height[i] == None and \
                   axes_scales[i]/scale_sum * bar_pixels_remaining < 2:
                scale_sum = scale_sum - axes_scales[i]
                bar_pixels_remaining = bar_pixels_remaining - 2
                part_height[i] = 2
                changed = True
    current_top = int(bar_pixels)
    for i in xrange(len(axes_scales)):
        if part_height[i] == None:
            part_height[i] = int(axes_scales[i]/scale_sum *
                                 bar_pixels_remaining)
        current_top = current_top - part_height[i]
        part_bottom[i] = int(current_top)

    scale_axes = make_axes(fig, margin_right=width-96, margin_left=0,
                           margin_bottom=32+fig._rem_height+legend_height,
                           margin_top=31,
                           frame_on=False,
                           xlim=(0,64), ylim=(0,bar_pixels+1))
    scale_axes.set_axis_off()
    for i in xrange(len(axes_scales)):
        r = Rectangle((0.5, part_bottom[i]),
                      15, part_height[i], edgecolor='#cccccc',
                      facecolor="#f8f8f8", lw=1)
        l = Line2D([16,36], [part_bottom[i]+part_height[i]/2,
                             bar_pixels*(1.0-float(i+0.5)/len(axes_scales))],
                   color="k", alpha=0.2, lw=1)
        l2 = Line2D([36,96], [bar_pixels*(1.0-float(i+0.5)/len(axes_scales)),
                              bar_pixels*(1.0-float(i+0.5)/len(axes_scales))],
                   color="k", alpha=0.2, lw=1)
        scale_axes.add_patch(r)
        scale_axes.add_line(l)
        scale_axes.add_line(l2)

    write_fig(fig, out_file_name, bgcolor=bgcolor)
  finally:
    renderer_lock.release()


def render_timeseries(out_file_name, time_data, value_data,
                      width, height, sdate, edate, by, ylab='', title='',
                      min_proportion=None, max_proportion=0.95,
                      ymin=None, ymax=None, y_units='',
                      average_by=300, bgcolor='#ffffff', legend=True):
  if len(time_data) == 0:
    render_blank(out_file_name, width, height, bgcolor)
    return
  renderer_lock.acquire()
  try:
    sdate = datetime_obj(bin_datetime(by, sdate, ceil=True))
    edate = datetime_obj(bin_datetime(by, edate))
    by = timedelta(seconds=by)
    average_by = timedelta(seconds=average_by)

    times = []
    t = sdate
    while t <= edate:
        times.append(t)
        t = t + by
    d = Dataset(time=time_data, value=value_data)

    fig = make_figure(width, height)

    if legend:
        axes = make_axes(fig, title=title, ylabel=ylab, margin_bottom=48)
        draw_legend(fig, by, average_by)
    else:
        axes = make_axes(fig, title=title, ylabel=ylab)

    axes.grid(alpha=0.2, color='k', linestyle='-')

    y_locator = MaxNLocator(nbins=4)
    axes.yaxis.set_major_locator(y_locator)
    
    y_formatter = RaveFormatter(y_units=y_units)
    axes.yaxis.set_major_formatter(y_formatter)

    x_locator = AutoDateLocator()
    axes.xaxis.set_major_locator(x_locator)

    x_formatter = RaveAutoDateFormatter(x_locator)
    axes.xaxis.set_major_formatter(x_formatter)

    y = [0.0] * len(times)
    seq_set(y, seq_find(times, time_data), value_data)

    plot_single_timeseries(axes, times, y, average_by,
                           min_proportion=min_proportion,
                           max_proportion=max_proportion)
    axes.set_xlim(xmin=date2num(sdate), xmax=date2num(edate))

    axes.set_ylim(ymin=0.0)
    setp(axes.get_yticklabels(), rotation=90.0, verticalalignment='top',
         size=font_small_size)

    write_fig(fig, out_file_name, bgcolor=bgcolor)
  finally:
    renderer_lock.release()


def render_timeseries_sparkline(out_file_name, time_data, value_data,
                      width, height, sdate, edate, by,
                      min_proportion=None, max_proportion=None,
                      ymin=None, ymax=None, 
                      bgcolor='#ffffff'):
  if len(time_data) == 0:
      render_blank(out_file_name, width, height, bgcolor)
      return
  renderer_lock.acquire()
  try:
    sdate = datetime_obj(bin_datetime(by, sdate, ceil=True))
    edate = datetime_obj(bin_datetime(by, edate))
    by = timedelta(seconds=by)

    times = []
    t = sdate
    while t <= edate:
        times.append(t)
        t = t + by
    d = Dataset(time=time_data, value=value_data)

    fig = make_figure(width, height)

    axes = make_axes(fig,
                     margin_left=0, margin_right=0,
                     margin_top=0, margin_bottom=0)
    axes.set_frame_on(False)
    axes.set_axis_off()


    if len(times):
        y = [0.0] * len(times)
        seq_set(y, seq_find(times, time_data), value_data)

        plot_single_timeseries(axes, times, y,
                               min_proportion=min_proportion,
                               max_proportion=max_proportion,
                               window_duration=timedelta(300),
                               average_line_style='None',
                               data_line_style={ 'marker': 'None', 'linestyle':'-',
                                                 'linewidth': 1.0,
                                                 'color'    : 'k' },
                               out_of_range_line_style='None',
                               deviation_line_style='None')
        axes.set_xlim(xmin=date2num(sdate), xmax=date2num(edate))
        axes.set_ylim(ymin=0.0)

    write_fig(fig, out_file_name, bgcolor=bgcolor)
  finally:
    renderer_lock.release()



def render_histogram(out_file_name, cats, values,
                     width, height, as_sparkline=False, 
                     title='', ylab='',
                     barwidth=None, orientation='vertical',
                     bottom=None, align='edge', log=True,
                     barlabels=None, bgcolor='#ffffff', **kwargs):
    if len(cats) == 0:
        render_blank(out_file_name, width, height, bgcolor)
        return
    renderer_lock.acquire()
    try:
        fig = make_figure(width, height)
        if as_sparkline:
            axes = make_axes(fig, title=title, ylabel=ylab,
                             margin_top=0, margin_left=0,
                             margin_bottom=0, margin_right=0)
            axes.set_frame_on(False)
            axes.set_axis_off()
        else:
            axes = make_axes(fig, title=title, ylabel=ylab)

        if len(cats) != len(values):
            render_message(out_file_name, "Bad data", width, height)

        if len(cats) and len(values):
            y_formatter = RaveFormatter()
            axes.yaxis.set_major_formatter(y_formatter)

            if barwidth is None:
                barwidth = 0.9*(cats[1]-cats[0])
            if orientation == 'horizontal':
                patches = axes.barh(cats, values, height=barwidth, left=bottom,
                                    align=align, log=log)
            elif orientation == 'vertical':
                patches = axes.bar(cats, values, width=barwidth, bottom=bottom,
                                    align=align, log=log)
            else:
                raise ValueError, 'invalid orientation: %s' % orientation
            for p in xrange(len(patches)):
                patches[p].update(kwargs)
            if log:
                axes.set_ylim(ymin=1.0)
            else:
                axes.set_ylim(ymin=0.0)
      
        write_fig(fig, out_file_name, bgcolor=bgcolor)
    finally:
        renderer_lock.release()


def render_boxplot(out_file_name, data, labels,
                   width, height, bgcolor="#ffffff", vert=1):
    renderer_lock.acquire()
    try:
        fig = make_figure(width, height)
        a = make_axes(fig)
        maxval = max([max(x) for x in data])
        def format_label(x, pos):
            return labels[pos]
        def format_timedelta(y, pos):
            neg = ""
            if y < 0:
                neg = "-"
                y = -y
            result = str(timedelta(seconds=y)).lstrip('0:')
            if '.' in result: result = result.rstrip('0')
            if result.startswith('.'):
                result = '0' + result
            return neg + result
        x_formatter = FuncFormatter(format_label)
        y_formatter = FuncFormatter(format_timedelta)
        if vert == 1:
            a.xaxis.set_major_formatter(x_formatter)
            a.yaxis.set_major_formatter(y_formatter)
        else:
            a.yaxis.set_major_formatter(x_formatter)
            a.xaxis.set_major_formatter(y_formatter)
        a.boxplot(data, vert=vert, sym='')
        a.set_frame_on(False)
        for t in a.xaxis.get_major_ticks():
            t.tick1On = False
            t.tick2On = False
        for t in a.yaxis.get_major_ticks():
            t.tick1On = False
            t.tick2On = False
        write_fig(fig, out_file_name)
    finally:
        renderer_lock.release()






def render_message(out_file_name, message, width, height, bgcolor='#ffffff'):
  renderer_lock.acquire()
  try:
    fig = make_figure(width, height)
    fig.text(0.5, 0.5, message, horizontalalignment='center',
             verticalalignment='center', size=font_large_size)
    write_fig(fig, out_file_name, bgcolor=bgcolor)
  finally:
    renderer_lock.release()


def render_blank(out_file_name, width, height, bgcolor='#ffffff'):
    render_message(out_file_name, 'no data', width, height, bgcolor=bgcolor)


smart_time_delta_size = 10

def render_smart(out_file_name, data,
                 x_column=None, y_column=None, label_column=None,
                 title='', xlab='', ylab='',
                 width=740, height=600, bgcolor=(0.95, 0.95, 0.95)):
    if len(data) < 2:
        render_blank(rave_tofile=out_file_name, width=width, height=height, bgcolor=bgcolor)
        return
    if x_column == None:
        if 'time' in data.columns and isinstance(data['time'][0], datetime):
            x_column = 'time'
        elif 'stime' in data.columns and isinstance(data['stime'][0], datetime):
            x_column = 'stime'
        elif 'etime' in data.columns and isinstance(data['etime'][0], datetime):
            x_column = 'etime'
    else:
        x_data = data[x_column]
    if not x_column in ('time', 'stime', 'etime'):
        raise 'Only time-series charts are currently implemented'
    # Figure out time-series properties for later work
    if x_column in ('time', 'stime', 'etime'):
        # The data is a time-series
        # Try to identify the bin size
        # First, try to collect the first smart_time_delta_size deltas.
        times = list(data[x_column])
        deltas = []
        times.sort()
        last_t = None
        for t in times:
            t = timegm(t.timetuple())
            if t <> last_t:
                if last_t:
                    deltas.append(t - last_t)
                last_t = t
            if len(deltas) >= smart_time_delta_size:
                break
        # Now take the smallest time delta as the bin size:
        deltas.sort()
        bin_size = deltas[0]
        # Also get the first and last times present in the series
        stime = times[0]
        etime = times[-1]
        # Average by five times the size of the bins
        average_by = bin_size * 5
    y_data = None
    if y_column == None:
        # Now pick an appropriate data (y) column
        if 'bytes' in data.columns:
            # Use bits per second if it's a time series, bytes otherwise
            y_column = 'bytes'
            if x_column in ('time', 'stime', 'etime'):
                y_data = [x*8/bin_size for x in data['bytes']]
                if ylab == '':
                    ylab = 'bits per second'
            else:
                y_data = data['bytes']
                if ylab == '':
                    ylab = 'bytes'
        elif 'packets' in data.columns:
            # Use packets per second if it's a time series, packets otherwise
            y_column = 'packets'
            if x_column in ('time', 'stime', 'etime'):
                y_data = [x/bin_size for x in data['packets']]
                if ylab == '':
                    ylab = 'packets per second'
            else:
                y_data = data['packets']
                if ylab == '':
                    ylab = 'packets'
        elif 'flows' in data.columns:
            y_column = 'flows'
            # Use flows per second if it's a time series, flows otherwise
            if x_column in ('time', 'stime', 'etime'):
                y_data = [x/bin_size for x in data['flows']]
                if ylab == '':
                    ylab = 'flows per second'
            else:
                y_data = data['flows']
                if ylab == '':
                    ylab = 'flows'
        elif 'shosts' in data.columns:
            y_column = 'shosts'
            # Use unique source hosts
            y_data = data['shosts']
            if ylab == '':
                if x_column in ('time', 'stime', 'etime'):
                    ylab = 'unique source hosts per %ds period' % bin_size
                else:
                    ylab = 'unique source hosts'
        elif 'dhosts' in data.columns:
            y_column = 'dhosts'
            # Use unique dest hosts
            y_data = data['dhosts']
            if ylab == '':
                if x_column in ('time', 'stime', 'etime'):
                    ylab = 'unique destination hosts per %ds period' % bin_size
                else:
                    ylab = 'unique destination hosts'
        else:
            raise "Could not find data y-column"
    if y_column and not y_data:
        y_data = data[y_column]
    # Figure out if the data contains a "label" column.
    if label_column == None:
        # Check for a column named 'label', or 'category'
        if 'label' in data.columns:
            label_column = 'label'
        elif 'category' in data.columns:
            label_column = 'category'
        # Try 'sval' or 'dval'
        elif 'sval' in data.columns:
            label_column = 'sval'
        elif 'dval' in data.columns:
            label_column = 'dval'
    # Now plot stuff!
    if x_column in ('time', 'stime', 'etime'):
        # If it's a timeseries, then
        # If there's a label column, use render_timeseries_ind
        if label_column:
            render_timeseries_ind(out_file_name, data[label_column],
                                  data[x_column], y_data,
                                  width, height, stime, etime,
                                  bin_size, average_by=average_by,
                                  ylab=ylab, title=title, bgcolor=bgcolor)
        # Otherwise, use render_timeseries
        else:
            render_timeseries(out_file_name, data[x_column], y_data,
                              width, height, stime, etime,
                              bin_size, average_by=average_by,
                              ylab=ylab, title=title, bgcolor=bgcolor)
    else:
        # Not a timeseries, blow up
        raise 'Only time-series charts are currently implemented'

def show_smart(*args, **kwargs):
    from subprocess import Popen
    from tempfile import NamedTemporaryFile
    tmp_file = NamedTemporaryFile(suffix='.png')
    tmp_file.close()
    render_smart(tmp_file.name, *args, **kwargs)
    Popen(['open', tmp_file.name])

#
# Radar plots
#

def polar2cart(angle, magnitude):
    "Convert polar coords to cartesian. angle is in radians."
    return magnitude * math.cos(angle), magnitude * math.sin(angle)

def make_radarplot(ax, radials=4, radlims=None, radprops=None, rad_max=.85,
                   scale=None, originprops=None, points=None, pointprops=None):
    "Render one or more multidimensional points as a polygon in a radar plot."

#   If we scale each radial dimension starting with 0 at the origin of
#   the graph, it prevents us from meaningfully plotting points with data in
#   only two of the plotted dimensions. By clamping 0 at an offset from the
#   origin, we make this kind of data visible.
#
#   The value is expressed as a percentage of a given radial's limit.
    origin_offset = .10 * rad_max

#   The zorder of the radial lines (including the polygon around the origin)
    rad_zorder = 100

    def draw_rad(rad, **props):
#        x,y = polar2cart(rad, 1 + origin_offset)
        x,y = polar2cart(rad, rad_max + origin_offset)
        ox, oy = polar2cart(rad, origin_offset)
        l = Line2D((ox,x), (oy,y), **props)
        l.set_zorder(100)
        ax.add_line(l)
        return l

    def rescale(p, radlims):
        rc = []
        for dim, lim in zip(p, radlims):
            if lim == 0:
            #   Special case -- assume a limit of zero implies
            #   data points are always zero
                scaled_dim = 0
            else:
            #   Hack zeroes into ones for log'ing
                dim = dim or 1
                if scale == 'log':
                    scaled_dim = math.log(dim)/math.log(lim)
                else:
                    scaled_dim = dim/lim
                scaled_dim = scaled_dim * rad_max
            scaled_dim += origin_offset
            rc.append(scaled_dim)
        return rc

    def plot_point(p, props):
    #   Plot a point on the radials, returning the polygon thus created
        scaled = rescale(p, radlims)
        verts = [
            polar2cart(radials[x], scaled[x])
            for x in range(len(radials))
        ]
        poly = Polygon(verts, **props)
        ax.add_patch(poly)
        return poly

    ax.set_xlim((-1. - origin_offset,1. + origin_offset))
    ax.set_ylim((-1. - origin_offset,1. + origin_offset))

#   Render aruments incontrovertibly sane:
#   radials
    try:
        len(radials)
    except TypeError:
        radials = [x * (360/radials) for x in xrange(radials)]

    #radials = [r/180. * math.pi for r in radials]
    radials = [math.radians(r) for r in radials]

#   radlims
    try:
        len(radlims)
    #   float-ify
        radlims = [float(radlims) for x in radlims]
    except TypeError:
        if radlims is None:
            radlims = 1.0
        radlims = [radlims for x in xrange(len(radials))]

#   radprops
#   radprops can be:
#   None - use default
#   A dict - use dict for all radii
#   A sequence of dicts - use dict[0] for radius[0], etc.
    defaultradprops = {'edgecolor': 'k'}
    if radprops is None:
        radprops = {}
    if type(radprops) == type({}):
        defaultradprops.update(radprops)
        radprops = [radprops for x in xrange(len(radials))]
    else:
        radprops = [defaultradprops.update(rp) for rp in radprops]

#   pointprops
#   pointprops can be:
#   None - use default
#   A dict - use dict for all points
#   A sequence of dicts - use dict[0] for point[0], etc.
#       recyclying as necessary
    defaultpointprops = {'facecolor':'k', 'edgecolor':'b', 'alpha':.15}
    if type(pointprops) == type({}):
        pointprops = [pointprops for x in xrange(len(radials))]
    elif pointprops is None:
        pointprops = [defaultpointprops for x in xrange(len(radials))]
    else:
    #   Assume pointprops is a sequence of properties dicts.
        if len(pointprops) != len(radials):
        #   Ensure that pointprops is the same length as radials,
        #   recycling as appropriate
            newprops = []
            for x in xrange(len(radials)):
                newprops.append(pointprops[x % len(pointprops)])
            pointprops = newprops

#   points
    if points is None:
        raise SyntaxError("Required argument points not supplied")
#   If any points are short, pad with zeroes
    for idx in range(len(points)):
        if len(points[idx]) < len(radials):
            diff = len(radials) - len(points[idx])
            points[idx] = points[idx] + [0 for x in xrange(diff)]
        points[idx] = [float(p) for p in points[idx]]

#   originprops
    if originprops is None:
        originprops = {
              'edgecolor': radprops[0].get(
                               'edgecolor', radprops[0].get('color', 'k')
              )
            , 'fill': False
            , 'zorder' : 100
        }
#
#   Do the actual, you know, work of the flippin' function
#

#   Plot radial lines and a polygon denoting the origin
    radlines = [ draw_rad(r, **props) for r,props in zip(radials,radprops) ]
    origin_poly = plot_point([0 for x in radials], originprops)

#   Plot the points
    polys = [ plot_point(p, props) for p, props in zip(points, pointprops) ]
    return radlines, polys



#@op_file
#@mime_type('image/png')
#def render_radar_multi(out_file_name, point_data, size
#                       rad_labels=None, rad_info=None, point_props=None,
#                       title='', text=[], **kwargs):
#    if rad_info is None:
#        rad_info = {}
#
#    kw = {}
#    kw['radials']    = rad_info.get('angles', len(point_data[0]))
#    kw['radlims']    = rad_info.get('limits', None)
#    kw['radprops']   = rad_info.get('props', None)
#    kw['points']     = point_data
#    kw['pointprops'] = point_props
#    kw.update(kwargs)
#
##   To work, the radar plot must be square. Hence, width and height
##   are the same number.
#    fig = make_figure(size, size)
#    axes = make_axes(fig, title=title)
#    axes.get_xaxis().set_visible(False)
#    axes.get_yaxis().set_visible(False)
#    axes.set_frame_on(False)
#
#    radials, polys = make_radarplot(axes, **kw)
#
##   Place text labels
##   TODO: radial labels
#    if rad_labels is not None:
#        for labelprops in rad_labels:
#            if type(labelprops) ==  type(str):
#                lstr, boxprops = labelprops, None
#            else:
#                lstr, boxprops = labelprops
#
##   Miscellaneous text
#    for xpos,ypos,txt,props in text:
#        t = Text(xpos,ypos,text, **props)
#        axes.add_patch(t)
#
#    write_fig(fig, out_file_name)


def render_hostchar(out_file_name, hc_data, width, height,
                    title='', bgcolor="#ffffff", **kwargs):
  renderer_lock.acquire()
  try:
    protos = { 'ICMP': 1, 6: 'TCP', 17: 'UDP'}

    def simplify(num):
        kb = 1024
        mb = 1024 * kb
        gb = 1024 * mb
        if num <  kb:
            return str(num)
        if num <  mb:
            return "%.2fK" % (float(num) / kb)
        if num < gb:
            return "%.2fM" % (float(num) / mb)
        return "%.2fG" % (float(num) / gb)

    def peak_str(peak):
        protonum, port, bytes = peak
        try:
            proto = protos[protonum]
        except KeyError:
            proto = "Proto %d" % protonum
        return "%s / %d\n%s" % (proto, port, simplify(bytes))


#   Maximum size of radial, as a ratio of total possible size
    rad_max = .65

    rad_info =  ( 
          (12.86, 'Mail', None)
        , (64.29, 'Web', None)
        , (115.71, 'Web', None)
        , (167.14, 'Mail', None)
        , (218.57, 'Other\nClient', peak_str(hc_data['peaks'][0]))
        , (270., 'Other\nUnclassified', peak_str(hc_data['peaks'][2]))
        , (321.42, 'Other\nServer', peak_str(hc_data['peaks'][1]))
    )

    rad_angles = [r[0] for r in rad_info]
    rad_labels = [r[1] for r in rad_info]

    kw = {}
    kw['radials']    = rad_angles
    kw['radlims']    = hc_data['total']
    kw['rad_max']    = rad_max
    kw['radprops']   = hc_data['display']['radials']
    kw['points']     = [ hc_data['all'], hc_data['recent'] ]
    kw['pointprops'] = [
          hc_data['display']['all']
        , hc_data['display']['recent']
    ]
    kw.update(kwargs)

    fig = make_figure(width, height)

#   To work, the radar plot must be square. (Make it the size of
#   the smallest dimension, minus some space for margin.)
    
    if width < height:
        plotsize = width - 96
    else:
        plotsize = height - 96

    margin_vert = int((height - plotsize) / 2)
    margin_horiz = int((width - plotsize) / 2)

    axes = make_axes(
          fig
        , margin_top=margin_vert
        , margin_bottom=margin_vert
        , margin_left=margin_horiz
        , margin_right=margin_horiz
        , title=title)
    axes.get_xaxis().set_visible(False)
    axes.get_yaxis().set_visible(False)
    axes.set_frame_on(False)

    radials, polys = make_radarplot(axes, **kw)

#   Place text labels
    plainrect = {'fill': False, 'pad': 15, 'linewidth': 2}
    for angle, label, boxlabel in rad_info:
        x,y = polar2cart(math.radians(angle), 1.35 * rad_max)
        t = Text(x, y, label
            , verticalalignment='center', horizontalalignment='center')
        axes.add_artist(t)
        if boxlabel is not None:
            t = Text(x, y - .25, boxlabel
                , verticalalignment='center', horizontalalignment='center')
            t.set_bbox(plainrect)
            axes.add_artist(t)

    write_fig(fig, out_file_name, bgcolor=bgcolor)
  finally:
    renderer_lock.release()
