#!/usr/bin/python
#  Copyright (C) 2012 by Carnegie Mellon University.
#  
#  @OPENSOURCE_HEADER_START@
#  Use of the Rayon 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.227.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 FA8721-05-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.
#  @OPENSOURCE_HEADER_END@

from __future__ import division


from rayon.rytools import *
from rayon import miscutils, toolbox, datautils, scales
from rayon.tickspec_parse import parse_tickspec

import math

def debug(msg):
    if rayon.DEBUG:
        tstamp = datetime.datetime.now()
        print "%s: %s" % (tstamp.strftime("%H:%M:%S"), msg)

def dbg_pprint(msg, d):
    if rayon.DEBUG:
        debug(msg)
        debug("-" * 25)
        pprint(d)
        debug("-" * 25)

def dbg_dataset(msg, d):
    if rayon.DEBUG:
        debug(msg)
        debug("-" * 25)
        print d.to_string()
        debug("-" * 25)


def choice(a, b, do_a=None, do_b=None,
           do_both=None, do_neither=None):
    # if a exists, run do_a (Default: return a)
    # if b exists, run do_b (Default: return b)
    # if both exist, run do_both (Default: do both do_a and do_b)
    # if neither exist, run do_neither (Default: do nothing)
    if do_a       is None: do_a       = lambda: a
    if do_b       is None: do_b       = lambda: b
    if do_both    is None: do_both    = lambda: (do_a(), do_b())
    if do_neither is None: do_neither = lambda: None
    
    if a is None and b is None           : return do_neither()
    elif a is not None and b is not None : return do_both()
    elif a is not None                   : return do_a()
    else                                 : return do_b()



# Trend line processing
default_trend_arguments = {
    'kernel': {'output_size': 100},
    'ols': {},
}

def get_trend_data(opts, scatter_data):
    if opts['trend_line'] is None:
        return None
    else:
        if opts['trend_args'] is None:
            trend_args = default_trend_arguments[opts['trend_line']]
        else:
            trend_args = opts['trend_args']
        return datautils.trend_data(opts['trend_line'], scatter_data,
                                    **trend_args)


# Data trimming

def get_trimmed_data(opts, tools, raw_data):
    # Get the real limits of the X and Y scales
    def get_limits(colidx, floor, floor_pct, ceil, ceil_pct):
        # Get the actual floor and ceiling values given the option
        # combinations and the column to limit.
        def val_from_pct(p):
            if p is None: return None
            return raw_data.get_column(colidx).percentile(p)
        # Take the higher of the two floor values
        real_floor = choice(
            a=floor, b=val_from_pct(floor_pct),
            do_both=lambda: max(floor, val_from_pct(floor_pct)))
        # Take the lower of the two ceiling values
        real_ceil = choice(
            a=ceil, b=val_from_pct(ceil_pct),
            do_both=lambda: min(floor, val_from_pct(ceil_pct)))
        return real_floor, real_ceil

    def munge_deprecated(old, new):
        if opts[old] != 'deprecated':
            return opts[old]
        return opts[new]
    
    x_floor_opt = munge_deprecated('xfloor', 'x_floor')
    x_floor_pct_opt = munge_deprecated('xfloor_pct', 'x_floor_pct')
    x_ceiling_opt = munge_deprecated('xceiling', 'x_ceiling')
    x_ceiling_pct_opt = munge_deprecated('xceiling_pct', 'x_ceiling_pct')
        
    x_colidx = opts['x_input']
    x_floor, x_ceil = get_limits(x_colidx,
                                 x_floor_opt, x_floor_pct_opt,
                                 x_ceiling_opt, x_ceiling_pct_opt)

    y_floor_opt = munge_deprecated('yfloor', 'y_floor')
    y_floor_pct_opt = munge_deprecated('yfloor_pct', 'y_floor_pct')
    y_ceiling_opt = munge_deprecated('yceiling', 'y_ceiling')
    y_ceiling_pct_opt = munge_deprecated('yceiling_pct', 'y_ceiling_pct')
        
    y_colidx = opts['y_input']
    y_floor, y_ceil = get_limits(y_colidx,
                                 y_floor_opt, y_floor_pct_opt,
                                 y_ceiling_opt, y_ceiling_pct_opt)

    # Use the limits to generate the keys we'll use in the filter
    # function
    def get_key(colidx, floor, ceil):
        floor_key = lambda r: r[colidx] > floor
        ceil_key = lambda r: r[colidx] < ceil
        between_key = lambda r: r[colidx] > floor and r[colidx] < ceil
        return choice(floor, ceil,
                      do_a=lambda: floor_key,
                      do_b=lambda: ceil_key,
                      do_both=lambda: between_key)
            
    x_key = get_key(x_colidx, x_floor, x_ceil)
    y_key = get_key(y_colidx, y_floor, y_ceil)

    # Combine the keys into one filter
    both_key = lambda r: x_key(r) and y_key(r)
    uberkey = choice(x_key, y_key,
                     do_both=lambda: both_key)
    
    # Finally, return the filtered data
    if uberkey is None:
        # After all that, no filtering
        return raw_data
    else:
        return raw_data.filter_pass(uberkey)


