Make the Spline UI widgets more widgety

Change-Id: I89d9ad7b795cef716f6da55414175acbf49c1855
Signed-off-by: Ravago Jones <ravagojones@gmail.com>
diff --git a/frc971/control_loops/python/graph.py b/frc971/control_loops/python/graph.py
index 3063bd1..1b83713 100644
--- a/frc971/control_loops/python/graph.py
+++ b/frc971/control_loops/python/graph.py
@@ -1,209 +1,55 @@
-from constants import *
-import cairo
-from color import Color, palette
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk
+import numpy as np
 from points import Points
-from drawing_constants import *
 from libspline import Spline, DistanceSpline, Trajectory
 
-AXIS_MARGIN_SPACE = 40
+from matplotlib.backends.backend_gtk3agg import (FigureCanvasGTK3Agg as
+                                                 FigureCanvas)
+from matplotlib.figure import Figure
 
 
-class Graph():  # (TODO): Remove Computer Calculation
-    def __init__(self, cr, mypoints):
-        # Background Box
-        set_color(cr, palette["WHITE"])
-        cr.rectangle(-1.0 * SCREEN_SIZE, -0.5 * SCREEN_SIZE, SCREEN_SIZE,
-                     SCREEN_SIZE * 0.6)
-        cr.fill()
+class Graph(Gtk.Bin):
+    def __init__(self):
+        super(Graph, self).__init__()
+        fig = Figure(figsize=(5, 4), dpi=100)
+        self.axis = fig.add_subplot(111)
+        canvas = FigureCanvas(fig)  # a Gtk.DrawingArea
+        canvas.set_vexpand(True)
+        canvas.set_size_request(800, 250)
+        self.add(canvas)
 
-        cr.set_source_rgb(0, 0, 0)
-        cr.rectangle(-1.0 * SCREEN_SIZE, -0.5 * SCREEN_SIZE, SCREEN_SIZE,
-                     SCREEN_SIZE * 0.6)
-        #Axis
-        cr.move_to(-1.0 * SCREEN_SIZE + AXIS_MARGIN_SPACE,
-                   -0.5 * SCREEN_SIZE + AXIS_MARGIN_SPACE)  # Y
-        cr.line_to(-1.0 * SCREEN_SIZE + AXIS_MARGIN_SPACE,
-                   0.1 * SCREEN_SIZE - 10)
+    def recalculate_graph(self, points):
+        if not points.getLibsplines(): return
 
-        cr.move_to(-1.0 * SCREEN_SIZE + AXIS_MARGIN_SPACE,
-                   -0.5 * SCREEN_SIZE + AXIS_MARGIN_SPACE)  # X
-        cr.line_to(-10, -0.5 * SCREEN_SIZE + AXIS_MARGIN_SPACE)
-        cr.stroke()
+        # set the size of a timestep
+        dt = 0.00505
 
-        skip = 2
-        dT = 0.00505
-        start = AXIS_MARGIN_SPACE - SCREEN_SIZE
-        end = -2.0 * AXIS_MARGIN_SPACE
-        height = 0.5 * (SCREEN_SIZE) - AXIS_MARGIN_SPACE
-        zero = AXIS_MARGIN_SPACE - SCREEN_SIZE / 2.0
-        legend_entries = {}
-        if mypoints.getLibsplines():
-            distanceSpline = DistanceSpline(mypoints.getLibsplines())
-            traj = Trajectory(distanceSpline)
-            mypoints.addConstraintsToTrajectory(traj)
-            traj.Plan()
-            XVA = traj.GetPlanXVA(dT)
-            if XVA is not None:
-                self.draw_x_axis(cr, start, height, zero, XVA, end)
-                self.drawVelocity(cr, XVA, start, height, skip, zero, end,
-                                  legend_entries)
-                self.drawAcceleration(cr, XVA, start, height, skip, zero,
-                                      AXIS_MARGIN_SPACE, end, legend_entries)
-                self.drawVoltage(cr, XVA, start, height, skip, traj, zero, end,
-                                 legend_entries)
-                cr.set_source_rgb(0, 0, 0)
-                cr.move_to(-1.0 * AXIS_MARGIN_SPACE, zero + height / 2.0)
-                cr.line_to(AXIS_MARGIN_SPACE - SCREEN_SIZE,
-                           zero + height / 2.0)
-                self.drawLegend(cr, XVA, start, height, skip, zero, end,
-                                legend_entries)
-        cr.stroke()
+        # call C++ wrappers to calculate the trajectory
+        distanceSpline = DistanceSpline(points.getLibsplines())
+        traj = Trajectory(distanceSpline)
+        points.addConstraintsToTrajectory(traj)
+        traj.Plan()
+        XVA = traj.GetPlanXVA(dt)
 
