import collections
import numpy as np

def calculate_heuristic_val(applicant_data):
	university, major, GPA, SAT = 0,0,0,0

	# Set university = 4 to the hueristic value if they went to a top 20 university
	if applicant_data[0] == '0':
		university = 4.0

	# Set major 4 to the hueristic value if their major is in CS, Math/Stat, or Engineer
	if applicant_data[1] in ['CS', 'Math/Stat', 'Engineering']:
		major = 4.0

	# Convert the GPA to a float from a strong, save it as the GPA variable
	GPA = float(applicant_data[2])

	# Divide SAT score by 400 (so it is between 0 and 4)
	# Save this variable as a float in the SAT variable
	SAT = float(applicant_data[3])/400

	# Add the four variables above and return this value
	applicant_heurisitc = university + major + GPA + SAT

	return applicant_heurisitc

def sort_applicants(input_file, output_file):
	# Open the input file
	input_file = open(input_file, "r")
	# Initialize a list
	applicant_list = list()

	# Iterate over the input file
	for line in input_file:
		# Split the applicant data by commas
		applicant_data_arr = line.split(",")
		# Calculate heuristic
		hueristic_val = calculate_heuristic_val(applicant_data_arr)
		# Add a tuple to the main lsit with hueristic_val as the first element
		# And the full applicant data as the second element
		applicant_list.append((hueristic_val, applicant_data_arr))

	# Close the input file (this is good practice but not totally necessary)
	input_file.close()

	# Sort the list in descending order using the reverse parameter
	applicant_list.sort(reverse=True)

	# Open the output file
	with open(output_file, 'a') as output_file:
		# Add the first 250 elements to the output file
		# Note that it needs to be in CSV format so each entry must be separated by a comma
		for x in range(250):
			output_line = str(applicant_list[x][0]) + ","
			output_line += ",".join(applicant_list[x][1])
			output_file.write(output_line)

def analyze_output(data_file):
	num_top_20_unveristy = 0

	num_majors_selected = [0,0,0,0]
	major_map = {"CS":0, "Math/Stat":1, "Engineering":2, "Other":3}

	gpa_scores = list()
	sat_scores = list()

	num_racial_cateogries_selected = [0,0,0,0,0]
	race_num_map = {"White":0, "Black":1, "Hispanic/Latinx":2, "Asian/Pacific Islander":3, "American Indian":4}
	num_male = 0

	data_file = open(data_file, "r")
	applicant_list = list()

	for line in data_file:
		app_data_arr = line.split(",")
		num_top_20_unveristy += 1 if app_data_arr[1] == '0' else 0
		num_majors_selected[major_map[app_data_arr[2]]] += 1
		gpa_scores.append(float(app_data_arr[3]))
		sat_scores.append(float(app_data_arr[4]))

		num_racial_cateogries_selected[race_num_map[app_data_arr[5]]] += 1
		num_male += 1 if app_data_arr[6] == "Male\n" else 0

	percent_top_20 = (float(num_top_20_unveristy)/250)*100
	percent_top_20 = round(percent_top_20, 1)
	print("Applicants from Top 20 University: " + str(percent_top_20) + "%\n")

	for x in range(len(num_majors_selected)):
		percent_major = (float(num_majors_selected[x])/250)*100
		percent_major = round(percent_major, 1)
		print(list(major_map.keys())[x] + ": " + str(percent_major) + "%")

	print("\nAverage GPA: " + str(round(np.mean(gpa_scores), 2)) + "\n")
	print("Average SAT: " + str(round(np.mean(sat_scores), 0)) + "\n")

	for x in range(len(num_racial_cateogries_selected)):
		percent_racial_cateogry = float((num_racial_cateogries_selected[x])/250)*100
		percent_racial_cateogry = round(percent_racial_cateogry, 1)
		print(list(race_num_map.keys())[x] + ": " + str(percent_racial_cateogry) + "%")

	percent_male = float(num_male)/250*100
	percent_male = round(percent_male, 1)

	percent_female = float(250-num_male)/250*100
	percent_female = round(percent_female, 1)

	print("\nMale Applicants Selected: " + str(percent_male) + "%")
	print("Female Applicants Selected: " + str(percent_female) + "%")