# MARK: TODO: add scale_max and use scale limits if supplied
def get_spatial_scale(tools, col, scale_name, dont_zero, scale_min, scale_max):
    # TODO: scale_max and scale_min should include all the points in
    # col!

    # Categorical scales are different(tm)
    if scale_name == 'cat':
        return tools.new_scale('cat', alignment='center')

    # Should we zero the scale? If so, replace scale_min with 0 (or 1)
    # By default, start the scale at zero if:
    #   * all the data is positive
    #   * scale_min isn't set
    #   * the user hasn't told us not to
    if (not dont_zero and scale_min is None and col.min() > 0):
        # Is this a log scale? If so, drop scale to 1 instead (after
        # checking to make sure the data isn't < 1).
        if scale_name == 'log':
            if col.min() < 1:
                raise Exception("Can't use log scale on values less than 1. "
                                "For integer data, consider 'clog'?")
            scale_min = 1
        else:
            scale_min = 0
    
    return tools.new_scale(scale_name, a_lo=scale_min, a_hi=scale_max)


    
def float_equals(a, b):
    return abs(a - b) < .00001

def get_precision(td_data, min_precision=0, max_precision=3):
    if min_precision >= max_precision:
        precision = min_precision
    else:
        precision = min_precision
        for d in td_data:
            tmp = d
            if precision > 0:
                tmp = tmp * 10 * min_precision
            tmp_precision = min_precision
            while tmp_precision <= max_precision:
                if float_equals(int(tmp), tmp):
                    break
                tmp = tmp * 10
                tmp_precision += 1
            precision = max(precision, tmp_precision)
            if precision == max_precision:
                break
    return precision


def make_tick_tuples(td_data, precision):
    if precision == 0:
        return tuple((i, "%d" % i) for i in td_data)
    else:
        fmt_str = "%%.%df" % precision
        return tuple((i, fmt_str % i) for i in td_data)

def get_tick_positions(col, scale, scale_type, tick_fns):
    if scale_type == 'cat':
        # Ignore the user and just print the nice stuff
        return scale.get_nice_tick_positions(), None
    else:
        tick_pos = tuple(reduce(lambda a,b: a+b,
                                (f([col], scale) for f in tick_fns)))
        precision = get_precision(tick_pos)
        return tick_pos, precision


def pad_chart(chart, opts):
    def spec_from_opt(src):
        if opts[src] is not None:
            return "%dpx" % opts[src]
        else:
            return None

    pad_opts = dict()
    for argname, optname in (('allpad','padding'),
                             ('tpad', 'pad_top'), ('bpad', 'pad_bottom'),
                             ('lpad', 'pad_left'), ('rpad', 'pad_right')):
        if opts[optname] is not None:
            pad_opts[argname] = "%dpx" % opts[optname]
    
    
    # Are any of the values set? Then use the user-supplied opts
    if len(pad_opts) == 0:
        chart.set_padding(allpad="20px")
    else:
        chart.set_padding(**pad_opts)
    return