-    def connectLines(self, cr, points, color):
-        for i in range(0, len(points) - 1):
-            set_color(cr, color)
-            cr.move_to(points[i][0], points[i][1])
-            cr.line_to(points[i + 1][0], points[i + 1][1])
-            cr.stroke()
+        # extract values to be graphed
+        total_steps_taken = XVA.shape[1]
+        total_time = dt * total_steps_taken
+        time = np.arange(total_time, step=dt)
+        position, velocity, acceleration = XVA
+        left_voltage, right_voltage = zip(*(traj.Voltage(x) for x in position))
 
-    def draw_x_axis(self, cr, start, height, zero, xva, end):
-        total_time = 0.00505 * len(xva[0])
-        for k in np.linspace(0, 1, 11):
-            self.tickMark(cr,
-                          k * np.abs(start - end) + start, zero + height / 2.0,
-                          10, palette["BLACK"])
-            cr.move_to(k * np.abs(start - end) + start,
-                       10 + zero + height / 2.0)
-            txt_scale = SCREEN_SIZE / 1000.0
-            display_text(cr, str(round(k * total_time, 3)), txt_scale,
-                         txt_scale, 1.0 / txt_scale, 1.0 / txt_scale)
-            cr.stroke()
+        # update graph
+        self.axis.clear()
+        self.axis.plot(time, velocity)
+        self.axis.plot(time, acceleration)
+        self.axis.plot(time, left_voltage)
+        self.axis.plot(time, right_voltage)
+        self.axis.legend(
+            ["Velocity", "Acceleration", "Left Voltage", "Right Voltage"])
+        self.axis.xaxis.set_label_text("Time (sec)")
 
-    def tickMark(self, cr, x, y, height, COLOR):
-        # X, Y is in the middle of the tick mark
-        set_color(cr, COLOR)
-        cr.move_to(x, y + (height / 2))
-        cr.line_to(x, y - (height / 2))
-        cr.stroke()
-
-    def HtickMark(self, cr, x, y, width, COLOR):
-        # X, Y is in the middle of the tick mark
-        set_color(cr, COLOR)
-        cr.move_to(x + (width / 2), y)
-        cr.line_to(x - (width / 2), y)
-        cr.stroke()
-
-    def drawLegend(self, cr, xva, start, height, skip, zero, end,
-                   legend_entries):
-        step_size = (end - start) / len(legend_entries)
-        margin_under_x_axis = height * 0.1
-        for index, (name, color) in enumerate(legend_entries.items()):
-            set_color(cr, color)
-            cr.move_to(start + index * step_size, zero - margin_under_x_axis)
-            txt_scale = SCREEN_SIZE / 900.0
-            display_text(cr, name, txt_scale, txt_scale, 1.0 / txt_scale,
-                         1.0 / txt_scale)
-
-    def drawVelocity(self, cr, xva, start, height, skip, zero, end,
-                     legend_entries):
-        COLOR = palette["RED"]
-        velocity = xva[1]
-        n_timesteps = len(velocity)
-        max_v = np.amax(velocity)
-        spacing = np.abs(start - end) / float(n_timesteps)
-        scaler = height / max_v
-        cr.set_source_rgb(1, 0, 0)
-        points = []
-        for i in range(0, len(velocity)):
-            if i % skip == 0:
-                points.append([
-                    start + (i * spacing),
-                    zero + height / 2.0 + (velocity[i] * scaler / 2.0)
-                ])
-        self.connectLines(cr, points, COLOR)
-
-        # draw axes marking
-        for i in np.linspace(-1, 1, 11):
-            self.HtickMark(cr, start, zero + i * height / 2.0 + height / 2.0,
-                           10, palette["BLACK"])
-            cr.set_source_rgb(1, 0, 0)
-            cr.move_to(start + 5, zero + i * height / 2.0 + height / 2.0)
-            txt_scale = SCREEN_SIZE / 1000.0
-            display_text(cr, str(round(i * max_v, 2)), txt_scale, txt_scale,
-                         1.0 / txt_scale, 1.0 / txt_scale)
-            cr.stroke()
-
-        # add entry to legend
-        legend_entries["Velocity"] = COLOR
-
-    def drawAcceleration(self, cr, xva, start, height, skip, zero, margin, end,
-                         legend_entries):
-        COLOR = palette["BLUE"]
-        accel = xva[2]
-        max_a = np.amax(accel)
-        min_a = np.amin(accel)
-        n_timesteps = len(accel)
-        spacing = np.abs(start - end) / float(n_timesteps)
-        scaler = height / (max_a - min_a)
-        cr.set_source_rgb(1, 0, 0)
-        points = []
-        for i in range(0, len(accel)):
-            if i % skip == 0:
-                points.append([
-                    start + (i * spacing), zero + ((accel[i] - min_a) * scaler)
-                ])
-        self.connectLines(cr, points, COLOR)
-
-        # draw axes marking
-        for i in np.linspace(0, 1, 11):
-            self.HtickMark(cr, -1.5 * margin, zero + i * height, 10,
-                           palette["BLACK"])
-            cr.set_source_rgb(0, 0, 1)
-            cr.move_to(-1.2 * margin, zero + i * height)
-            txt_scale = SCREEN_SIZE / 1000.0
-            display_text(cr, str(round(i * (max_a - min_a) + min_a,
-                                       2)), txt_scale, txt_scale,
-                         1.0 / txt_scale, 1.0 / txt_scale)
-            cr.stroke()
-
-        # draw legend
-        legend_entries["Acceleration"] = COLOR
-
-    def drawVoltage(self, cr, xva, start, height, skip, traj, zero, end,
-                    legend_entries):
-        COLOR_LEFT = palette["GREEN"]
-        COLOR_RIGHT = palette["CYAN"]
-        poses = xva[0]
-        n_timesteps = len(poses)
-        spacing = np.abs(start - end) / float(n_timesteps)
-        points_left = []
-        points_right = []
-        for i in range(0, len(poses)):
-            if i % skip == 0:
-                # libspline says the order is left-right
-                voltage = traj.Voltage(poses[i])
-                points_left.append([
-                    start + (i * spacing),
-                    zero + height / 2 + height * (voltage[0] / 24.0)
-                ])
-                points_right.append([
-                    start + (i * spacing),
-                    zero + height / 2 + height * (voltage[1] / 24.0)
-                ])
-        self.connectLines(cr, points_left, COLOR_LEFT)
-        self.connectLines(cr, points_right, COLOR_RIGHT)
-
-        for i in np.linspace(-1, 1, 7):
-            self.HtickMark(cr, -1.0 * SCREEN_SIZE,
-                           zero + i * height / 2.0 + height / 2.0, 10,
-                           palette["BLACK"])
-            cr.set_source_rgb(0, 1, 1)
-            cr.move_to(-1.0 * SCREEN_SIZE,
-                       zero + i * height / 2.0 + height / 2.0)
-            txt_scale = SCREEN_SIZE / 1000.0
-            display_text(cr, str(round(i * 12.0, 2)), txt_scale, txt_scale,
-                         1.0 / txt_scale, 1.0 / txt_scale)
-            cr.stroke()
-
-        legend_entries["Left Voltage"] = COLOR_LEFT
-        legend_entries["Right Voltage"] = COLOR_RIGHT
+        # renumber the x-axis to include the last point,
+        # the total time to drive the spline
+        self.axis.xaxis.set_ticks(np.linspace(0, total_time, num=8))
diff --git a/frc971/control_loops/python/path_edit.py b/frc971/control_loops/python/path_edit.py
index 6ddcb1c..d402593 100755
--- a/frc971/control_loops/python/path_edit.py
+++ b/frc971/control_loops/python/path_edit.py
@@ -2,58 +2,51 @@
 from __future__ import print_function
 import os
 import sys