def main():
	sort_applicants("applicant-data.csv", "top-applicants.csv")
	analyze_output("top-applicants.csv")

if __name__ == '__main__':
	main()


from urllib.request import urlopen,Request
from urllib.error import HTTPError
from bs4 import BeautifulSoup
from datetime import datetime
import re

def get_indeces_by_region(url):
    q = Request(url)
    html = urlopen(q)
    bsObj = BeautifulSoup(html.read(),'html.parser')
    tables = bsObj.findAll("div",{"class":re.compile("^data-tables.*")})
    for table in tables:
        region_div = table.find("div",{"class":"table-container__header"})
        region_name = region_div.h2.get_text()
        indices = get_index_for_region(table)
        if(len(indices)!=0):
            for index in indices:
                index['region'] = region_name
            print(indices)

def get_index_for_region(table):
    results =[]
    for table_row in table.tbody.children:
        try:
            name_div =  table_row.find("div",{"data-type":"full"})
            name = name_div.get_text()
            abbreviation = table_row.find("div",{"data-type":"abbreviation"}).get_text()
            tds = [x.get_text() for x  in table_row.findAll("td")]
            value = tds[1]
            net_change = tds[2]
            pct_change = tds[3]
            pct_change_month = tds[4]
            pct_change_year = tds[5]
            curtime = tds[6]
            now = datetime.now()
            timestamp = datetime.strptime(str(now.year)+" "+ str(now.month)+" "+ str(now.day)+" "+curtime,"%Y %m %d %I:%M %p")
            result = {"name":name,"abbreviation":abbreviation,"value":value,"net_change":net_change,"pct_change":pct_change,"pct_change_month":pct_change_month,"pct_change_year":pct_change_year,"timestamp":timestamp}
            results.append(result)

        except Exception as e:
            #print(e)
            continue
    return results

get_indeces_by_region('https://www.bloomberg.com/markets/stocks/world-indexes/europe-africa-middle-east')


import random

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

from common.ld_utils import unsort as unsort_list

# pytorch implementation for np.nanmean
# https://github.com/pytorch/pytorch/issues/21987#issuecomment-539402619
def nanmean(v, *args, inplace=False, **kwargs):
    if not inplace:
        v = v.clone()
    is_nan = torch.isnan(v)
    v[is_nan] = 0
    return v.sum(*args, **kwargs) / (~is_nan).float().sum(*args, **kwargs)

def grad_norm(model):
    # compute norm of gradient for a model
    total_norm = None
    for p in model.parameters():
        if p.grad is not None:
            if total_norm is None:
                total_norm = 0
            param_norm = p.grad.detach().data.norm(2)
            total_norm += param_norm.item() ** 2

    if total_norm is not None:
        total_norm = total_norm ** (1.0 / 2)
    else:
        total_norm = 0.0
    return total_norm

def pad_tensor_list(v_list: list):
    dev = v_list[0].device
    num_meshes = len(v_list)
    num_dim = 1 if len(v_list[0].shape) == 1 else v_list[0].shape[1]
    v_len_list = []
    for verts in v_list:
        v_len_list.append(verts.shape[0])

    pad_len = max(v_len_list)
    dtype = v_list[0].dtype
    if num_dim == 1:
        padded_tensor = torch.zeros(num_meshes, pad_len, dtype=dtype)
    else:
        padded_tensor = torch.zeros(num_meshes, pad_len, num_dim, dtype=dtype)
    for idx, (verts, v_len) in enumerate(zip(v_list, v_len_list)):
        padded_tensor[idx, :v_len] = verts
    padded_tensor = padded_tensor.to(dev)
    v_len_list = torch.LongTensor(v_len_list).to(dev)
    return padded_tensor, v_len_list