def get_single_subchart(opts, tools, d, trend_d):
    c = tools.new_chart("square")

    x_col = d.get_column(opts['x_input'])
    y_col = d.get_column(opts['y_input'])
    
    if opts['marker_color_input'] is not None:
        color_col = d.get_column(opts['marker_color_input'])
    else:
        color_col = None

    if opts['marker_size_input'] is not None:
        size_col = d.get_column(opts['marker_size_input'])
    else:
        size_col = None


    # Create scales
    try:
        x_scale = get_spatial_scale(tools, x_col,
                                    opts['x_scale'],
                                    opts['x_scale_dont_zero'],
                                    opts['x_scale_min'],
                                    opts['x_scale_max'])
    except scales.RayonLimitException, e:
        raise ToolRuntimeError(
            "Can't plot zero with a log scale. Perhaps use "
            "--x-scale=clog, or a non-log scale.")

    try:
        y_scale = get_spatial_scale(tools, y_col,
                                    opts['y_scale'],
                                    opts['y_scale_dont_zero'],
                                    opts['y_scale_min'],
                                    opts['y_scale_max'])
    except scales.RayonLimitException, e:
        raise ToolRuntimeError(
            "Can't plot zero with a log scale. Perhaps use "
            "--y-scale=clog, or a non-log scale.")

    if size_col is None:
        size_scale = opts['marker_size']
    else:
        try:
            size_scale = tools.new_scale(
                opts['marker_size_scale'],
                a_lo=size_col.min(),
                a_hi=size_col.max(),
                b_lo=opts['marker_size_scale_min'],
                b_hi=opts['marker_size_scale_max'])
        except scales.RayonLimitException, e:
            raise ToolRuntimeError(
                "Can't plot zero with a log scale. Perhaps use "
                "--marker-size-scale=clog, or a non-log scale")
            

    if color_col is None:
        color_scale = opts['marker_color']
    else:
        # MARK: TODO: This is all fucked up. fix it.
        try:
            color_scale = tools.new_scale(
                color_scale_map[opts['marker_color_scale']],
                num_lo=color_col.min(),
                num_hi=color_col.max(),
                color_lo=opts['marker_color_scale_min'],
                color_hi=opts['marker_color_scale_max'])
        except scales.RayonLimitException, e:
            raise ToolRuntimeError(
                "Can't plot zero with a log scale. Perhaps use "
                "--marker-color-scale=clog, or a non-log scale")
                                                        

    # Make the plot
    main_plot = tools.new_plot(
        "scatter",
        x_data=x_col,
        y_data=y_col,
        marker_size_data=size_col,
        marker_color_data=color_col,
        x_scale=x_scale,
        y_scale=y_scale,
        marker_shape_scale=opts['marker'],
        marker_size_scale=size_scale,
        marker_color_scale=color_scale)

    c.add_plot(main_plot, "main")

    # Set up tick marks (Precision is used to pretty up presentation
    # of the numbers.)

    if opts['xticks'] != 'deprecated':
        # Use old param
        opts_xticks = opts['xticks']
    else:
        # Use new param
        opts_xticks = opts['x_ticks']

    if opts['yticks'] != 'deprecated':
        opts_yticks = opts['yticks']
    else:
        opts_yticks = opts['y_ticks']


    x_tick_pos, x_precision = get_tick_positions(
        x_col, x_scale,
        opts['x_scale'], opts_xticks)
    y_tick_pos, y_precision = get_tick_positions(
        y_col, y_scale,
        opts['y_scale'], opts_yticks)

    if x_precision is None:
        x_tick_tuples = tuple((i, None) for i in x_tick_pos)
    else:
        x_tick_tuples = make_tick_tuples(x_tick_pos, x_precision)

    if y_precision is None:
        y_tick_tuples = tuple((i, None) for i in y_tick_pos)
    else:
        y_tick_tuples = make_tick_tuples(y_tick_pos, y_precision)

    x_tickmarker = tools.new_tickmarker(
        'horizontal',
        marker_args=dict(below=opts['x_tick_size'], above=0),
        label_spacing=opts['x_label_spacing'],
        angle=opts['x_label_angle'],
        halign=opts['x_label_halign'],
        valign=opts['x_label_valign'],
        font_size=opts['x_label_size'])

    if opts['xborder'] != "deprecated":
        bottom_border_line_style=opts['xborder']
    else:
        bottom_border_line_style=opts['bottom_border_line_style']
        
    btm_border = tools.new_border('htick', scale=x_scale,
                                  line_type=bottom_border_line_style,
                                  tick_tuples=x_tick_tuples,
                                  tickmarker=x_tickmarker)

    y_tickmarker = tools.new_tickmarker(
        'vertical',
        marker_args=dict(before=opts['y_tick_size'], after=0),
        label_spacing=opts['y_label_spacing'],
        angle=opts['y_label_angle'],
        halign=opts['y_label_halign'],
        valign=opts['y_label_valign'],
        font_size=opts['y_label_size'])

    if opts['yborder'] != "deprecated":
        left_border_line_style = opts['yborder']
    else:
        left_border_line_style = opts['left_border_line_style']
    left_border = tools.new_border('vtick', scale=y_scale,
                                   line_type=left_border_line_style,
                                   tick_tuples=y_tick_tuples,
                                   tickmarker=y_tickmarker)

    c.add_bottom_border(btm_border, height=.1)
    c.add_left_border(left_border, width=.1)
           

    # Trend line
    if trend_d is not None:
        line_plot = tools.new_plot('line',
                                   x_data=trend_d.get_column(0),
                                   y_data=trend_d.get_column(1),
                                   x_scale=x_scale, y_scale=y_scale,
                                   line_color_scale=opts['trend_line_color'],
                                   line_width_scale=opts['trend_line_width'],
                                   line_style_scale="solid")
        c.add_plot(line_plot, "line")
                                   

    # Titles and labels
    if opts['title'] is not None:
        title = tools.new_border('hlabel', label=opts['title'],
                                 font_size="large", bpad=.05)
        c.add_top_title(title, height=.1)

    if opts['caption'] is not None:
        title = tools.new_border('hlabel', label=opts['caption'], tpad=.05)
        c.add_bottom_title(title, height=.075)

    if opts['xlabel'] != 'deprecated':
        bottom_label = opts['xlabel']
    else:
        bottom_label = opts['bottom_label']

    if bottom_label is not None:
        title = tools.new_border('hlabel', label=bottom_label, tpad=.05)
        c.add_bottom_border(title, height=.05)

        
    if opts['ylabel'] != 'deprecated':
        left_label = opts['ylabel']
    else:
        left_label = opts['left_label']
        
    if opts['left_label'] is not None:
        title = tools.new_border('vlabel', label=left_label, rpad=.05)
        c.add_left_border(title, width=.05)
        

    # Padding
    # base_padding = 20
    # TODO: Add padding based on label-angle. Right now, we assume
    # everything's horizontal. Therefore, the top and bottom don't
    # need extra padding, but the left and right do.
    pad_chart(c, opts)

    return c