-import copy
-from color import Color, palette
-import random
+from color import palette
+from graph import Graph
 import gi
 import numpy as np
-import scipy.spatial.distance
 gi.require_version('Gtk', '3.0')
 gi.require_version('Gdk', '3.0')
 from gi.repository import Gdk, Gtk, GLib
 import cairo
-from libspline import Spline, DistanceSpline, Trajectory
+from libspline import Spline
 import enum
 import json
-from basic_window import *
 from constants import *
-from drawing_constants import *
+from drawing_constants import set_color, draw_px_cross, draw_px_x, display_text, draw_control_points
 from points import Points
-from graph import Graph
+import time
 
 
 class Mode(enum.Enum):
     kViewing = 0
     kPlacing = 1
     kEditing = 2
-    kExporting = 3
-    kImporting = 4
 
 
-class GTK_Widget(BaseWindow):
+class FieldWidget(Gtk.DrawingArea):
     """Create a GTK+ widget on which we will draw using Cairo"""
 
     def __init__(self):
-        super(GTK_Widget, self).__init__()
+        super(FieldWidget, self).__init__()
+        self.set_size_request(mToPx(FIELD.width), mToPx(FIELD.length))
 
         self.points = Points()
+        self.graph = Graph()
+        self.set_vexpand(True)
+        self.set_hexpand(True)
 
         # init field drawing
         # add default spline for testing purposes
         # init editing / viewing modes and pointer location
         self.mode = Mode.kPlacing
