def lower_defensive_box_mark(full_surf = True, rotate = False, rotation_dir = 'ccw'): """ Generate the dataframe for the points that comprise the bounding box of the lower defensive box tick marks as in the court diagram on page 8 of the NBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- lower_defensive_box_mark_dict: a dictionary with the two hash marks that comprise the lower defensive box hash marks """ # The defensive hash marks are 13' (interior) from the end line, is 2" # wide, and 6" long lower_defensive_box_mark_1 = create.rectangle( x_min = -34, x_max = -34 + (2/12), y_min = -5, y_max = -4.5 ) lower_defensive_box_mark_2 = create.rectangle( x_min = -34, x_max = -34 + (2/12), y_min = 4.5, y_max = 5 ) # Reflect the x coordinates over the y axis if full_surf: lower_defensive_box_mark_1 = lower_defensive_box_mark_1.append( transform.reflect(lower_defensive_box_mark_1, over_y = True) ) lower_defensive_box_mark_2 = lower_defensive_box_mark_2.append( transform.reflect(lower_defensive_box_mark_2, over_y = True) ) # Rotate the coordinates if necessary if rotate: lower_defensive_box_mark_1 = transform.rotate( lower_defensive_box_mark_1, rotation_dir ) lower_defensive_box_mark_2 = transform.rotate( lower_defensive_box_mark_2, rotation_dir ) lower_defensive_box_mark_dict = { 'lower_defensive_box_mark_1': lower_defensive_box_mark_1, 'lower_defensive_box_mark_2': lower_defensive_box_mark_2 } return lower_defensive_box_mark_dict
def faceoff_lines( center=(0, 0), full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the face-off spot's hash marks as specified in Rule 1.9 of the NHL rule book Parameters ---------- center: a tuple containing the center coordinates of the spot to be drawn full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- faceoff_line_dict: a dictionary with keys of 'center' and 'faceoff_lines', where 'center' corresponds to the center coordinates of the face-off spot, and 'faceoff_lines' is a pandas dataframe of coordinates needed to plot the hashmarks around a face-off spot """ faceoff_line = pd.DataFrame({ 'x': [-2, -6, -6, -2 - (2 / 12), -2 - (2 / 12), -2, -2], 'y': [.75, .75, .75 + (2 / 12), .75 + (2 / 12), 3.75, 3.75, .75] }) # At each face-off spot, there are four of these lines. Now that one is # created, reflect over the x axis to get a second, then reflect both of # those sets of lines over the y axis to get the remaining two faceoff_line = faceoff_line.append( transform.reflect(faceoff_line, over_y=True)) faceoff_line = faceoff_line.append( transform.reflect(faceoff_line, over_x=True, over_y=True)) faceoff_line = faceoff_line.append( transform.reflect(faceoff_line, over_x=True, over_y=False)) # Move the lines to be in the correct positions around the face-off spot # centers faceoff_line = transform.translate(faceoff_line, translate_x=center[0], translate_y=center[1]) # If the spot is in the neutral zone, there should not be a circle around # it if abs(center[0]) == 20 or center == (0, 0): faceoff_line = pd.DataFrame({'x': [], 'y': []}) # Rotate the coordinates if necessary if rotate: faceoff_line = transform.rotate(faceoff_line, rotation_dir) faceoff_line_dict = {'center': center, 'faceoff_lines': faceoff_line} return faceoff_line_dict
def painted_area(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the bounding box of the free throw lane as specified in the court diagram of the WNBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- painted_areas: a pandas dataframe of the painted areas """ # The interior of the free throw lane is known as the painted area, and # can be a different color than the markings and court. These coordinates # can be used to color them on the plot painted_area = create.rectangle(x_min=-47, x_max=-28 - (2 / 12), y_min=-8 + (2 / 12), y_max=8 - (2 / 12)) # Reflect the x coordinates over the y axis if full_surf: painted_area = painted_area.append( transform.reflect(painted_area, over_y=True)) # Rotate the coordinates if necessary if rotate: painted_area = transform.rotate(painted_area, rotation_dir) return painted_area
def net(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the rings as specified in the court diagram of the WNBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- nets: a pandas dataframe of the nets """ # The ring's center is 15" from the backboard, and 63" from the baseline, # which means it is centered at (+/-41.75, 0). The ring has an interior # diameter of 18", which is where the net is visible from above net = create.circle(center=(-41.75, 0), d=1.5) # Reflect the x coordinates over the y axis if full_surf: net = net.append(transform.reflect(net, over_y=True)) # Rotate the coordinates if necessary if rotate: net = transform.rotate(net, rotation_dir) return net
def division_line(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the bounding box of the division line as in the court diagram on page 8 of the WNBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- division_line: A pandas dataframe of the division line on the court """ # The line's center should be 47' from the interior side of the baselines, # and must be 2" thick division_line = create.rectangle(x_min=-1 / 12, x_max=0, y_min=-25, y_max=25) # Reflect the x coordinates over the y axis if full_surf: division_line = division_line.append( transform.reflect(division_line, over_y=True)) # Rotate the coordinates if necessary if rotate: division_line = transform.rotate(division_line, rotation_dir) return division_line
def blue_line(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the blue line as specified in Rule 1.7 of the NHL rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- blue_line: a pandas dataframe of coordinates needed to plot the blue line """ # The blue line is a 12" wide line that's 25' (interior) from the center # line. It spans the width of the playing surface blue_line = create.rectangle(x_min=-26, x_max=-25, y_min=-42.5, y_max=42.5) # Reflect the x coordinates over the y axis if full_surf: blue_line = blue_line.append(transform.reflect(blue_line, over_y=True)) # Rotate the coordinates if necessary if rotate: blue_line = transform.rotate(blue_line, rotation_dir) return blue_line
def try_line(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the bounding box of the try line as specified in the field diagram of the NCAA rule book (Appendix D) Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- try_line: a pandas dataframe of the goal line """ try_line = create.rectangle(x_min=-141 - (2 / 12), x_max=-141 + (2 / 12), y_min=-1, y_max=1) # Reflect the x coordinates over the y axis if full_surf: try_line = try_line.append(transform.reflect(try_line, over_y=True)) # Rotate the coordinates if necessary if rotate: try_line = transform.rotate(try_line, rotation_dir) return try_line
def backboard(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the backboard as specified in the court diagram of the WNBA rule book Returns ------- backboard: a pandas dataframe of the backboard """ # Per the rule book, the backboard must by 6' wide. The height of the # backboard is irrelevant in this graphic, as this is a bird's eye view # over the court backboard = create.rectangle(x_min=-43 - (4 / 12), x_max=-43, y_min=-3, y_max=3) # Reflect the x coordinates over the y axis if full_surf: backboard = backboard.append(transform.reflect(backboard, over_y=True)) # Rotate the coordinates if necessary if rotate: backboard = transform.rotate(backboard, rotation_dir) return backboard
def court_apron(full_surf = True, rotate = False, rotation_dir = 'ccw'): """ Generate the dataframe for the points that comprise the bounding box of the court apron as in the court diagram on page 8 of the NBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- court_apron: a pandas dataframe of the court border """ # Courts have borders around the outside of the court that are different # in color than the end lines and sidelines. They are usually about 5' on # the sidelines and 8' on the end lines court_apron = pd.DataFrame({ 'x': [ 0, -47 - (2/12), -47 - (2/12), 0, 0, -55 - (4/12), -55 - (4/12), 0, 0 ], 'y': [ -25 - (2/12), -25 - (2/12), 25 + (2/12), 25 + (2/12), 30 + (2/12), 30 + (2/12), -30 - (2/12), -30 - (2/12), -25 - (2/12) ] }) # Reflect the x coordinates over the y axis if full_surf: court_apron = court_apron.append( transform.reflect(court_apron, over_y = True) ) # Rotate the coordinates if necessary if rotate: court_apron = transform.rotate( court_apron, rotation_dir ) return court_apron
def boards(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the boards as specified in Rule 1.3 of the NHL rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- boards: a pandas dataframe of coordinates needed to plot the boards """ boards = pd.DataFrame({ 'x': [0], 'y': [42.5] }).append(create.circle(center=(-72, 14.5), start=.5, end=1, d=56)).append( pd.DataFrame({ 'x': [-100], 'y': [0] })).append(create.circle( center=(-72, -14.5), start=1, end=1.5, d=56)).append( pd.DataFrame({ 'x': [0, 0], 'y': [-42.5, -42.5 - (2 / 12)] })).append( create.circle(center=(-72, -14.5), start=1.5, end=1, d=56 + (4 / 12))).append( pd.DataFrame({ 'x': [-100 - (2 / 12)], 'y': [0] })).append( create.circle( center=(-72, 14.5), start=1, end=.5, d=56 + (4 / 12))).append( pd.DataFrame({ 'x': [0, 0], 'y': [42.5 + (2 / 12), 42.5] })) # Reflect the x coordinates over the y axis if full_surf: boards = boards.append(transform.reflect(boards, over_y=True)) # Rotate the coordinates if necessary if rotate: boards = transform.rotate(boards, rotation_dir) return boards
def outer_center_circle(full_surf = True, rotate = False, rotation_dir = 'ccw'): """ Generate the dataframe for the points that comprise the outer center circle as in the court diagram on page 8 of the NBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- outer_center_circle: A pandas dataframe containing the points that comprise the center circle of the court """ # Draw the outer semicircle at half court. It has an outer radius of 6' # and thickness of 2" outer_center_circle = create.circle( d = 12 - (4/12), start = 1/2, end = 3/2 ).append( pd.DataFrame({ 'x': [0], 'y': [-12] }) ).append( create.circle( d = 12, start = 3/2, end = 1/2 ) ).append( pd.DataFrame({ 'x': [0], 'y': [12] }) ) # Reflect the x coordinates over the y axis if full_surf: outer_center_circle = outer_center_circle.append( transform.reflect(outer_center_circle, over_y = True) ) # Rotate the coordinates if necessary if rotate: outer_center_circle = transform.rotate( outer_center_circle, rotation_dir ) return outer_center_circle
def goal(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the goals as specified in the court diagram of the WNBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- goals: a pandas dataframe of the goals """ # Get the starting angle of the ring. The connector has a width of 5", so # 2.5" are on either side. The ring has a radius of 9", so the arcsine of # these measurements should give the angle at which point they connect start_angle = np.pi - math.asin(2.5 / 9) # The ending angle of the ring would be the negative of the starting angle end_angle = -start_angle # Define the coordinates for the goal goal = pd.DataFrame({ 'x': [-43, -41.75 - ((9 / 12) * math.cos(start_angle))], 'y': [2.5 / 12, 2.5 / 12] }).append( create.circle( center=(-41.75, 0), start=start_angle, end=end_angle, d=1.5 + (4 / 12))).append( pd.DataFrame({ 'x': [-41.75 - ((9 / 12) * math.cos(start_angle)), -43, -43], 'y': [-2.5 / 12, -2.5 / 12, 2.5 / 12] })) # Reflect the x coordinates over the y axis if full_surf: goal = goal.append(transform.reflect(goal, over_y=True)) # Rotate the coordinates if necessary if rotate: goal = transform.rotate(goal, rotation_dir) return goal
def endline_sideline(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the bounding box of the end lines, sidelines, hash marks, and substitution areas as in the court diagram on page 8 of the WNBA rule book, as well as the hash marks on the sides of the court Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- endline_sideline: a pandas dataframe of the end lines, side lines, hash marks, and substitution area markers """ # The endline, sideline, hash marks, and substitution area form the # boundary of the court endline_sideline = pd.DataFrame({ 'x': [ 0, -19 + (2 / 12), -19 + (2 / 12), -19, -19, -47, -47, -47 + (6 / 12), -47 + (6 / 12), -47, -47, -47 + (6 / 12), -47 + (6 / 12), -47, -47, -19, -19, -19 + (2 / 12), -19 + (2 / 12), 0, 0, -4, -4, -4 - (2 / 12), -4 - (2 / 12), -47 - (2 / 12), -47 - (2 / 12), 0, 0 ], 'y': [ -25, -25, -22, -22, -25, -25, -11 - (2 / 12), -11 - (2 / 12), -11, -11, 11, 11, 11 + (2 / 12), 11 + (2 / 12), 25, 25, 22, 22, 25, 25, 25 + (2 / 12), 25 + (2 / 12), 29, 29, 25 + (2 / 12), 25 + (2 / 12), -25 - (2 / 12), -25 - (2 / 12), -25 ] }) # Reflect the x coordinates over the y axis if full_surf: endline_sideline = endline_sideline.append( transform.reflect(endline_sideline, over_y=True)) # Rotate the coordinates if necessary if rotate: endline_sideline = transform.rotate(endline_sideline, rotation_dir) return endline_sideline
def goal_line(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the goal line as specified in Rule 1.7 of the NHL rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- goal_line: a pandas dataframe of coordinates needed to plot the goal line """ # The goal line is a little tricky. It is 11' away from the boards (or 89' # from the center), but follows the curvature of the boards in the corner. # To get the curvature, a similar calculation to that of the face-off spot # interior can be performed theta1 = math.asin((17 - (1 / 12)) / 28) / np.pi theta2 = math.asin((17 + (1 / 12)) / 28) / np.pi goal_line = create.circle(center=(-72, 14.5), start=.5 + theta1, end=.5 + theta2, d=56).append( create.circle(center=(-72, -14.5), start=1.5 - theta2, end=1.5 - theta1, d=56)).append( create.circle( center=(-72, 14.5), start=.5 + theta1, end=.5 + theta2, d=56).iloc[0]) # Reflect the x coordinates over the y axis if full_surf: goal_line = goal_line.append(transform.reflect(goal_line, over_y=True)) # Rotate the coordinates if necessary if rotate: goal_line = transform.rotate(goal_line, rotation_dir) return goal_line
def referee_crease(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the referee's crease as specified in Rule 1.7 of the NHL rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- referee_crease: a pandas dataframe of coordinates needed to plot the referee's crease """ # The referee's crease is located at center ice in front of the penalty # timekeeper's seat. It is a 2" thick, 10' radius semi-circle referee_crease = create.circle( center=(0, -42.5), start=.5, end=1, d=20).append(pd.DataFrame({ 'x': [-10 + (2 / 12)], 'y': [-42.5] })).append( create.circle(center=(0, -42.5), start=1, end=.5, d=20 - (4 / 12))).append( pd.DataFrame({ 'x': [0], 'y': [10] })) # Reflect the x coordinates over the y axis if full_surf: referee_crease = referee_crease.append( transform.reflect(referee_crease, over_y=True)) # Rotate the coordinates if necessary if rotate: referee_crease = transform.rotate(referee_crease, rotation_dir) return referee_crease
def goalkeeper_restricted_area(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the goalkeeper's restricted area as specified in Rule 1.8 of the NHL rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- goalkeeper_restricted_area: a pandas dataframe of coordinates needed to plot the goalkeeper's restricted area """ goalkeeper_restricted_area = pd.DataFrame({ 'x': [ -100, -89 + 1 / 12, -89 + 1 / 12, -100, -100, -89 - (1 / 12), -89 - (1 / 12), -100, -100 ], 'y': [ 14, 11, -11, -14, -14 + (2 / 12), -11 + (2 / 12), 11 - (2 / 12), 14 - (2 / 12), 14 ] }) # Reflect the x coordinates over the y axis if full_surf: goalkeeper_restricted_area = goalkeeper_restricted_area.append( transform.reflect(goalkeeper_restricted_area, over_y=True)) # Rotate the coordinates if necessary if rotate: goalkeeper_restricted_area = transform.rotate( goalkeeper_restricted_area, rotation_dir) return goalkeeper_restricted_area
def endline_sideline(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the bounding box of the end line and sideline as specified in the field diagram of the NCAA rule book (Appendix D) Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- endline_sideline: a pandas dataframe of the goal line """ # The sidelines and end lines must be a solid white border, 6' wide along # the endlines, and 6' wide along the sidelines. The length of the field # is 100 yards, or 360' (interior on the inside of the goal lines), and # the sidelines connect with the end lines 10 yards (30') behind the # interior edge of the goal line. The field is 160' wide (interior) endline_sideline = pd.DataFrame({ 'x': [0, -180, -180, 0, 0, -186, -186, 0, 0], 'y': [-80, -80, 80, 80, 86, 86, -86, -86, -80] }) # Reflect the x coordinates over the y axis if full_surf: endline_sideline = endline_sideline.append( transform.reflect(endline_sideline, over_y=True)) # Rotate the coordinates if necessary if rotate: endline_sideline = transform.rotate(endline_sideline, rotation_dir) return endline_sideline
def goal_line(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the bounding box of the goal line as specified in the field diagram of the NCAA rule book(Appendix D) Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- goal_line: a pandas dataframe of the goal line """ # The interior measurement between goal lines is 100 yards, or 300'. So, # taking the center of the field to be (0, 0), each goal line must be 150' # away from this point. Goal lines have thickness of 8", extending back # into the endzone, and extend across the width of the field (which totals # 160' across) goal_line = create.rectangle(x_min=-150 - (8 / 12), x_max=-150, y_min=-80, y_max=80) # Reflect the x coordinates over the y axis if full_surf: goal_line = goal_line.append(transform.reflect(goal_line, over_y=True)) # Rotate the coordinates if necessary if rotate: goal_line = transform.rotate(goal_line, rotation_dir) return goal_line
def free_throw_circle(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframes for the points that comprise the free-throw circles as specified in page 8 of the WNBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- free_throw_circles_dict: a dictionary with keys 'solid' and 'dash_x', where x is a number that corresponds to a dashed section of the free throw circle """ # Per the WNBA rule book, the solid portion of the circle extends along an # arc of length 12.29" behind the free-throw line. The angle theta must be # calculated to determine where to start. It can be determined via the # relationship s = r*theta, where s is the arc length, r is the radius, and # theta is the angle (in radians) # First, define s to be the arc length in feet s = 12.29 / 12 # The outer radius is 6' r = 6 # Theta is therefore s/r, but since the create.circle() function takes an # angle in radians/pi, this must be divided out as well theta = (s / r) / np.pi # Since the circle must extend theta radians past +/-pi/2, theta must be # added/subtracted from 1/2 accordingly start_angle = (-1 / 2) - theta end_angle = (1 / 2) + theta # The free-throw circle is 6' in diameter from the center of the free-throw # line (exterior) free_throw_circle_solid = create.circle( center=(-28 - (1 / 12), 0), start=start_angle, end=end_angle, d=12).append( create.circle(center=(-28 - (1 / 12), 0), start=end_angle, end=start_angle, d=12 - (4 / 12))).append( create.circle(center=(-28 - (1 / 12), 0), start=start_angle, end=end_angle, d=12).iloc[0]) # Reflect the x coordinates over the y axis if full_surf: free_throw_circle_solid = free_throw_circle_solid.append( transform.reflect(free_throw_circle_solid, over_y=True)) # Rotate the coordinates if necessary if rotate: free_throw_circle_solid = transform.rotate(free_throw_circle_solid, rotation_dir) # The dashed sections of the free-throw circle are all of length 15.5", and # are spaced 15.5" from each other. Following a similar process as above, # the starting and ending angles can be computed # First, compute the arc length in feet s = 15.5 / 12 # The outer radius is 6' r = 6 # Finally, compute the angle traced by the dashed lines theta_dashes = (s / r) / np.pi # This theta must be added to start_angle above to get the starting angle # for each dash, and added twice to get the ending angle for each dash. # This pattern can be repeated, taking the end angle of the previous dash # as the start angle for the following dash dash_1_start_angle = start_angle - theta_dashes dash_1_end_angle = start_angle - (2 * theta_dashes) dash_2_start_angle = dash_1_end_angle - theta_dashes dash_2_end_angle = dash_1_end_angle - (2 * theta_dashes) dash_3_start_angle = dash_2_end_angle - theta_dashes dash_3_end_angle = dash_2_end_angle - (2 * theta_dashes) free_throw_circle_dash_1 = create.circle( center=(-28 - (1 / 12), 0), start=dash_1_start_angle, end=dash_1_end_angle, d=12).append( create.circle(center=(-28 - (1 / 12), 0), start=dash_1_end_angle, end=dash_1_start_angle, d=12 - (4 / 12))).append( create.circle(center=(-28 - (1 / 12), 0), start=dash_1_start_angle, end=dash_1_end_angle, d=12).iloc[0]) free_throw_circle_dash_2 = create.circle( center=(-28 - (1 / 12), 0), start=dash_2_start_angle, end=dash_2_end_angle, d=12).append( create.circle(center=(-28 - (1 / 12), 0), start=dash_2_end_angle, end=dash_2_start_angle, d=12 - (4 / 12))).append( create.circle(center=(-28 - (1 / 12), 0), start=dash_2_start_angle, end=dash_2_end_angle, d=12).iloc[0]) free_throw_circle_dash_3 = create.circle( center=(-28 - (1 / 12), 0), start=dash_3_start_angle, end=dash_3_end_angle, d=12).append( create.circle(center=(-28 - (1 / 12), 0), start=dash_3_end_angle, end=dash_3_start_angle, d=12 - (4 / 12))).append( create.circle(center=(-28 - (1 / 12), 0), start=dash_3_start_angle, end=dash_3_end_angle, d=12).iloc[0]) # The remaining dashes are just the reflections of dashes 1, 2, and 3 over # the x axis free_throw_circle_dash_4 = transform.reflect(free_throw_circle_dash_3, over_x=True, over_y=False) free_throw_circle_dash_5 = transform.reflect(free_throw_circle_dash_2, over_x=True, over_y=False) free_throw_circle_dash_6 = transform.reflect(free_throw_circle_dash_1, over_x=True, over_y=False) # Reflect the x coordinates over the y axis if full_surf: free_throw_circle_dash_1 = free_throw_circle_dash_1.append( transform.reflect(free_throw_circle_dash_1, over_y=True)) free_throw_circle_dash_2 = free_throw_circle_dash_2.append( transform.reflect(free_throw_circle_dash_2, over_y=True)) free_throw_circle_dash_3 = free_throw_circle_dash_3.append( transform.reflect(free_throw_circle_dash_3, over_y=True)) free_throw_circle_dash_4 = free_throw_circle_dash_4.append( transform.reflect(free_throw_circle_dash_4, over_y=True)) free_throw_circle_dash_5 = free_throw_circle_dash_5.append( transform.reflect(free_throw_circle_dash_5, over_y=True)) free_throw_circle_dash_6 = free_throw_circle_dash_6.append( transform.reflect(free_throw_circle_dash_6, over_y=True)) # Rotate the coordinates if necessary if rotate: free_throw_circle_dash_1 = transform.rotate(free_throw_circle_dash_1, rotation_dir) free_throw_circle_dash_2 = transform.rotate(free_throw_circle_dash_2, rotation_dir) free_throw_circle_dash_3 = transform.rotate(free_throw_circle_dash_3, rotation_dir) free_throw_circle_dash_4 = transform.rotate(free_throw_circle_dash_4, rotation_dir) free_throw_circle_dash_5 = transform.rotate(free_throw_circle_dash_5, rotation_dir) free_throw_circle_dash_6 = transform.rotate(free_throw_circle_dash_6, rotation_dir) free_throw_circle_dict = { 'solid': free_throw_circle_solid, 'dash_1': free_throw_circle_dash_1, 'dash_2': free_throw_circle_dash_2, 'dash_3': free_throw_circle_dash_3, 'dash_4': free_throw_circle_dash_4, 'dash_5': free_throw_circle_dash_5, 'dash_6': free_throw_circle_dash_6 } return free_throw_circle_dict
def directional_arrows(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate a dictionary for the points that comprise the directional arrrows that are located at every 10-yard interval as described in the NCAA rule book (Appendix D) Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- directional_arrows_dict: a dictionary of the directional arrows, and the coordinates required to plot them """ # The arrow has two sides of 36", and one side of 18". The Pythagorean # Theorem can be used to determine the height (using half the length of # the base, which in this case is 18") arrow_width = np.sqrt(((36 / 12)**2) - ((9 / 12)**2)) # The arrows only occur every 10 yards, but not at the 50 yard line arrow_yardages = np.arange(-40, 0, 10) # The output dictionary directional_arrows_dict = dict() # Create the arrows for yardage in arrow_yardages: arrow_lower = pd.DataFrame({ 'x': [ # The numbers are 1' from the outer edge of the yard line, # which is 2" wide. The number itself is 4' wide, and the # number is 6" off the outside edge of the number (3 * yardage) - (2 / 12) - 5.5, (3 * yardage) - (2 / 12) - 5.5, (3 * yardage) - (2 / 12) - 5.5 - arrow_width, (3 * yardage) - (2 / 12) - 5.5 ], 'y': [ # The bottom of the numbers must be 12 yards (36') off the # interior of the sideline. The number itself is then 6' tall, # and the top tip of the arrow is 15" below this line -53 - (15 / 12), -53 - (15 / 12) - (18 / 12), -53 - (15 / 12) - (9 / 12), -53 - (15 / 12) ] }) arrow_upper = transform.reflect(arrow_lower, over_x=True, over_y=False) directional_arrows_dict[f'{50 + yardage}_arrow_l'] = arrow_lower directional_arrows_dict[f'{50 + yardage}_arrow_u'] = arrow_upper # Reflect the x coordinates over the y axis if full_surf: for directional_arrow, arrow_coords in directional_arrows_dict.items(): directional_arrows_dict[directional_arrow] = pd.concat([ arrow_coords, pd.DataFrame({ 'x': -1 * arrow_coords['x'], 'y': arrow_coords['y'] }) ]) # Rotate the coordinates if necessary if rotate: for directional_arrow, arrow_coords in directional_arrows_dict.items(): directional_arrows_dict[directional_arrow] = transform.rotate( arrow_coords, rotation_dir) return directional_arrows_dict
def yard_markings(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the bounding box of the line markings at 5-yard intervals as specified in the field diagram of the NCAA rule book (Appendix D) Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- yard_line_dict: a dictionary of the lines of the field, and the coordinates required to plot them """ # The lines are to be placed 8" from the interior of the sidelines, and be # 4" thick. At 5-yard intervals across the field, the lines should stretch # the width of the field, with a 2' long by 4" wide hash 60' from the # interior of each boundary. At 1-yard intervals between these markings at # 5-yard intervals, a 2' tall by 4" wide marker should be placed 4" from # the interior of the sideline as well as 60' from the interior of the # sideline (and extending back towards the sideline). The field is being # constructed from left to right, and the right-side lines can be computed # via reflection, so only the left-side points and half the 50 yard line # are needed yardages = np.arange(-49, 1) yard_line_dict = dict() for yardage in yardages: if yardage == 0: # This is the 50 yard line. Only half the line is needed, since the # other half will be created via reflection yard_line = pd.DataFrame({ 'x': [ # Start 4" from the sideline (3 * yardage) - (2 / 12), # Lower inbound line marker (3 * yardage) - (2 / 12), (3 * yardage) - (2 / 12) - (10 / 12), (3 * yardage) - (2 / 12) - (10 / 12), (3 * yardage) - (2 / 12), # Upper inbound line marker (3 * yardage) - (2 / 12), (3 * yardage) - (2 / 12) - (10 / 12), (3 * yardage) - (2 / 12) - (10 / 12), (3 * yardage) - (2 / 12), # Top (3 * yardage) - (2 / 12), # Crossover 0, # Return to bottom 0, # Return to start (3 * yardage) - (2 / 12) ], 'y': [ # Start 4" from the sideline -80 + (4 / 12), # Lower inbound line marker -20, -20, -20 + (4 / 12), -20 + (4 / 12), # Upper inbound line marker 20 - (4 / 12), 20 - (4 / 12), 20, 20, # Top 80 - (8 / 12), # Crossover 80 - (8 / 12), # Return to bottom -80 + (4 / 12), # Return to start -80 + (4 / 12), ] }) yard_line_dict[f'yard_line_{50 + yardage}'] = yard_line elif yardage % 5 == 0: # At 5-yard intervals, the line should stretch the width of the # field, with the inbound line marker 60 from the interior of # the sideline boundary yard_line = pd.DataFrame({ 'x': [ # Start 4" from the sideline (3 * yardage) - (2 / 12), # Lower inbound line marker (3 * yardage) - (2 / 12), (3 * yardage) - (2 / 12) - (10 / 12), (3 * yardage) - (2 / 12) - (10 / 12), (3 * yardage) - (2 / 12), # Upper inbound line marker (3 * yardage) - (2 / 12), (3 * yardage) - (2 / 12) - (10 / 12), (3 * yardage) - (2 / 12) - (10 / 12), (3 * yardage) - (2 / 12), # Top (3 * yardage) - (2 / 12), # Crossover (3 * yardage) + (2 / 12), # Upper inbound line marker (3 * yardage) + (2 / 12), (3 * yardage) + (2 / 12) + (10 / 12), (3 * yardage) + (2 / 12) + (10 / 12), (3 * yardage) + (2 / 12), # Lower inbound line marker (3 * yardage) + (2 / 12), (3 * yardage) + (2 / 12) + (10 / 12), (3 * yardage) + (2 / 12) + (10 / 12), (3 * yardage) + (2 / 12), # Return to bottom (3 * yardage) + (2 / 12), # Return to start (3 * yardage) - (2 / 12) ], 'y': [ # Start 4" from the sideline -80 + (4 / 12), # Lower inbound line marker -20, -20, -20 + (4 / 12), -20 + (4 / 12), # Upper inbound line marker 20 - (4 / 12), 20 - (4 / 12), 20, 20, # Top 80 - (8 / 12), # Crossover 80 - (8 / 12), # Upper inbound line marker 20, 20, 20 - (4 / 12), 20 - (4 / 12), # Lower inbound line marker -20 + (4 / 12), -20 + (4 / 12), -20, -20, # Return to bottom -80 + (8 / 12), # Return to start -80 + (4 / 12), ] }) yard_line_dict[f'yard_line_{50 + yardage}'] = yard_line else: # At 1-yard intervals, the line should be 2' long. The line should # appear at the bottom (b) and top (t) of the field inside the 6' # wide boundary, and also appear at 70'9" from the nearest boundary # and extending from this point towards that boundary (l and u) yard_line_b = create.rectangle(x_min=(3 * yardage) - (2 / 12), x_max=(3 * yardage) + (2 / 12), y_min=-80 + (4 / 12), y_max=-78 + (4 / 12)) yard_line_t = transform.reflect(yard_line_b, over_x=True, over_y=False) yard_line_l = create.rectangle(x_min=(3 * yardage) - (2 / 12), x_max=(3 * yardage) + (2 / 12), y_min=-22, y_max=-20) yard_line_u = transform.reflect(yard_line_l, over_x=True, over_y=False) yard_line_dict[f'yard_line_{50 + yardage}_b'] = yard_line_b yard_line_dict[f'yard_line_{50 + yardage}_t'] = yard_line_t yard_line_dict[f'yard_line_{50 + yardage}_l'] = yard_line_l yard_line_dict[f'yard_line_{50 + yardage}_u'] = yard_line_u # Reflect the x coordinates over the y axis if full_surf: for yard_line_no, yard_line_coords in yard_line_dict.items(): yard_line_dict[yard_line_no] = pd.concat([ yard_line_coords, pd.DataFrame({ 'x': -1 * yard_line_coords['x'], 'y': yard_line_coords['y'] }) ]) # Rotate the coordinates if necessary if rotate: for yard_line_no, yard_line_coords in yard_line_dict.items(): yard_line_dict[yard_line_no] = transform.rotate( yard_line_coords, rotation_dir) return yard_line_dict
def faceoff_circle( center=(0, 0), full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the face-off circles as specified in Rule 1.9 of the NHL rule book Parameters ---------- center: a tuple containing the center coordinates of the spot to be drawn full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- faceoff_circle_dict: a dictionary with keys of 'center' and 'spot', where 'center' corresponds to the center coordinates of the face-off spot, and 'spot' is a pandas dataframe of coordinates needed to plot a face-off circle """ # The center circle has no external hash marks, so this circle just needs # to be a circle of 2" thickness if center == (0, 0): faceoff_circle = create.circle( center=(0, 0), start=.5, end=1.5, d=30).append( pd.DataFrame({ 'x': [0, 0], 'y': [-15, -15 + (2 / 12)] })).append( create.circle(center=(0, 0), start=1.5, end=.5, d=30 - (4 / 12))).append( pd.DataFrame({ 'x': [0, 0], 'y': [15 - (2 / 12), 15] })) else: # Similar to the method described above, the starting angle to draw the # outer ring can be computed. The hash marks are 5' 11" (71") apart on # the exterior, so taking where this hash mark meets the circle to be # the center, the starting angle is computed as follows theta1 = math.asin((35.5 / 12) / 15) / np.pi # The same process gives the angle to find the point on the interior # of the hash mark, which are 5' 7" (67") apart theta2 = math.asin((33.5 / 12) / 15) / np.pi # Since the hash mark will be plotted on the top of the circle, the # starting angle will be theta + pi/2 faceoff_circle = create.circle( center=(0, 0), start=.5 + theta1, end=1.5 - theta1, d=30).append( pd.DataFrame({ 'x': [ -35.5 / 12, -33.5 / 12, ], 'y': [-17, -17] })).append( create.circle( center=(0, 0), start=1.5 - theta2, end=1.5, d=30)).append( pd.DataFrame({ 'x': [0], 'y': [-15 + (2 / 12)] })).append( create.circle( center=(0, 0), start=1.5, end=.5, d=30 - (4 / 12))).append( pd.DataFrame({ 'x': [0], 'y': [15] })).append( create.circle( center=(0, 0), start=.5, end=.5 + theta2, d=30)).append( pd.DataFrame({ 'x': [ -33.5 / 12, -35.5 / 12, ], 'y': [17, 17] })).append( create.circle( center=(0, 0), start=.5 + theta1, end=1.5 - theta1, d=30).iloc[0]) # If the faceoff circle being drawn is the center circle, and only half # the ice is desired, return the semi-circle that is present if center == (0, 0) and not full_surf: pass # If the spot is in the neutral zone, there should not be a circle around # it elif abs(center[0]) == 20: faceoff_circle = pd.DataFrame({'x': [], 'y': []}) else: # In all other cases, the entire circle should be generated, so the # current points set needs to be reflected over the y axis faceoff_circle = faceoff_circle.append( transform.reflect(faceoff_circle, over_y=True)) faceoff_circle = transform.translate(faceoff_circle, translate_x=center[0], translate_y=center[1]) # Rotate the coordinates if necessary. This is more for consistency # than necessity as a rotated circle is visually the same if rotate: faceoff_circle = transform.rotate(faceoff_circle, rotation_dir) faceoff_circle_dict = {'center': center, 'faceoff_circle': faceoff_circle} return faceoff_circle_dict
def free_throw_lane(full_surf=True, rotate=False, rotation_dir='ccw', include_amateur=False): """ Generate the dataframe for the points that comprise the bounding box of the professional-sized free throw lane as in the court diagram on page 8 of the WNBA rule book. This free-throw lane is required for all teams. This will also generate the free-throw lane for amateur (college) basketball if desired Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- free_throw_lane_dict: a dictionary containing up to two pandas dataframes: one for professional-sized free-throw lanes, and one for amateur (college) """ # The free throw lane goes from the endline inwards a distance of 18' 10" # (interior) and 19' (exterior) with a width of 6' pro_free_throw_lane = pd.DataFrame({ 'x': [-47, -28, -28, -47, -47, -28 - (2 / 12), -28 - (2 / 12), -47, -47], 'y': [ -8, -8, 8, 8, 8 - (2 / 12), 8 - (2 / 12), -8 + (2 / 12), -8 + (2 / 12), -8 ] }) # Reflect the x coordinates over the y axis if full_surf: pro_free_throw_lane = pro_free_throw_lane.append( transform.reflect(pro_free_throw_lane, over_y=True)) # Rotate the coordinates if necessary if rotate: pro_free_throw_lane = transform.rotate(pro_free_throw_lane, rotation_dir) free_throw_lane_dict = {'professional': pro_free_throw_lane} if include_amateur: am_free_throw_lane = pd.DataFrame({ 'x': [ -47, -28, -28, -47, -47, -28 - (2 / 12), -28 - (2 / 12), -47, -47 ], 'y': [ -6, -6, 6, 6, 6 - (2 / 12), 6 - (2 / 12), -6 + (2 / 12), -6 + (2 / 12), -6 ] }) # Reflect the x coordinates over the y axis if full_surf: am_free_throw_lane = am_free_throw_lane.append( transform.reflect(am_free_throw_lane, over_y=True)) # Rotate the coordinates if necessary if rotate: am_free_throw_lane = transform.rotate(am_free_throw_lane, rotation_dir) free_throw_lane_dict['amateur'] = am_free_throw_lane return free_throw_lane_dict
def goal_crease(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the goal crease outline and blue inner area as specified in Rule 1.7 of the NHL rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- goal_crease_dict: a dictionary with keys of 'goal_crease_outline' and 'goal_crease_inner', where 'goal_crease_outline' is a pandas dataframe that corresponds to the coordinates of the red outline of the goal crease, and 'goal_crease_inner' is a pandas dataframe of coordinates of the boundary of the blue interior of the goal crease """ theta = math.asin(4 / 6) / np.pi goal_crease_outline = pd.DataFrame({ 'x': [-89 + (1 / 12), -89 + 4.5 + (1 / 12)], 'y': [4, 4] }).append(create.circle( center=(-89, 0), start=theta, end=-theta, d=12)).append( pd.DataFrame({ 'x': [ -89 + (1 / 12), -89 + (1 / 12), -85 + (1 / 12), -85 + (1 / 12), -85 + (3 / 12), -85 + (3 / 12) ], 'y': [ -4, -4 + (2 / 12), -4 + (2 / 12), -4 + (7 / 12), -4 + (7 / 12), -4 + (2 / 12) ] })).append( create.circle(center=(-89, 0), start=-theta, end=theta, d=12 - (4 / 12))).append( pd.DataFrame({ 'x': [ -85 + (3 / 12), -85 + (3 / 12), -85 + (1 / 12), -85 + (1 / 12), -89 + (1 / 12), -89 + (1 / 12) ], 'y': [ 4 - (2 / 12), 4 - (7 / 12), 4 - (7 / 12), 4 - (2 / 12), 4 - (2 / 12), 4 ] })) goal_crease_inner = pd.DataFrame({ 'x': [ -89 + (1 / 12), -85 + (1 / 12), -85 + (1 / 12), -85 + (3 / 12), -85 + (3 / 12) ], 'y': [ -4 + (2 / 12), -4 + (2 / 12), -4 + (7 / 12), -4 + (7 / 12), -4 + (2 / 12) ] }).append( create.circle(center=(-89, 0), start=-theta, end=theta, d=12 - (4 / 12))).append( pd.DataFrame({ 'x': [ -85 + (3 / 12), -85 + (3 / 12), -85 + (1 / 12), -85 + (1 / 12), -89 + (1 / 12), -89 + (1 / 12) ], 'y': [ 4 - (2 / 12), 4 - (7 / 12), 4 - (7 / 12), 4 - (2 / 12), 4 - (2 / 12), -4 + (2 / 12) ] })) # Reflect the x coordinates over the y axis if full_surf: goal_crease_outline = goal_crease_outline.append( transform.reflect(goal_crease_outline, over_y=True)) goal_crease_inner = goal_crease_inner.append( transform.reflect(goal_crease_inner, over_y=True)) # Rotate the coordinates if necessary if rotate: goal_crease_outline = transform.rotate(goal_crease_outline, rotation_dir) goal_crease_inner = transform.rotate(goal_crease_inner, rotation_dir) goal_crease_dict = { 'goal_crease_outline': goal_crease_outline, 'goal_crease_inner': goal_crease_inner } return goal_crease_dict
def three_point_line(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the three-point line as specified in the WNBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- three_point_lines: a pandas dataframe of the three-point line """ # First, a bit of math is needed to determine the starting and ending # angles of the three-point arc, relative to 0 radians. Since in the end, # the angle is what matters, the units of measure do not. Inches are easier # to use for this calculation. The angle begins 3' from the interior edge # of the sideline start_y = (22 * 12) # The rule book describes the arc as having a radius of 23' 9" radius_outer = (22 * 12) + 1.75 # From here, the calculation is relatively straightforward. To determine # the angle, the inverse sine is needed. It will be multiplied by pi # so that it can be passed to the create_circle() function start_angle_outer = math.asin(start_y / radius_outer) / np.pi end_angle_outer = -start_angle_outer # The same method can be used for the inner angles, however, since the # inner radius will be traced from bottom to top, the angle must be # negative to start radius_inner = (22 * 12) + 1.75 - 2 start_angle_inner = -math.asin((start_y - 2) / radius_inner) / np.pi end_angle_inner = -start_angle_inner # According to the rulebook, the three-point line is 21' 7 7/8" in the # corners three_point_line = pd.DataFrame({ 'x': [-47], 'y': [22] }).append( create.circle(center=(-41.75, 0), d=2 * (radius_outer / 12), start=start_angle_outer, end=end_angle_outer)).append( pd.DataFrame({ 'x': [-47, -47], 'y': [-22, -22 + (2 / 12)] })).append( create.circle(center=(-41.75, 0), d=2 * (radius_inner / 12), start=start_angle_inner, end=end_angle_inner)).append( pd.DataFrame({ 'x': [-47, -47], 'y': [22 - (2 / 12), 22] })) # Reflect the x coordinates over the y axis if full_surf: three_point_line = three_point_line.append( transform.reflect(three_point_line, over_y=True)) # Rotate the coordinates if necessary if rotate: three_point_line = transform.rotate(rotation_dir, three_point_line) return three_point_line
def restricted_area_arc(full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the restricted-area arcs as specified in the WNBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- restricted_area_arcs: a pandas dataframe of the restricted-area arcs """ # Following the same process as for the three-point line, the restricted # area arc's starting and ending angle can be computed start_y = -4 - (2 / 12) # The rule book describes the arc as having a radius of 4' radius_outer = 4 + (2 / 12) # From here, the calculation is relatively straightforward. To determine # the angle, the inverse sine is needed. It will be multiplied by pi # so that it can be passed to the create_circle() function start_angle_outer = math.asin(start_y / radius_outer) / np.pi end_angle_outer = -start_angle_outer # The same method can be used for the inner angles, however, since the # inner radius will be traced from bottom to top, the angle must be # negative to start radius_inner = 4 start_angle_inner = -math.asin((start_y + (2 / 12)) / radius_inner) / np.pi end_angle_inner = -start_angle_inner # The restricted area arc is an arc of radius 4' from the center of the # basket, and extending in a straight line to the front face of the # backboard, and having thickness of 2" restricted_area_arc = pd.DataFrame({ 'x': [-43], 'y': [-4 - (2 / 12)] }).append( create.circle(center=(-41.75, 0), d=8 + (4 / 12), start=start_angle_outer, end=end_angle_outer)).append( pd.DataFrame({ 'x': [-43, -43], 'y': [4 + (2 / 12), 4] })).append( create.circle(center=(-41.75, 0), d=8, start=start_angle_inner, end=end_angle_inner)).append( pd.DataFrame({ 'x': [-43, -43], 'y': [-4, -4 - (2 / 12)] })) # Reflect the x coordinates over the y axis if full_surf: restricted_area_arc = restricted_area_arc.append( transform.reflect(restricted_area_arc, over_y=True)) # Rotate the coordinates if necessary if rotate: restricted_area_arc = transform.rotate(rotation_dir, restricted_area_arc) return restricted_area_arc
def faceoff_spot( center=(0, 0), full_surf=True, rotate=False, rotation_dir='ccw'): """ Generate the dataframe for the points that comprise the face-off spots as specified in Rule 1.9 of the NHL rule book Parameters ---------- center: a tuple containing the center coordinates of the spot to be drawn full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- spot_dict: a dictionary with keys of 'center' and 'spot', where 'center' corresponds to the center coordinates of a face-off spot, and 'spot' is a pandas dataframe of coordinates to plot to make a face-off spot """ # Center faceoff spot if center == (0, 0): # The center face-off spot is 12" in diameter spot = create.circle(center=(0, 0), start=1 / 2, end=3 / 2, d=1) # Reflect the x coordinates over the y axis if full_surf: spot = create.circle(center=(0, 0), start=1 / 2, end=5 / 2, d=1) # Rotate the coordinates if necessary if rotate: spot = transform.rotate(spot, rotation_dir) spot_dict = { 'center': center, 'spot_outer': spot, 'spot_inner': pd.DataFrame({ 'x': [], 'y': [] }) } return spot_dict else: # The non-center face-off spots are 2' in diameter, with a 3" gap # between the top and bottom of the spot and the strip in the center. # First, find the angle at which to start the trace for the interior # of the spot. # The spot has a radius of 1', and a thickness of 2", so the inner # radius is 10". Since there is a 3" gap at theta = 180deg, this # indicates that the stripe's curve starts at x = -7" from the center. # Using trigonometry, the angle can be computed theta = math.asin(7 / 10) / np.pi # To draw this evenly, it's easiest to create two dataframes: one is # the underlying red spot of diameter 2', and the other being the # white inner portion described in the rule book. The starting point # is found using theta calculated above spot_outer = create.circle(center=(0, 0), start=0, end=2, d=2) spot_inner = create.circle(center=(0, 0), start=.5 + theta, end=1.5 - theta, d=2 - (4 / 12)).append( create.circle(center=(0, 0), start=.5 + theta, end=1.5 - theta, d=2 - (4 / 12)).iloc[0]) spot_inner = spot_inner.append( transform.reflect(spot_inner, over_y=True)) # Rotate the coordinates if necessary if rotate: spot_outer = transform.rotate(spot_outer, rotation_dir) spot_inner = transform.rotate(spot_inner, rotation_dir) spot_outer = transform.translate(spot_outer, translate_x=center[0], translate_y=center[1]) spot_inner = transform.translate(spot_inner, translate_x=center[0], translate_y=center[1]) spot_dict = { 'center': center, 'spot_outer': spot_outer, 'spot_inner': spot_inner } return spot_dict
def blocks(full_surf=True, rotate=False, rotation_dir='ccw', include_amateur=False): """ Generate the dataframes for the points that comprise the blocks as specified in page 8 of the WNBA rule book Parameters ---------- full_surf: a bool indicating whether or not this feature is needed for a full-surface representation rotate: a bool indicating whether or not this feature needs to be rotated rotation_dir: a string indicating which direction to rotate the feature Returns ------- blocks_dict: a dictionary with the block numbers as keys and the dataframes to use for plotting as values """ # The first block is 7' (interior) from the endline, and is 2" thick professional_block_1 = create.rectangle(x_min=-40, x_max=-40 + (2 / 12), y_min=-8.5, y_max=-8) # The second block is 8" (interior) from the first block, and is 2" thick professional_block_2 = create.rectangle(x_min=-40 + (10 / 12), x_max=-39, y_min=-8.5, y_max=-8) # The third block is 3' (interior) from the second block, and is 2" thick professional_block_3 = create.rectangle(x_min=-36, x_max=-36 + (2 / 12), y_min=-8.5, y_max=-8) # The fourth block is 3' (interior) from the third block, and is 2" thick professional_block_4 = create.rectangle(x_min=-33 + (2 / 12), x_max=-33 + (4 / 12), y_min=-8.5, y_max=-8) # Block 1 but on the other side of the free-throw lane professional_block_5 = create.rectangle(x_min=-40, x_max=-40 + (2 / 12), y_min=8.5, y_max=8) # Block 2 but on the other side of the free-throw lane professional_block_6 = create.rectangle(x_min=-40 + (10 / 12), x_max=-39, y_min=8.5, y_max=8) # Block 3 but on the other side of the free-throw lane professional_block_7 = create.rectangle(x_min=-36, x_max=-36 + (2 / 12), y_min=8.5, y_max=8) # Block 4 but on the other side of the free-throw lane professional_block_8 = create.rectangle(x_min=-33 + (2 / 12), x_max=-33 + (4 / 12), y_min=8.5, y_max=8) # Reflect the x coordinates over the y axis if full_surf: professional_block_1 = professional_block_1.append( transform.reflect(professional_block_1, over_y=True)) professional_block_2 = professional_block_2.append( transform.reflect(professional_block_2, over_y=True)) professional_block_3 = professional_block_3.append( transform.reflect(professional_block_3, over_y=True)) professional_block_4 = professional_block_4.append( transform.reflect(professional_block_4, over_y=True)) professional_block_5 = professional_block_5.append( transform.reflect(professional_block_5, over_y=True)) professional_block_6 = professional_block_6.append( transform.reflect(professional_block_6, over_y=True)) professional_block_7 = professional_block_7.append( transform.reflect(professional_block_7, over_y=True)) professional_block_8 = professional_block_8.append( transform.reflect(professional_block_8, over_y=True)) # Rotate the coordinates if necessary if rotate: professional_block_1 = transform.rotate(professional_block_1, rotation_dir) professional_block_2 = transform.rotate(professional_block_2, rotation_dir) professional_block_3 = transform.rotate(professional_block_3, rotation_dir) professional_block_4 = transform.rotate(professional_block_4, rotation_dir) professional_block_5 = transform.rotate(professional_block_5, rotation_dir) professional_block_6 = transform.rotate(professional_block_6, rotation_dir) professional_block_7 = transform.rotate(professional_block_7, rotation_dir) professional_block_8 = transform.rotate(professional_block_8, rotation_dir) blocks_dict = { 'professional_block_1': professional_block_1, 'professional_block_2': professional_block_2, 'professional_block_3': professional_block_3, 'professional_block_4': professional_block_4, 'professional_block_5': professional_block_5, 'professional_block_6': professional_block_6, 'professional_block_7': professional_block_7, 'professional_block_8': professional_block_8, } if include_amateur: # The first set of blocks are 7' from the interior of the baseline, and # measure 1' in width amateur_block_1 = create.rectangle(x_min=-40, x_max=-39, y_min=-6 - (8 / 12), y_max=-6) # The second set of blocks are 3' from the first block, and measure 2" # in width amateur_block_2 = create.rectangle(x_min=-36, x_max=-36 + (2 / 12), y_min=-6 - (8 / 12), y_max=-6) # The third set of blocks are 3' from the second block, and measure 2" # in width amateur_block_3 = create.rectangle(x_min=-33 + (2 / 12), x_max=-33 + (4 / 12), y_min=-6 - (8 / 12), y_max=-6) # The fourth set of blocks are 3' from the third block, and measure 2" # in width amateur_block_4 = create.rectangle(x_min=-30 + (4 / 12), x_max=-30 + (6 / 12), y_min=-6 - (8 / 12), y_max=-6) # Block 1 but on the other side of the free-throw lane amateur_block_5 = create.rectangle(x_min=-40, x_max=-39, y_min=6, y_max=6 + (8 / 12)) # Block 2 but on the other side of the free-throw lane amateur_block_6 = create.rectangle(x_min=-36, x_max=-36 + (2 / 12), y_min=6, y_max=6 + (8 / 12)) # Block 3 but on the other side of the free-throw lane amateur_block_7 = create.rectangle(x_min=-33 + (2 / 12), x_max=-33 + (4 / 12), y_min=6, y_max=6 + (8 / 12)) # Block 4 but on the other side of the free-throw lane amateur_block_8 = create.rectangle(x_min=-30 + (4 / 12), x_max=-30 + (6 / 12), y_min=6, y_max=6 + (8 / 12)) # Reflect the x coordinates over the y axis if full_surf: amateur_block_1 = amateur_block_1.append( transform.reflect(amateur_block_1, over_y=True)) amateur_block_2 = amateur_block_2.append( transform.reflect(amateur_block_2, over_y=True)) amateur_block_3 = amateur_block_3.append( transform.reflect(amateur_block_3, over_y=True)) amateur_block_4 = amateur_block_4.append( transform.reflect(amateur_block_4, over_y=True)) amateur_block_5 = amateur_block_5.append( transform.reflect(amateur_block_5, over_y=True)) amateur_block_6 = amateur_block_6.append( transform.reflect(amateur_block_6, over_y=True)) amateur_block_7 = amateur_block_7.append( transform.reflect(amateur_block_7, over_y=True)) amateur_block_8 = amateur_block_8.append( transform.reflect(amateur_block_8, over_y=True)) # Rotate the coordinates if necessary if rotate: amateur_block_1 = transform.rotate(amateur_block_1, rotation_dir) amateur_block_2 = transform.rotate(amateur_block_2, rotation_dir) amateur_block_3 = transform.rotate(amateur_block_3, rotation_dir) amateur_block_4 = transform.rotate(amateur_block_4, rotation_dir) amateur_block_5 = transform.rotate(amateur_block_5, rotation_dir) amateur_block_6 = transform.rotate(amateur_block_6, rotation_dir) amateur_block_7 = transform.rotate(amateur_block_7, rotation_dir) amateur_block_8 = transform.rotate(amateur_block_8, rotation_dir) blocks_dict['amateur_block_1'] = amateur_block_1 blocks_dict['amateur_block_2'] = amateur_block_2 blocks_dict['amateur_block_3'] = amateur_block_3 blocks_dict['amateur_block_4'] = amateur_block_4 blocks_dict['amateur_block_5'] = amateur_block_5 blocks_dict['amateur_block_6'] = amateur_block_6 blocks_dict['amateur_block_7'] = amateur_block_7 blocks_dict['amateur_block_8'] = amateur_block_8 return blocks_dict