def unpad_vtensor(
    vtensor: (torch.Tensor), lens: (torch.LongTensor, torch.cuda.LongTensor)
):
    tensors_list = []
    for verts, vlen in zip(vtensor, lens):
        tensors_list.append(verts[:vlen])
    return tensors_list

def one_hot_embedding(labels, num_classes):
    """Embedding labels to one-hot form.
    Args:
      labels: (LongTensor) class labels, sized [N, D1, D2, ..].
      num_classes: (int) number of classes.
    Returns:
      (tensor) encoded labels, sized [N, D1, D2, .., Dk, #classes].
    """
    y = torch.eye(num_classes).float()
    return y[labels]

def unsort(ten, sort_idx):
    """
    Unsort a tensor of shape (N, *) using the sort_idx list(N).
    Return a tensor of the pre-sorting order in shape (N, *)
    """
    assert isinstance(ten, torch.Tensor)
    assert isinstance(sort_idx, list)
    assert ten.shape[0] == len(sort_idx)

    out_list = list(torch.chunk(ten, ten.size(0), dim=0))
    out_list = unsort_list(out_list, sort_idx)
    out_list = torch.cat(out_list, dim=0)
    return out_list

def softargmax_kd(inputs_hm, temperature):
    """
    Perform softargmax on a k-dimensional heatmap
    Inputs: (N, D1, D2, .., Dk)
    Outputs: (N, K)
    """
    assert isinstance(inputs_hm, torch.Tensor)
    assert isinstance(temperature, float)
    assert len(inputs_hm.shape) >= 2
    dev = inputs_hm.device
    batch_size = inputs_hm.shape[0]
    num_dim = len(inputs_hm.shape) - 1
    dims = inputs_hm.shape[1:]
    lins = [torch.arange(dim).view(-1, 1).to(dev) + 0.5 for dim in dims]
    points_kd = lins[0]
    for lin in lins[1:]:
        points_kd = all_comb(points_kd, lin)

    points_kd = points_kd.unsqueeze(0)
    hms = inputs_hm.view(batch_size, -1, 1)

    norm_hms = F.softmax(hms * temperature, dim=1)
    weighted_points = (points_kd * norm_hms).sum(dim=1)
    assert weighted_points.shape[0] == batch_size
    assert weighted_points.shape[1] == num_dim
    return weighted_points

def gumbel_sample_kd(hm, num_samples, tau, logit_scale, hard):
    """
    hm: (N, D1, D2, D3, .., Dk), e.g., (N, x, y, z)
    num_samples: int, num samples to draw from each heatmap (N*samples) samples at the end
    tau: float, tau term for gumbel
    logit_scale: scale back logits before log(logits)
    hard: gives one hot softmax prob or not in gumbel.

    Outputs:
        samples: (N, samples, k)
    """
    batch_size = hm.shape[0]
    hm_size = hm.shape[1]
    assert np.all(np.array(hm.shape[1:]) == hm_size)
    num_dim = len(hm.shape[1:])
    dev = hm.device

    assert (
        num_dim == 2 or num_dim == 3
    ), "This function only supports 2d/3d heatmaps; verify for higher dim"
    hm = hm.reshape(batch_size, 1, -1)
    prob_dim = hm.shape[2]
    mesh_grids = torch.meshgrid([torch.arange(hm_size) for _ in range(num_dim)])
    mesh_grids = torch.stack(mesh_grids, dim=-1).to(dev).float()
    mesh_grids = mesh_grids.view(1, 1, prob_dim, num_dim)

    hm = hm.expand(batch_size, num_samples, prob_dim).reshape(
        batch_size * num_samples, prob_dim
    )
    samples = F.gumbel_softmax(logit_scale * hm, tau=tau, hard=hard, dim=1)
    samples = samples.view(batch_size, num_samples, prob_dim, 1)
    samples = samples * mesh_grids
    samples = samples.sum(dim=2)
    return samples