-        self.x = 0
-        self.y = 0
-        module_path = os.path.dirname(os.path.realpath(sys.argv[0]))
-        self.path_to_export = os.path.join(module_path,
+        self.mousex = 0
+        self.mousey = 0
+        self.module_path = os.path.dirname(os.path.realpath(sys.argv[0]))
+        self.path_to_export = os.path.join(self.module_path,
                                            'points_for_pathedit.json')
 
-        # update list of control points
-        self.point_selected = False
-        # self.adding_spline = False
-        self.index_of_selected = -1
-        self.new_point = []
-
         # For the editing mode
         self.index_of_edit = -1  # Can't be zero beause array starts at 0
         self.held_x = 0
@@ -62,59 +55,12 @@
         self.curves = []
 
         try:
-            self.field_png = cairo.ImageSurface.create_from_png("frc971/control_loops/python/field_images/" + FIELD.field_id + ".png")
+            self.field_png = cairo.ImageSurface.create_from_png(
+                "frc971/control_loops/python/field_images/" + FIELD.field_id +
+                ".png")
         except cairo.Error:
             self.field_png = None
 
-        self.colors = []
-
-        for c in palette:
-            self.colors.append(palette[c])
-
-        self.reinit_extents()
-
-        self.inStart = None
-        self.inEnd = None
-        self.inValue = None
-        self.startSet = False
-
-        self.module_path = os.path.dirname(os.path.realpath(sys.argv[0]))
-
-    """set extents on images"""
-
-    def reinit_extents(self):
-        self.extents_x_min = -1.0 * SCREEN_SIZE
-        self.extents_x_max = SCREEN_SIZE
-        self.extents_y_min = -1.0 * SCREEN_SIZE
-        self.extents_y_max = SCREEN_SIZE
-
-    # this needs to be rewritten with numpy, i dont think this ought to have
-    # SciPy as a dependecy
-    def get_index_of_nearest_point(self):
-        cur_p = [[self.x, self.y]]
-        distances = scipy.spatial.distance.cdist(cur_p, self.all_controls)
-
-        return np.argmin(distances)
-
-    # return the closest point to the loc of the click event
-    def get_nearest_point(self):
-        return self.all_controls[self.get_index_of_nearest_point()]
-
-    def draw_field_elements(self, cr):
-        if FIELD.year == 2019:
-            draw_HAB(cr)
-            draw_rockets(cr)
-            draw_cargo_ship(cr)
-        elif FIELD.year == 2020:
-            set_color(cr, palette["BLACK"])
-            markers(cr)
-            draw_shield_generator(cr)
-            draw_trench_run(cr)
-            draw_init_lines(cr)
-            draw_control_panel(cr)
-        elif FIELD.year == 2021:
-            draw_at_home_grid(cr)
-
     def draw_robot_at_point(self, cr, i, p, spline):
         p1 = [mToPx(spline.Point(i)[0]), mToPx(spline.Point(i)[1])]
         p2 = [mToPx(spline.Point(i + p)[0]), mToPx(spline.Point(i + p)[1])]
@@ -123,8 +69,10 @@
         distance = np.sqrt((p2[1] - p1[1])**2 + (p2[0] - p1[0])**2)
         x_difference_o = p2[0] - p1[0]
         y_difference_o = p2[1] - p1[1]
-        x_difference = x_difference_o * mToPx(FIELD.robot.length / 2) / distance
-        y_difference = y_difference_o * mToPx(FIELD.robot.length / 2) / distance
+        x_difference = x_difference_o * mToPx(
+            FIELD.robot.length / 2) / distance
+        y_difference = y_difference_o * mToPx(
+            FIELD.robot.length / 2) / distance
 
         front_middle = []
         front_middle.append(p1[0] + x_difference)
@@ -212,41 +160,23 @@
         cr.stroke()
         cr.set_source_rgba(0, 0, 0, 1)
 
-    def handle_draw(self, cr):  # main
-        # Fill the background color of the window with grey
-        set_color(cr, palette["WHITE"])
-        cr.paint()
+    def do_draw(self, cr):  # main
 
-        # Draw a extents rectangle
-        set_color(cr, palette["WHITE"])
-        cr.rectangle(self.extents_x_min, self.extents_y_min,
-                     (self.extents_x_max - self.extents_x_min),
-                     self.extents_y_max - self.extents_y_min)
-        cr.fill()
-
-        cr.move_to(0, 50)
-        cr.show_text('Press "e" to export')
-        cr.show_text('Press "i" to import')
+        start_time = time.perf_counter()
 
         cr.save()
         set_color(cr, palette["BLACK"])
 
-        if FIELD.year == 2019:  # half field
-            cr.rectangle(0, -SCREEN_SIZE / 2, SCREEN_SIZE, SCREEN_SIZE)
-        else:  # full field
-            cr.translate(mToPx(FIELD.width) / 2.0, 0.0)
-            cr.rectangle(-mToPx(FIELD.width) / 2.0, -mToPx(FIELD.length) / 2.0,
-                         mToPx(FIELD.width), mToPx(FIELD.length))
+        cr.rectangle(0, 0, mToPx(FIELD.width), mToPx(FIELD.length))
         cr.set_line_join(cairo.LINE_JOIN_ROUND)
         cr.stroke()
 
         if self.field_png:
             cr.save()
-            cr.translate(-mToPx(FIELD.width) / 2, -mToPx(FIELD.length) / 2)
             cr.scale(
-                    mToPx(FIELD.width) / self.field_png.get_width(),
-                    mToPx(FIELD.length) / self.field_png.get_height(),
-                    )
+                mToPx(FIELD.width) / self.field_png.get_width(),
+                mToPx(FIELD.length) / self.field_png.get_height(),
+            )
             cr.set_source_surface(self.field_png)
             cr.paint()
             cr.restore()