def get_grid_dimensions(num_cells):
    # Determine the appropriate number of rows and columns in a grid
    # to accomodate the given number of cells.
    #
    # If the number if cells is a perfect square, use that for rows
    # and columns. Otherwise, start with the square root of the number
    # of cells and alternate adding rows and columns until we have a
    # grid that can contain them.
    num_rows = num_cols = int(math.sqrt(num_cells))
    if int(math.sqrt(num_cells)) != math.sqrt(num_cells):
        ct = 0
        while (num_rows * num_cols) < num_cells:
            if ct % 2 == 0: num_cols += 1
            else          : num_rows += 1
    return num_rows, num_cols

def get_grid_label(opts, key):
    # Use options and key to generate a label for a subchart.
    label = opts['grid_label']
    # Manual replacement in case someone fancies a format-string
    # attack. Heck, it's probably faster, too.
    label = label.replace('%s', key)
    return label

def get_grid_chart(opts, tools):
    raw_data = tools.new_dataset_from_stream(
        istream_from_str(opts['input_path']))

    # Verify that this isn't the X or Y column
    raw_key_col = raw_data.get_column(opts['grid_key_input'])
    if (raw_key_col == raw_data.get_column(opts['x_input']) or
        raw_key_col == raw_data.get_column(opts['y_input'])):
            raise ConfigurationError(
                "--grid-key-input should not overlap with "
                "--x-input or --y-input")

    # Get data subsets
    datasets = raw_data.partition(opts['grid_key_input'])
    
    # Create overarching tiled chart
    num_rows, num_cols = get_grid_dimensions(len(datasets))
    uberchart = tools.new_chart('tiled',
                                num_rows=num_rows,
                                num_columns=num_cols)
    
    # Generate subcharts and add to uberchart
    subcharts = []
    for key, dataset in datasets:
        # TODO: add options to specify percentile-floors over _all_
        # the data, not just per dataset. (Easy enough to to, right?
        # Just take the percentile from raw_data. The "hard part" is
        # just deciding what options to provide.)
        trimmed_data = get_trimmed_data(opts, tools, dataset)
        if opts['trend_untrimmed_data']:
            trended_data = get_trend_data(opts, dataset)
        else:
            trended_data = get_trend_data(opts, trimmed_data)
        subchart = get_single_subchart(opts, tools, trimmed_data, trended_data)
        subchart.add_bottom_title(
            tools.new_border('hlabel', label=get_grid_label(opts, key)),
            height=.1)
        uberchart.add_plot(subchart, key)

    return uberchart