def gumbel_sample_kd_iter(hm, num_samples, tau, logit_scale, hard, chunk_size):
    """
    Helper for gumbel_sample_kd to avoid memory requirements by splitting the batch into chunks
    hm: (N, D1, D2, D3, .., Dk), e.g., (N, x, y, z)
    num_samples: int, num samples to draw from each heatmap (N*samples) samples at the end
    tau: float, tau term for gumbel
    logit_scale: scale back logits before log(logits)
    hard: gives one hot softmax prob or not in gumbel.

    Outputs:
        samples: (N, samples, k)
    """
    hm_splits = list(hm.split(chunk_size))
    sample_list = []
    for curr_hm in hm_splits:
        sample_list.append(
            gumbel_sample_kd(curr_hm, num_samples, tau, logit_scale, hard)
        )
    samples = torch.cat(sample_list, dim=0)
    return samples

def fetch_comb_index(num, dev, comb_type):
    if comb_type == "lower":
        comb_idx = torch.LongTensor(
            [(i, j) for i in range(num) for j in range(num) if i < j]
        ).to(dev)
    elif comb_type == "lower_diag":
        comb_idx = torch.LongTensor(
            [(i, j) for i in range(num) for j in range(num) if i <= j]
        ).to(dev)
    elif comb_type == "diag":
        comb_idx = torch.LongTensor(
            [(i, j) for i in range(num) for j in range(num) if i == j]
        ).to(dev)
    elif comb_type == "off_diag":
        comb_idx = torch.LongTensor(
            [(i, j) for i in range(num) for j in range(num) if i != j]
        ).to(dev)
    elif comb_type == "full":
        comb_idx = torch.LongTensor(
            [(i, j) for i in range(num) for j in range(num)]
        ).to(dev)
    else:
        assert False
    return comb_idx

def all_comb(X, Y):
    """
    Returns all possible combinations of elements in X and Y.
    X: (n_x, d_x)
    Y: (n_y, d_y)
    Output: Z: (n_x*x_y, d_x+d_y)
    Example:
    X = tensor([[8, 8, 8],
                [7, 5, 9]])
    Y = tensor([[3, 8, 7, 7],
                [3, 7, 9, 9],
                [6, 4, 3, 7]])
    Z = tensor([[8, 8, 8, 3, 8, 7, 7],
                [8, 8, 8, 3, 7, 9, 9],
                [8, 8, 8, 6, 4, 3, 7],
                [7, 5, 9, 3, 8, 7, 7],
                [7, 5, 9, 3, 7, 9, 9],
                [7, 5, 9, 6, 4, 3, 7]])
    """
    assert len(X.size()) == 2
    assert len(Y.size()) == 2
    X1 = X.unsqueeze(1)
    Y1 = Y.unsqueeze(0)
    X2 = X1.repeat(1, Y.shape[0], 1)
    Y2 = Y1.repeat(X.shape[0], 1, 1)
    Z = torch.cat([X2, Y2], -1)
    Z = Z.view(-1, Z.shape[-1])
    return Z

def toggle_parameters(model, requires_grad):
    """
    Set all weights to requires_grad or not.
    """
    for param in model.parameters():
        param.requires_grad = requires_grad

def detach_tensor(ten):
    """This function move tensor to cpu and convert to numpy"""
    if isinstance(ten, torch.Tensor):
        return ten.cpu().detach().numpy()
    return ten

def count_model_parameters(model):
    """
    Return the amount of parameters that requries gradients.
    """
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

def reset_all_seeds(seed):
    """Reset all seeds for reproduciability."""
    random.seed(seed)
    torch.manual_seed(seed)
    np.random.seed(seed)