@@ -255,7 +185,6 @@
 
         if self.mode == Mode.kPlacing or self.mode == Mode.kViewing:
             set_color(cr, palette["BLACK"])
-            cr.move_to(-SCREEN_SIZE, 170)
             plotPoints = self.points.getPoints()
             if plotPoints:
                 for i, point in enumerate(plotPoints):
@@ -302,9 +231,12 @@
 
         cr.paint_with_alpha(0.2)
 
-        draw_px_cross(cr, self.x, self.y, 10)
+        draw_px_cross(cr, self.mousex, self.mousey, 10)
         cr.restore()
-        mygraph = Graph(cr, self.points)
+        if self.points.getLibsplines():
+            self.graph.recalculate_graph(self.points)
+
+        print("spent {:.2f} ms drawing the field widget".format(1000 * (time.perf_counter() - start_time)))
 
     def draw_splines(self, cr):
         holder_spline = []
@@ -332,17 +264,18 @@
         self.curves.append(holder_spline)
 
     def mouse_move(self, event):
-        old_x = self.x
-        old_y = self.y
-        self.x = event.x - mToPx(FIELD.width / 2.0)
-        self.y = event.y
-        dif_x = self.x - old_x
-        dif_y = self.y - old_y
+        old_x = self.mousex
+        old_y = self.mousey
+        self.mousex = event.x
+        self.mousey = event.y
+        dif_x = self.mousex - old_x
+        dif_y = self.mousey - old_y
         difs = np.array([pxToM(dif_x), pxToM(dif_y)])
 
         if self.mode == Mode.kEditing:
-            self.points.updates_for_mouse_move(
-                self.index_of_edit, self.spline_edit, self.x, self.y, difs)
+            self.points.updates_for_mouse_move(self.index_of_edit,
+                                               self.spline_edit, self.mousex,
+                                               self.mousey, difs)
 
     def export_json(self, file_name):
         self.path_to_export = os.path.join(
@@ -351,16 +284,12 @@
             get_json_folder(FIELD),  # path from the root
             file_name  # selected file
         )
-        if file_name[-5:] != ".json":
-            print("Error: Filename doesn't end in .json")
-        else:
-            # Will export to json file
-            self.mode = Mode.kEditing
 
-            multi_spline = self.points.toMultiSpline()
-            print(multi_spline)
-            with open(self.path_to_export, mode='w') as points_file:
-                json.dump(multi_spline, points_file)
+        # Will export to json file
+        multi_spline = self.points.toMultiSpline()
+        print(multi_spline)
+        with open(self.path_to_export, mode='w') as points_file:
+            json.dump(multi_spline, points_file)
 
     def import_json(self, file_name):
         self.path_to_export = os.path.join(
@@ -370,47 +299,37 @@
             file_name  # selected file
         )
 
-        if file_name[-5:] != ".json":
-            print("Error: Filename doesn't end in .json")
-        else:
-            # import from json file
-            self.mode = Mode.kEditing
-            print("LOADING LOAD FROM " + file_name)  # Load takes a few seconds
-            with open(self.path_to_export) as points_file:
-                multi_spline = json.load(points_file)
+        # import from json file
+        print("LOADING LOAD FROM " + file_name)  # Load takes a few seconds
+        with open(self.path_to_export) as points_file:
+            multi_spline = json.load(points_file)
 
