Homework 7a - Baby Graphics

For this project, you will bring the Baby Names data to life. All parts of HW7 are due Wed Nov 20th at 11:55pm.

Fun background reading: Where Have All The Lisas Gone. This is the article that gave me the idea to create this assignment way back when.

Download babygraphics.zip. From the babygraphics folder, move the babygraphics.py and babydata.py files into your existing babynames folder.

a. main() / Search

The main() code is provided in babygraphics.py. The main() function calls your babynames.read_files() function to read in the names data. The challenge on this assignment is providing an interactive GUI for the baby data. Run the program from the command line in the usual way.

$ python3 babygraphics.py

Without adding any code, running babygraphics.py should load the baby data, and display a largely empty window which waits for you to type something. The provided code takes care of setting up the GUI elements, and detecting when the return-key is typed to call your search and draw functions. Click in the search field, type "arg" and hit return. The provided handle_search() functions calls your babynames.search_names() function, and pastes the result into the window. Those functions were called to print output in the terminal for HW 6a. Here, the GUI code calls the exact same functions, but puts the output in the GUI.

Syntax: here is the key line in handle_search(), showing the syntax that calls your search_names() function from HW6a:

    ...
    # Call the search_names function in babynames.py
    result = babynames.search_names(names, target)
    ...

If your HW6 babynames.py is not working sufficiently to support babygraphics.py, see the Appendix for a workaround so you can still build this GUI.

Milestone a: you can run the program and see the results of searches like "aa" and "arg".

Constants

Here are constants for the use in the babygraphics algorithms.

# Provided constants to load and draw the baby data
FILENAMES = ['baby-1900.txt', 'baby-1910.txt', 'baby-1920.txt', 'baby-1930.txt',
             'baby-1940.txt', 'baby-1950.txt', 'baby-1960.txt', 'baby-1970.txt',
             'baby-1980.txt', 'baby-1990.txt', 'baby-2000.txt', 'baby-2010.txt']
YEARS = [1900, 1910, 1920, 1930, 1940, 1950, 1960, 1970, 1980, 1990, 2000, 2010]
SPACE = 20
COLORS = ['red', 'purple', 'green', 'blue']
TEXT_DX = 2
LINE_WIDTH = 2
MAX_RANK = 1000

b. draw_fixed()

The draw_fixed() function draws the fixed lines and text behind the data lines. It is called once from main() to set up the window initially, and then again whenever the graph is re-drawn. When draw_fixed() runs, the canvas is some width/height in the window. The provided code retrieves those numbers from the canvas for use by the subsequent lines.

Draw the year grid as follows. All of these drawings are in black: The provided constant SPACE=20 defines an empty space which should be reserved at the 4 edges of the canvas. Draw a top horizontal line, SPACE pixels from the top, and starting SPACE pixels from the left edge and ending SPACE pixels before the right edge. Draw a bottom horizontal line SPACE pixels from the bottom edge, and SPACE from the left and right edges. For this project, we will not be picky about +/- 1 pixel coordinates in the drawing.

Here is a diagram of the line spacing for draw_fixed(). The outer edge of the canvas is a shown as a rectangle, with the various lines drawn within it. Each double-arrow marks a distance of SPACE pixels.

In the GUI, the text field takes up the top of the window, and the canvas is a big rectangle below it. Then the search text field is at the bottom of the window below the canvas.

Year Lines

The provided constant YEARS lists the int years to draw. In draw_fixed(), draw a vertical "year" line for each year in YEARS. The first year line should be SPACE pixels from the left canvas edge. The year lines should touch the 2 horizontal lines, spaced out proportionately so each year line gets a roughly equal amount of empty space to its right on the horizontal lines. Vertically, the year lines should extend all the way from the top of the canvas to its bottom.

The trickiest math here is computing the x value for each year. Decompose out a short helper function year_index_x() to compute the x coordinate in the canvas for each year index: 0 (the first year), 1, 2, .... len(YEARS)-1 (the last year). The two functions draw_fixed() and draw_names() need to agree exactly on the x coordinate for each year. By both calling year_index_x(), they are perfectly in sync. Doctests are not required. The vertical lines should be spread evenly across the width (vs. the strategy where all the years have the same int sub_width). The function to draw a black line in TK is:

canvas.create_line(x1, y1, x2, y2)

Note that the TK create_line() and other function truncate coordinates from float to int internally, so you can do your computations as float.

At a point TEXT_DX pixels to the right of the intersection of each vertical line with the lower horizontal line, draw the year string. The TK create_text() function shown below will draw the 'hi' string with its upper left corner at the given x/y. The constant 'tkinter.NW' indicates that the x,y point is at the north-west corner relative to the text.

canvas.create_text(x, y , text='hi', anchor=tkinter.NW)

By default, main() creates a window with a 1000 x 600 canvas in it. Try running main() like this to try different width/height numbers:

$ python3 babygraphics.py 800 400

Your line-drawing math should still look right for different width/height values. Note that if you specify a width of, say, 400, that will be the size of the canvas, but the window may be a wider number since it also needs space to the right of the search text field to display the search results. You should also be able to change temporarily, say, the SPACE constant to a value like 100, and your drawing should use the new value. (SPACE is a good example of a constant - a value which is used in several places. Defining it as a constant makes it easy to change, and the lines of code that use it remain consistent with each other.)

Milestone b: your code can create all the fixed straight lines and year strings and works for various widths and heights.

c. draw_names()

The draw_names() function draws the whole canvas, calling draw_fixed() first to fill in the background. The parameter "lookups" contains a list of names like ['John', 'Jennifer']. The provided code function handle_draw() is configured to be called by TK each time the user hits the return key with typed in names, and it calls your draw_names() function to re-draw the canvas.

For each decade vertical line (1900, 1910), figure out the y value for the name's rank that year. If the rank is 1 (the best possible rank), the y should be at the very top (covering the top horizontal line). If rank is MAX_RANK (1000), the y should be at the very bottom (covering the bottom horizontal line). If a name does not have any data for a particular year, treat it as having the rank MAX_RANK.

Connect the points with lines with a thickness of LINE_WIDTH=2. The TK create_line() function takes named parameters "width" to set the line thickness and "fill" to set the line color, like this:

canvas.create_line(x1, y1, x2, y2, width=LINE_WIDTH, fill=color-str)

TEXT_DX pixels to the right and above each year/rank point, draw the text of the name and its rank number. If a name has no data for a year, use the rank number 1000. The call to canvas.create_text(..) is the same as before, except using the constant 'tkinter.SW'.

The provided constant COLORS is a list of 4 color names. Draw the name 0 line and text with color 0. Draw name 1 with color 1, and so on. When the number of names is greater than the number of colors, wrap around to use the first color again (see the % "mod" operator).

'Jennifer' is a good test, since that name hits both the very bottom and the very top (a rags-to-riches story of baby names!). Here is a Jennifer Lucy graph:

Once it's drawing everything nicely, you've built a nice end to end program - raw data, to dict organization, to GUI drawing. please turn in babygraphics.py on paperless.

Appendix: babydata.py

If your babynames.py is not working sufficiently to support babygraphics.py, there is a workaround. Add an 'import babydata' at the top of your file, and change main() to load the names variable like this:

import babydata

....
def main():
  ...
  names = babydata.names_data()  # use babydata work-around

Instead of using your HW6 parsing, this will use a canned version of the data in the supplied file babydata.py. In this way, HW7 can work even if HW6 has some problems.

If you did not complete the search feature for HW6 or it had bugs, you can implement it in babygraphics.py to get some credit for it. Change the line babynames.search_names(...) in babygraphics.py to call your newly implemented function.