def get_activation(name):
    """This function return an activation constructor by name."""
    if name == "tanh":
        return nn.Tanh()
    elif name == "sigmoid":
        return nn.Sigmoid()
    elif name == "relu":
        return nn.ReLU()
    elif name == "selu":
        return nn.SELU()
    elif name == "relu6":
        return nn.ReLU6()
    elif name == "softplus":
        return nn.Softplus()
    elif name == "softshrink":
        return nn.Softshrink()
    else:
        print("Undefined activation: %s" % (name))
        assert False

def stack_ll_tensors(tensor_list_list):
    """
    Recursively stack a list of lists of lists .. whose elements are tensors with the same shape
    """
    if isinstance(tensor_list_list, torch.Tensor):
        return tensor_list_list
    assert isinstance(tensor_list_list, list)
    if isinstance(tensor_list_list[0], torch.Tensor):
        return torch.stack(tensor_list_list)

    stacked_tensor = []
    for tensor_list in tensor_list_list:
        stacked_tensor.append(stack_ll_tensors(tensor_list))
    stacked_tensor = torch.stack(stacked_tensor)
    return stacked_tensor

def get_optim(name):
    """This function return an optimizer constructor by name."""
    if name == "adam":
        return optim.Adam
    elif name == "rmsprop":
        return optim.RMSprop
    elif name == "sgd":
        return optim.SGD
    else:
        print("Undefined optim: %s" % (name))
        assert False

def decay_lr(optimizer, gamma):
    """
    Decay the learning rate by gamma
    """
    assert isinstance(gamma, float)
    assert 0 <= gamma and gamma <= 1.0
    for param_group in optimizer.param_groups:
        param_group["lr"] *= gamma


from __future__ import division, print_function

import logging
from math import pi

from lib.gibindings import Gtk, Gdk

from lib.helpers import clamp

logger = logging.getLogger(__name__)

RADIUS = 2