-            # if people messed with the spline json,
-            # it might not be the right length
-            # so give them a nice error message
-            try:  # try to salvage as many segments of the spline as possible
-                self.points.fromMultiSpline(multi_spline)
-            except IndexError:
-                # check if they're both 6+5*(k-1) long
-                expected_length = 6 + 5 * (multi_spline["spline_count"] - 1)
-                x_len = len(multi_spline["spline_x"])
-                y_len = len(multi_spline["spline_x"])
-                if x_len is not expected_length:
-                    print(
-                        "Error: spline x values were not the expected length; expected {} got {}"
-                        .format(expected_length, x_len))
-                elif y_len is not expected_length:
-                    print(
-                        "Error: spline y values were not the expected length; expected {} got {}"
-                        .format(expected_length, y_len))
+        # if people messed with the spline json,
+        # it might not be the right length
+        # so give them a nice error message
+        try:  # try to salvage as many segments of the spline as possible
+            self.points.fromMultiSpline(multi_spline)
+        except IndexError:
+            # check if they're both 6+5*(k-1) long
+            expected_length = 6 + 5 * (multi_spline["spline_count"] - 1)
+            x_len = len(multi_spline["spline_x"])
+            y_len = len(multi_spline["spline_x"])
+            if x_len is not expected_length:
+                print(
+                    "Error: spline x values were not the expected length; expected {} got {}"
+                    .format(expected_length, x_len))
+            elif y_len is not expected_length:
+                print(
+                    "Error: spline y values were not the expected length; expected {} got {}"
+                    .format(expected_length, y_len))
 
-            print("SPLINES LOADED")
+        print("SPLINES LOADED")
+        self.mode = Mode.kEditing
 
-    def do_key_press(self, event, file_name):
+    def key_press(self, event, file_name):
         keyval = Gdk.keyval_to_lower(event.keyval)
-        if keyval == Gdk.KEY_q:
-            print("Found q key and exiting.")
-            quit_main_loop()
-        if keyval == Gdk.KEY_e:
-            export_json(file_name)
 
-        if keyval == Gdk.KEY_i:
-            import_json(file_name)
-
+        # TODO: This should be a button
         if keyval == Gdk.KEY_p:
             self.mode = Mode.kPlacing
             # F0 = A1
@@ -423,16 +342,19 @@
                 self.points.getSplines()[len(self.points.getSplines()) - 1][4],
                 self.points.getSplines()[len(self.points.getSplines()) - 1][3])
 
-    def button_press_action(self):
+    def button_press(self, event):
+        self.mousex = event.x
+        self.mousey = event.y
+
         if self.mode == Mode.kPlacing:
-            if self.points.add_point(self.x, self.y):
+            if self.points.add_point(self.mousex, self.mousey):
                 self.mode = Mode.kEditing
         elif self.mode == Mode.kEditing:
             # Now after index_of_edit is not -1, the point is selected, so
             # user can click for new point
-            if self.index_of_edit > -1 and self.held_x != self.x:
+            if self.index_of_edit > -1 and self.held_x != self.mousex:
                 self.points.setSplines(self.spline_edit, self.index_of_edit,
-                                       pxToM(self.x), pxToM(self.y))
+                                       pxToM(self.mousex), pxToM(self.mousey))
 
                 self.points.splineExtrapolate(self.spline_edit)
 
@@ -442,7 +364,7 @@
                 # Get clicked point
                 # Find nearest
                 # Move nearest to clicked
-                cur_p = [pxToM(self.x), pxToM(self.y)]
+                cur_p = [pxToM(self.mousex), pxToM(self.mousey)]
                 # Get the distance between each for x and y
                 # Save the index of the point closest
                 nearest = 1  # Max distance away a the selected point can be in meters
@@ -459,10 +381,4 @@
                             print("Index: " + str(index_of_closest))
                             self.index_of_edit = index_of_closest
                             self.spline_edit = index_splines
-                            self.held_x = self.x
-
-    def do_button_press(self, event):
-        # Be consistent with the scaling in the drawing_area
-        self.x = event.x * 2 - mToPx(FIELD.width / 2.0)
-        self.y = event.y * 2
-        self.button_press_action()
+                            self.held_x = self.mousex
diff --git a/frc971/control_loops/python/spline_graph.py b/frc971/control_loops/python/spline_graph.py
index 347837b..ad3ae96 100755
--- a/frc971/control_loops/python/spline_graph.py
+++ b/frc971/control_loops/python/spline_graph.py
@@ -1,12 +1,16 @@
 #!/usr/bin/python3
+
+# matplotlib overrides fontconfig locations, so has to be imported before gtk
+import matplotlib
 import gi
-from path_edit import *
-import numpy as np
+from path_edit import FieldWidget
+from basic_window import RunApp
+from constants import FIELDS, FIELD, SCREEN_SIZE
 gi.require_version('Gtk', '3.0')
 from gi.repository import Gdk, Gtk, GLib
 import cairo
 import basic_window
-
+import os
 
 class GridWindow(Gtk.Window):
     def method_connect(self, event, cb):
@@ -16,99 +20,68 @@
         self.connect(event, handler)
 
     def mouse_move(self, event):
-        # Changes event.x and event.y to be relative to the center.
-        x = event.x - self.drawing_area.window_shape[0] / 2
-        y = self.drawing_area.window_shape[1] / 2 - event.y
-        scale = self.drawing_area.get_current_scale()
-        event.x = x / scale + self.drawing_area.center[0]
-        event.y = y / scale + self.drawing_area.center[1]
-        self.drawing_area.mouse_move(event)
+        self.field.mouse_move(event)
         self.queue_draw()
 
     def button_press(self, event):