def get_single_chart(opts, tools):
    raw_data = tools.new_dataset_from_stream(
        istream_from_str(opts['input_path']),
        first_line_is_colnames=opts['first_line_colnames'])
    trimmed_data = get_trimmed_data(opts, tools, raw_data)
    if opts['trend_untrimmed_data']:
        trended_data = get_trend_data(opts, raw_data)
    else:
        trended_data = get_trend_data(opts, trimmed_data)
    return get_single_subchart(opts, tools, trimmed_data, trended_data)


def main(opts):
    tools = toolbox.Toolbox.for_file()

    if opts['grid'] != "deprecated":
        do_grid = opts['grid']
    else:
        do_grid = opts['grid_plot']

    if do_grid:
        c = get_grid_chart(opts, tools)
    else:
        c = get_single_chart(opts, tools)

    if c is None:
        # Not implemented yet
        return 1
        
    c.set_chart_background(opts['chart_bgcolor'])
    c.set_plot_background(opts['plot_bgcolor'])

    page = tools.new_page_from_filename(
        opts['output_path'], opts['width'], opts['height'])
    page.write(c)
    return 0


# Option parsing and execution

    

xborder_choices = ['none', 'line']
border_line_style_choices = ['solid', 'dotted', 'dotdash', 'dashed', 'none']
scale_choices = ['linear', 'log', 'clog', 'cat']
color_scale_choices = ['linear', 'log']
color_scale_map = {
    'linear' : 'gradient',
    'log'    : 'loggrad'
}
size_scale_choices = ['linear', 'log']
font_size_choices = ['small', 'normal', 'large', 'x-large', 'x-large']
halign_choices = ['left', 'center', 'right']
valign_choices = ['top', 'center', 'bottom']
marker_choices = ['dot', 'circle', 'vline', 'hline', 'cross']

def cstr(spec): return miscutils.parse_color_string(spec)

def str_from_choices(choices):
    return ", ".join(["'%s'" % c for c in choices])

def deprecated_dict(deprecated_name, new_name, do_default_val=True):
    if do_default_val:
        return dict(optname=deprecated_name,
                    default_val="deprecated",
                    hlp="Deprecated. Use %s" % new_name)
    else:
        return dict(optname=deprecated_name,
                    hlp="Deprecated. Use %s" % new_name)