class CurveWidget(Gtk.DrawingArea):
    """Widget for modifying a (restricted) nonlinear curve.
    """
    __gtype_name__ = 'CurveWidget'
    _SNAP_TO = tuple(float(n)/100 for n in range(0, 105, 5))
    _WHINED_ABOUT_ALPHA = False

    def __init__(self, changed_cb=None, magnetic=True, npoints=None,
                 ylockgroups=()):
        Gtk.DrawingArea.__init__(self)
        self.points = [(0.0, 0.2), (.25, .5), (.75, .75), (1.0, 1.0)]
        self._ylock = {}
        self.ylockgroups = ylockgroups

        self.maxpoints = None
        self._npoints = None
        self.npoints = npoints
        self.grabbed = None
        if changed_cb is None:
            self.changed_cb = lambda *a: None
        else:
            self.changed_cb = changed_cb
        self.magnetic = magnetic

        self.connect("draw", self.draw_cb)

        self.connect("button-press-event", self.button_press_cb)
        self.connect("button-release-event", self.button_release_cb)
        self.connect("motion-notify-event", self.motion_notify_cb)
        self.set_events(Gdk.EventMask.EXPOSURE_MASK |
                        Gdk.EventMask.BUTTON_PRESS_MASK |
                        Gdk.EventMask.BUTTON_RELEASE_MASK |
                        Gdk.EventMask.POINTER_MOTION_MASK
                        )
        self.set_size_request(300, 200)

        self.graypoint = None

    @property
    def npoints(self):
        """If the curve contains a fixed number of points, return that number.

        :return: The point count if the count is fixed, otherwise None
        :rtype: int | None
        """
        return self._npoints

    @npoints.setter
    def npoints(self, n):
        """Set the number of points for a fixed curve, or disable fixedness.

        :param n: The number of points, or None to disable
        :type n: int | None
        """
        self._npoints = n
        self.maxpoints = 64 if not n else n

    @property
    def ylockgroups(self):
        """Get a copy of the y-lock groups used by the curve.

        :return: Dictionary of index-> (i1, i2, ...) associations
        :rtype: dict
        """
        return {k: w for k, w in self._ylock.items()}

    @ylockgroups.setter
    def ylockgroups(self, ylockgroups):
        """Set y-lock groups from a list of index tuples.

        Each tuple should contain a up a set of indices that represent curve
        points that will share the same y-value. When the y-value of one point
        in a group is changed, all other points in that same group are changed.

        :param ylockgroups: List of tuples of indices
        :type ylockgroups: [(int, int, ..)]
        """
        self._ylock.clear()
        for group in ylockgroups:
            for idx in group:
                self._ylock[idx] = tuple(i for i in group if i != idx)

    def eventpoint(self, event_x, event_y):
        width, height = self.get_display_area()
        x, y = event_x, event_y
        x -= RADIUS
        y -= RADIUS
        x = x / width
        y = y / height
        return x, y

    def get_display_area(self):
        alloc = self.get_allocation()
        width, height = alloc.width, alloc.height
        width -= 2*RADIUS
        height -= 2*RADIUS
        width = width / 4 * 4
        height = height / 4 * 4
        return width, height

    def set_point(self, index, value):
        y = value[1]
        self.points[index] = value
        if index in self._ylock:
            for locked_to in self._ylock[index]:
                self.points[locked_to] = (self.points[locked_to][0], y)

    def button_press_cb(self, widget, event):
        if not (self.points or event.button == 1):
            return
        x, y = self.eventpoint(event.x, event.y)

        # Note: Squared distance used for comparisons
        def dist_squared(p):
            return abs(x - p[0])**2 + abs(y - p[1])**2
        points = self.points
        dsq, pos = min((dist_squared(p), i) for i, p in enumerate(points))

        # Unless the number of points are fixed, maxed out, or the intent
        # was to move an existing point, insert a new curve point.
        if not (self.npoints or dsq <= 0.003 or len(points) >= self.maxpoints):
            candidates = [i+1 for i, (px, _) in enumerate(points) if px < x]
            insert_pos = candidates and candidates[-1]
            if insert_pos and insert_pos < len(points):
                points.insert(insert_pos, (x, clamp(y, 0.0, 1.0)))
                pos = insert_pos
                self.queue_draw()

        self.grabbed = pos

    def button_release_cb(self, widget, event):
        if not event.button == 1:
            return
        if self.grabbed:
            if self.points[self.grabbed] is None:
                self.points.pop(self.grabbed)
        self.grabbed = None
        # notify user of the widget
        self.changed_cb(self)

    def motion_notify_cb(self, widget, event):
        if self.grabbed is None:
            return
        x, y = self.eventpoint(event.x, event.y)
        i = self.grabbed
        points = self.points
        # XXX this may fail for non contiguous groups.
        if i in self._ylock:
            i_candidate = None
            if x > points[max(self._ylock[i])][0]:
                i_candidate = max((i,) + self._ylock[i])
            elif x < points[min(self._ylock[i])][0]:
                i_candidate = min((i,) + self._ylock[i])
            if (i_candidate is not None and
                    abs(points[i][0] - points[i_candidate][0]) < 0.001):
                i = i_candidate
        out = False  # by default, points cannot be removed
        if i == len(points) - 1:
            # last point stays right
            left_bound = right_bound = 1.0
        elif i == 0:
            # first point stays left
            left_bound = right_bound = 0.0
        else:
            # other points can be dragged out
            left_bound = points[i-1][0]
            right_bound = points[i+1][0]
            margin = 0.02
            inside_x_bounds = left_bound - margin < x < right_bound + margin
            inside_y_bounds = -0.1 <= y <= 1.1
            out = not (self.npoints or (inside_x_bounds and inside_y_bounds))
        if out:
            points[i] = None
        else:
            y = clamp(y, 0.0, 1.0)
            if self.magnetic:
                x_diff = [abs(x - v) for v in self._SNAP_TO]
                y_diff = [abs(y - v) for v in self._SNAP_TO]
                if min(x_diff) < 0.015 and min(y_diff) < 0.015:
                    y = self._SNAP_TO[y_diff.index(min(y_diff))]
                    x = self._SNAP_TO[x_diff.index(min(x_diff))]
            x = clamp(x, left_bound, right_bound)
            self.set_point(i, (x, y))
        self.queue_draw()

    def draw_cb(self, widget, cr):

        def gdk2rgb(c):
            if c.alpha < 1 and not self.__class__._WHINED_ABOUT_ALPHA:
                logger.warning('The GTK3 style is reporting a color with '
                               'alpha less than 1. This should be harmless, '
                               'but please report any glitches with the curve '
                               'widget. Adwaita is thought not to suffer from '
                               'this problem.')
                self.__class__._WHINED_ABOUT_ALPHA = True
            return c.red, c.green, c.blue
        style = widget.get_style_context()
        state = widget.get_state_flags()
        fg = gdk2rgb(style.get_color(state))

        width, height = self.get_display_area()
        if width <= 0 or height <= 0:
            return

        # 1-pixel width, align lines with pixels
        # (but filled rectangles are off by 0.5px now)
        cr.translate(0.5, 0.5)

        # draw feint grid lines
        cr.set_line_width(0.333)
        cr.set_source_rgb(*fg)
        for i in range(11):
            cr.move_to(RADIUS, i*height/10 + RADIUS)
            cr.line_to(width + RADIUS, i*height/10 + RADIUS)
            cr.move_to(i*width/10 + RADIUS, RADIUS)
            cr.line_to(i*width/10 + RADIUS, height + RADIUS)
            cr.stroke()

        # The graypoint is represented by a dashed vertical line spanning the
        # entire graph, with a gap where a circle marks the vertical position.
        if self.graypoint:
            x1, y1 = self.graypoint
            x1 = int(x1*width) + RADIUS
            y1 = int(y1*height) + RADIUS
            cr.set_line_width(0.5)
            cr.set_dash([height/50.0])
            cr.move_to(x1, RADIUS)
            cr.line_to(x1, y1 - 2 * RADIUS)
            cr.move_to(x1, y1 + 2 * RADIUS)
            cr.line_to(x1, height + RADIUS)
            cr.stroke()
            cr.set_dash([])
            cr.arc(x1, y1, 2 * RADIUS, 0, 2*pi)
            cr.stroke()

        # back to regular weight
        cr.set_line_width(1.0)

        # draw points
        prev_x = prev_y = 0
        for p in self.points:
            if p is None:
                continue
            current_x = int(p[0] * width) + RADIUS
            current_y = int(p[1] * height) + RADIUS

            cr.rectangle(
                current_x-RADIUS-0.5, current_y-RADIUS-0.5,
                2*RADIUS+1, 2*RADIUS+1
            )
            cr.fill()

            # If it's the first point, we won't draw any lines yet
            if p is not self.points[0]:
                cr.move_to(prev_x, prev_y)
                cr.line_to(current_x, current_y)
                cr.stroke()

            prev_x, prev_y = current_x, current_y

        return True

def _test(case=1):
    logging.basicConfig()
    win = Gtk.Window()
    curve = CurveWidget()
    if case == 1:
        curve.ylockgroups = [(1, 2), (3, 4)]
        curve.npoints = 6
        curve.points = [
            (0., 0.), (.2, .5), (.4, .75), (.6, .5), (.8, .3), (1., 1.)
        ]
        curve.graypoint = (0.5, 0.0)
    win.add(curve)
    win.set_title("curve test")
    win.connect("destroy", lambda *a: Gtk.main_quit())
    win.show_all()
    Gtk.main()

if __name__ == '__main__':
    _test()