-        original_x = event.x
-        original_y = event.y
-        x = event.x - self.drawing_area.window_shape[0] / 2
-        y = self.drawing_area.window_shape[1] / 2 - event.y
-        scale = 2 * self.drawing_area.get_current_scale()
-        event.x = x / scale + self.drawing_area.center[0]
-        event.y = y / scale + self.drawing_area.center[1]
-        self.drawing_area.do_button_press(event)
-        event.x = original_x
-        event.y = original_y
+        self.field.button_press(event)
 
     def key_press(self, event):
-        self.drawing_area.do_key_press(event, self.file_name_box.get_text())
+        self.field.key_press(event, self.file_name_box.get_text())
         self.queue_draw()
 
     def configure(self, event):
-        self.drawing_area.window_shape = (event.width, event.height)
+        self.field.window_shape = (event.width, event.height)
 
     def output_json_clicked(self, button):
-        print("OUTPUT JSON CLICKED")
-        self.drawing_area.export_json(self.file_name_box.get_text())
+        self.field.export_json(self.file_name_box.get_text())
 
     def input_json_clicked(self, button):
-        print("INPUT JSON CLICKED")
-        self.drawing_area.import_json(self.file_name_box.get_text())
+        self.field.import_json(self.file_name_box.get_text())
         self.long_input.set_value(
-            self.drawing_area.points.getConstraint(
-                "LONGITUDINAL_ACCELERATION"))
+            self.field.points.getConstraint("LONGITUDINAL_ACCELERATION"))
         self.lat_input.set_value(
-            self.drawing_area.points.getConstraint("LATERAL_ACCELERATION"))
-        self.vol_input.set_value(
-            self.drawing_area.points.getConstraint("VOLTAGE"))
+            self.field.points.getConstraint("LATERAL_ACCELERATION"))
+        self.vol_input.set_value(self.field.points.getConstraint("VOLTAGE"))
 
     def long_changed(self, button):
         value = self.long_input.get_value()
-        self.drawing_area.points.setConstraint("LONGITUDINAL_ACCELERATION",
-                                               value)
+        self.field.points.setConstraint("LONGITUDINAL_ACCELERATION", value)
 
     def lat_changed(self, button):
         value = self.lat_input.get_value()
-        self.drawing_area.points.setConstraint("LATERAL_ACCELERATION", value)
+        self.field.points.setConstraint("LATERAL_ACCELERATION", value)
 
     def vel_changed(self, button):
         value = self.vel_input.get_value()
 
     def vol_changed(self, button):
         value = self.vol_input.get_value()
-        self.drawing_area.points.setConstraint("VOLTAGE", value)
+        self.field.points.setConstraint("VOLTAGE", value)
 
     def input_combobox_choice(self, combo):
         text = combo.get_active_text()
         if text is not None:
             print("Combo Clicked on: " + text)
-            set_field(text)
+            #set_field(text)
 
     def __init__(self):
         Gtk.Window.__init__(self)
 
         self.set_default_size(1.5 * SCREEN_SIZE, SCREEN_SIZE)
 
-        flowBox = Gtk.FlowBox()
-        flowBox.set_valign(Gtk.Align.START)
-        flowBox.set_selection_mode(Gtk.SelectionMode.NONE)
-
-        flowBox.set_valign(Gtk.Align.START)
-
-        self.add(flowBox)
-
-        container = Gtk.Fixed()
-        flowBox.add(container)
+        container = Gtk.Grid()
+        container.set_vexpand(True)
+        self.add(container)
 
         self.eventBox = Gtk.EventBox()
-        container.add(self.eventBox)
-
         self.eventBox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK
                                  | Gdk.EventMask.BUTTON_RELEASE_MASK
                                  | Gdk.EventMask.POINTER_MOTION_MASK
                                  | Gdk.EventMask.SCROLL_MASK
                                  | Gdk.EventMask.KEY_PRESS_MASK)
 
-        #add the graph box
-        self.drawing_area = GTK_Widget()
-        self.eventBox.add(self.drawing_area)
+        self.field = FieldWidget()
 
         self.method_connect("delete-event", basic_window.quit_main_loop)
         self.method_connect("key-release-event", self.key_press)
@@ -118,12 +91,9 @@
 
         self.file_name_box = Gtk.Entry()
         self.file_name_box.set_size_request(200, 40)
-
         self.file_name_box.set_text(FIELD.field_id + ".json")
         self.file_name_box.set_editable(True)
 
-        container.put(self.file_name_box, 0, 0)
-
         self.long_input = Gtk.SpinButton()
         self.long_input.set_size_request(100, 20)
         self.long_input.set_numeric(True)