app_options = [
    ColnameOption(
        "x-input", default_val=0,
        hlp="Name or index (from left, starting at 0) of "
        "column to plot on X axis (default: 0 (i.e., first column))"),
    ColnameOption(
        "y-input", default_val=1,
        hlp="Name or index (from left, starting at 0) of "
        "column to plot on Y axis (default: 1 (i.e., second column))"),
    # Old deprecated borders stuff
    ChoiceOption(
        "xborder", default_val='deprecated',
        hlp="Deprecated. Use bottom-border-line-style.",
        choices=['none', 'line', 'deprecated']),
    ChoiceOption(
        "yborder", default_val='deprecated',
        hlp="Deprecated. Use left-border-line-style.",
        choices=['none', 'line', 'deprecated']),
     # New Borders
    ChoiceOption(
        "bottom-border-line-style", default_val="solid",
        choices=border_line_style_choices,
        hlp="Type of line to draw on bottom edge of plot. "
        "(Values: 'solid', 'dotted', 'dotdash', 'dashed', 'none')"
        "Default: 'solid'"),
    ChoiceOption(
        "left-border-line-style", default_val="solid",
        choices=border_line_style_choices,
        hlp="Type of line to draw on left edge of plot. "
        "(Values: 'solid', 'dotted', 'dotdash', 'dashed', 'none')"
        "Default: 'solid'"),
    # X tick marks
    NewTickspecOption(**deprecated_dict("xticks", "x-ticks")),
    NewTickspecOption(
        "x-ticks", default_val=parse_tickspec("x-ticks",
                                              "smin,max"),
        hlp="positions of tickmarks. (Default: 'smin,max')"),
    # Y tick marks
    NewTickspecOption(**deprecated_dict("yticks", "y-ticks")),
    NewTickspecOption(
        "y-ticks", default_val=parse_tickspec("y-ticks",
                                              "smin,smax"),
        hlp="positions of tickmarks. (Default: 'smin,smax')"),
    # X scale
    ChoiceOption(
        "xscale", default_val="deprecated",
        choices=['deprecated'] + scale_choices,
        hlp="Deprecated. Use x-scale."),
    ChoiceOption(
        "x-scale", default_val="linear",
        choices=scale_choices,
        hlp=("How to scale X axis. (Options: %s)" %
             str_from_choices(scale_choices))),
    # -- Scale max
    FloatOption(**deprecated_dict("xscale-max", "x-scale-max")),
    FloatOption(
        "x-scale-max", default_val=None, # i.e., no limit
        hlp="X scale maximum value."),
    # -- Scale min
    FloatOption(**deprecated_dict("xscale-min", "x-scale-min")),
    FloatOption(
        "x-scale-min", default_val=None, # i.e., no limit
        hlp="X scale minimum value."),
    Flag("x-scale-dont-zero",
         hlp="Disable default scale-zeroing behavior"),
    # Y scale
    ChoiceOption(
        "yscale", default_val="deprecated",
        choices=['deprecated'] + scale_choices,
        hlp="Deprecated. Use y-scale."),
    ChoiceOption(
        "y-scale", default_val="linear",
        choices=scale_choices,
        hlp=("How to scale Y axis. (Options: %s)" %
             str_from_choices(scale_choices))),
    # -- Scale max
    FloatOption(**deprecated_dict("yscale-max", "y-scale-max")),
    FloatOption(
        "y-scale-max", default_val=None, # i.e., no limit
        hlp="Y scale maximum value."),
    # -- Scale min
    FloatOption(**deprecated_dict("yscale-min", "y-scale-min")),
    FloatOption(
        "y-scale-min", default_val=None, # i.e., no limit
        hlp="Y scale minimum value."),
    Flag("y-scale-dont-zero",
         hlp="Disable default scale-zeroing behavior"),
    # Color scale
    ColnameOption(
        "marker-color-input", default_val=None,
        hlp="Name or index (from left, starting at 0) of "
        "column to plot as marker color (default: none)"),
    ChoiceOption(
        "marker-color-scale", default_val="linear",
        choices=color_scale_choices,
        hlp=("How to scale color axis. (Options: %s)" %
             str_from_choices(color_scale_choices))),
    ColorOption(
        "marker-color-scale-min", default_val=cstr("#ccccccff"),
        hlp="Lower bound of color scale"),
    ColorOption(
        "marker-color-scale-max", default_val=cstr("#000000ff"),
        hlp="Upperbound of color scale"),
    # Size Scale
    ColnameOption(
        "marker-size-input", default_val=None,
        hlp="Name or index (from left, starting at 0) of "
        "column to plot as marker size (default: none)"),
    ChoiceOption(
        "marker-size-scale", default_val="linear",
        choices=color_scale_choices,
        hlp=("How to scale marker size axis. (Options: %s)" %
             str_from_choices(color_scale_choices))),
    FloatOption(
        "marker-size-scale-min", default_val=1,
        hlp="Lower bound of marker size scale"),
    FloatOption(
        "marker-size-scale-max", default_val=1,
        hlp="Upperbound of marker size scale"),
    # Data Floor
    # -- X
    FloatOption(**deprecated_dict("xfloor", "x-floor")),
    FloatOption(
        "x-floor", default_val=None,
        hlp='omit points < n on x. (Default: no floor)'),
    IntOption(**deprecated_dict("xfloor-pct", "x-floor-pct")),
    IntOption(
        "x-floor-pct", default_val=None,
        hlp="omit points > nth percentile on x. (Default: no floor)"),
    # -- Y
    FloatOption(**deprecated_dict("yfloor", "y-floor")),
    FloatOption(
        "y-floor", default_val=None,
        hlp='omit points < n on y (Default: no floor)'),
    IntOption(**deprecated_dict("yfloor-pct", "y-floor-pct")),
    IntOption(
        "y-floor-pct", default_val=None,
        hlp="omit points > nth percentile on y. (Default: no floor)"),
    # Data Ceiling
    # -- X
    FloatOption(**deprecated_dict("xceiling", "x-ceiling")),
    FloatOption(
        "x-ceiling", default_val=None,
        hlp='omit points < n on x. (Default: no ceiling)'),
    IntOption(**deprecated_dict("xceiling-pct", "x-ceiling-pct")),
    IntOption(
        "x-ceiling-pct", default_val=None,
        hlp="omit points > nth percentile on x. (Default: no ceiling)"),
    # -- Y
    FloatOption(**deprecated_dict("yceiling", "y-ceiling")),
    FloatOption(
        "y-ceiling", default_val=None,
        hlp='omit points < n on y. (Default: no ceiling)'),
    IntOption(**deprecated_dict("yceiling-pct", "y-ceiling-pct")),
    IntOption(
        "y-ceiling-pct", default_val=None,
        hlp="omit points > nth percentile on y. (Default: no ceiling)"),
    # Words around the vis
    # -- X label
    StringOption(**deprecated_dict("xlabel", "x-label")),
    StringOption("bottom-label", default_val="", hlp="bottom label"),
    IntOption(
        "x-label-angle", default_val=45,
        hlp="Degrees of rotation (0 == horizontal, left-to-right) "
        "to apply to text on X axis. (Default: 45)"),
    ChoiceOption(
        "x-label-halign", default_val="right",
        choices=halign_choices,
        hlp=("Horizontal alignment of text on X axis rel. to tick mark. "
             "(Values: %s) (Default: 'right')" %
             str_from_choices(halign_choices))),
    ChoiceOption(
        "x-label-valign", default_val="center",
        choices=valign_choices,
        hlp=("Vertical alignment of text on X axis rel. to tick mark. "
             "(Values: %s) (Default: 'center')" %
             str_from_choices(valign_choices))),
    ChoiceOption(
        "x-label-size", default_val="normal",
        choices=font_size_choices,
        hlp=("Relative font size of X axis label. "
             "(Values: %s) (Default: 'normal')" %
             str_from_choices(font_size_choices))),
    IntOption(
        "x-label-spacing", default_val=10,
        hlp="Spacing between text and plot, in pts/pixels. "
        "(Default: 10)"),
    IntOption(
        "x-tick-size", default_val=3,
        hlp="Size in pts/pixels of tick mark on X axis. (Default: 3)"),
    # -- Y label
    StringOption(**deprecated_dict("ylabel", "y-label")),
    StringOption("left-label", default_val="", hlp="left label"),
    IntOption(
        "y-label-angle", default_val=0,
        hlp="Degrees of rotation (0 == vertical, top-to-bottom) "
        "to apply to text on Y axis. (Default: 0)"),
    ChoiceOption(
        "y-label-halign", default_val="right",
        choices=halign_choices,
        hlp=("Horizontal alignment of text on Y axis rel. to tick mark. "
             "(Values: %s) (Default: 'right')" %
             str_from_choices(halign_choices))),
    ChoiceOption(
        "y-label-valign", default_val="center",
        choices=valign_choices,
        hlp=("Vertical alignment of text on Y axis rel. to tick mark. "
             "(Values: %s) (Default: 'center')" %
             str_from_choices(valign_choices))),
    ChoiceOption(
        "y-label-size", default_val="normal",
        choices=font_size_choices,
        hlp=("Relative font size of Y axis label. "
             "(Values: %s) (Default: 'normal')" %
             str_from_choices(font_size_choices))),
    IntOption(
        "y-label-spacing", default_val=10,
        hlp="Spacing between top of text and plot, in pts/pixels. "
        "(Default: 10)"),
    IntOption(
        "y-tick-size", default_val=3,
        hlp="Size in pts/pixels of tick mark on Y axis. (Default: 8)"),
    # Markers
    ChoiceOption(
        "marker", default_val="dot",
        choices=marker_choices,
        hlp=("Marker for points. (Values: %s) (Default: 'dot')"
             % str_from_choices(marker_choices))),
    FloatOption(
        "marker-size", default_val=1,
        hlp="If size is not a function of data, the size "
        "of marker in pts/pixels (Default: 1)"),
    ColorOption(
        "marker-color", default_val=cstr("#00000040"),
        hlp="If color is not a function of data, the color "
        "of the point marker. (Default: transparent gray)"),
    # Trending
    #
    # TODO: May not implement these. I'm thinking trending should be
    # done by an external tool. This would mean an additional dataset,
    # which could come from another file or from a mythical file
    # containing multiple datasets.
    ChoiceOption(
        "trend-line", default_val=None,
        choices=['kernel', 'ols'],
        hlp="Add trend (regression) line of specified type. "
             "(Values: 'kernel', 'ols') (Default: no trend line)"),
    KVOption(
        "trend-args", default_val=None,
        #hlp="(Optional) args to customize --trend-line",
        hlp=SUPPRESS_HELP,
        converter=lambda v:miscutils.infer_type_from_string(v)(v)),
    ColorOption(
        "trend-line-color", default_val=cstr("#ff0000ff"),
        hlp="Color of trend line (Default: red)"),
    FloatOption(
        "trend-line-width", default_val=3,
        hlp="Width of trend line in pixels/points (Default: 3)"),
    Flag("trend-untrimmed-data",
        hlp="ignore floor and ceiling options when trending data"),
    # Colors
    ColorOption(**deprecated_dict("background-color", "chart-bgcolor")),
    ColorOption(
        "plot-bgcolor", default_val=cstr("#00000000"),
        hlp="Background color of plotting area (Default: transparent)"),
    ColorOption(
        "chart-bgcolor", default_val=cstr("#ffffffff"),
        hlp="Background color of chart area (Default: white)"),
    # Scatterplot grid
    #StringOption(**deprecated_dict("grid", "grid-plot")),
    Flag(**deprecated_dict("grid", "grid-plot", False)),
    Flag("grid-plot",
         hlp="Input is multiple datasets. Present as grid of "
         "multiple scatterplots. (Default: no)"),
    ColnameOption(
        "grid-key-input", default_val=2,
        hlp="Name or index (from left, starting at 0) of column"
        "to use as key to distinguish multiple datasets in input file. "
        "(Req. --grid-plot) (Default: 2 (i.e., third column))"),
    StringOption(
        "grid-label", default_val="",
        hlp="Label for each subplot. '%s' = contents of grid key. "
        "(Default: no label)"),
    # -- Other
    StringOption("title", default_val="", hlp="Chart title"),
    StringOption("caption", default_val="", hlp="Chart caption"),
    IntOption(
        "padding", default_val=None,
        hlp="Padding to add to all sides of output. (Default: see man page)"),
    IntOption(
        "pad-top", default_val=None,
        hlp="Padding to add to top of output. (Default: see man page)"),
    IntOption(
        "pad-bottom", default_val=None,
        hlp="Padding to add to bottom of output. (Default: see man page)"),
    IntOption(
        "pad-left", default_val=None,
        hlp="Padding to add to left of output. (Default: see man page)"),
    IntOption(
        "pad-right", default_val=None,
        hlp="Padding to add to right of output. (Default: see man page)"),
    
                  
    # TODO: implement
    # IntOption(
    #     "hpad", default_val=0,
    #     hlp="Horizontal space between Y axis and plot space"),
    # IntOption(
    #     "vpad", default_val=0,
    #     hlp="Vertical space between X axis and plot space"),
]




desc = "plot data in a scatterplot"

execute(cmd_func=main, cmd_options=app_options, cmd_desc=desc)