@@ -134,8 +104,7 @@
         self.long_label = Gtk.Label()
         self.long_label.set_text("Longitudinal Acceleration Restriction")
         self.long_input.set_value(
-            self.drawing_area.points.getConstraint(
-                "LONGITUDINAL_ACCELERATION"))
+            self.field.points.getConstraint("LONGITUDINAL_ACCELERATION"))
 
         self.lat_input = Gtk.SpinButton()
         self.lat_input.set_size_request(100, 20)
@@ -147,7 +116,7 @@
         self.lat_label = Gtk.Label()
         self.lat_label.set_text("Lateral Acceleration Restriction")
         self.lat_input.set_value(
-            self.drawing_area.points.getConstraint("LATERAL_ACCELERATION"))
+            self.field.points.getConstraint("LATERAL_ACCELERATION"))
 
         self.vel_input = Gtk.SpinButton()
         self.vel_input.set_size_request(100, 20)
@@ -170,19 +139,9 @@
         self.vol_input.connect("value-changed", self.vol_changed)
         self.vol_label = Gtk.Label()
         self.vol_label.set_text("Voltage Restriction")
-        self.vol_input.set_value(
-            self.drawing_area.points.getConstraint("VOLTAGE"))
+        self.vol_input.set_value(self.field.points.getConstraint("VOLTAGE"))
 
-        container.put(self.long_input, 0, 60)
-        container.put(self.lat_input, 0, 110)
-        container.put(self.vel_input, 0, 160)
-        container.put(self.vol_input, 0, 210)
-        container.put(self.long_label, 0, 40)
-        container.put(self.lat_label, 0, 90)
-        container.put(self.vel_label, 0, 140)
-        container.put(self.vol_label, 0, 190)
-
-        self.output_json = Gtk.Button.new_with_label("Output")
+        self.output_json = Gtk.Button.new_with_label("Export")
         self.output_json.set_size_request(100, 40)
         self.output_json.connect("clicked", self.output_json_clicked)
 
@@ -190,40 +149,57 @@
         self.input_json.set_size_request(100, 40)
         self.input_json.connect("clicked", self.input_json_clicked)
 
-        container.put(self.output_json, 210, 0)
-        container.put(self.input_json, 320, 0)
-
-
         #Dropdown feature
-        self.label = Gtk.Label("Change Map:")
-        self.label.set_size_request(100,40)
-        container.put(self.label,430,0)
+        self.label = Gtk.Label()
+        self.label.set_text("Change Field:")
+        self.label.set_size_request(100, 40)
 
         game_store = Gtk.ListStore(str)
-        games = [
-           "2020 Field",
-           "2019 Field",
-           "2021 Galactic Search ARed",
-           "2021 Galactic Search ABlue",
-           "2021 Galactic Search BRed",
-           "2021 Galactic Search BBlue",
-           "2021 AutoNav Barrel Racing",
-           "2021 AutoNav Slalom",
-           "2021 AutoNav Bounce",
-           ]
 
         self.game_combo = Gtk.ComboBoxText()
         self.game_combo.set_entry_text_column(0)
         self.game_combo.connect("changed", self.input_combobox_choice)
 
-        for game in games:
-          self.game_combo.append_text(game)
+        for game in FIELDS.keys():
+            self.game_combo.append_text(game)
 
         self.game_combo.set_active(0)
-        self.game_combo.set_size_request(100,40)
-        container.put(self.game_combo,440,30)
+        self.game_combo.set_size_request(100, 40)
+
+        limitControls = Gtk.FlowBox()
+        limitControls.set_min_children_per_line(1)
+        limitControls.set_max_children_per_line(2)
+        limitControls.add(self.long_label)
+        limitControls.add(self.long_input)
+
+        limitControls.add(self.lat_label)
+        limitControls.add(self.lat_input)
+
+        limitControls.add(self.vel_label)
+        limitControls.add(self.vel_input)
+
+        limitControls.add(self.vol_label)
+        limitControls.add(self.vol_input)
+
+        container.attach(limitControls, 5, 1, 1, 1)
+
+        jsonControls = Gtk.FlowBox()
+        jsonControls.set_min_children_per_line(3)
+        jsonControls.add(self.file_name_box)
+        jsonControls.add(self.output_json)
+        jsonControls.add(self.input_json)
+        container.attach(jsonControls, 1, 0, 1, 1)
+
+        container.attach(self.label, 4, 0, 1, 1)
+        container.attach(self.game_combo, 5, 0, 1, 1)
+
+        self.eventBox.add(self.field)
+        container.attach(self.eventBox, 1, 1, 4, 4)
+
+        container.attach(self.field.graph, 0, 10, 10, 1)
 
         self.show_all()
 
+
 window = GridWindow()
 RunApp()