Assignment 5. Drawing!


Due Wednesday, July 28 at 11:59 pm Pacific

  • Submissions received by due date receive a small on-time bonus.
  • All students are granted a pre-approved extension or "grace period" of 48 hours after the due date. Late submissions are accepted during the grace period with no penalty.
  • The grace period expires Fri, Jul 30 at 11:59 pm Pacific, after which we cannot accept further late submissions.
  • In this course, we express all date/times in Pacific time GMT -7. Our Paperless submission system also displays/records due dates and submission times in Pacific time.

Homework 5a - Quilt

For this project, you'll combine decomposition with code that draws lines, ovals etc. to make some neat patterns. We will cover drawing on Friday's lecture, but you are welcome to look at the drawing reference to get started.


Warmups: "parsehw" on Server

This week's warmups are on string while-loop patterns: parsehw.

Here are some notes on the warmup problems, in case the instructions on the experimental server aren't clear:

  • binary1: A binary number is a number made up of just 0s and 1s. You should use an or boolean operator for this problem. Otherwise, it is pretty straightforward.

  • find_binary: An example for this problem is find_binary('xx#b0101xx'). The idea is to return just the binary number that starts with #b, so for this example the function returns #b0101. You should use the find() function to find the index of the #b, first. Then, in a while loop, you walk through the rest of the string to find the 0s and 1s, but stop looping if you get to a character that isn't a 0 or 1. We did examples like this in Wednesday's lecture. Be careful with your boolean logic for the while loop condition! You should use a string slice to return the part of the string that is the binary number (with the #b).

  • find_lol: This is similar to find_binary, but you have to go backwards after you find LOL. Review Wednesday's lecture for examples of how to use a while loop in this way.

  • find_word: You can use two while loops for this one. The first while loop determines the index of the first alphabetic character, and the second while loop determines the index where alpha/digits stop. Be careful to ensure that you don't go off the end of the string (this can be a part of the while loop test).

Turn In Turn In Warmups to Paperless


quilt.zip

To get started, download quilt.zip. Open that .zip file to get a "quilt" folder containing the quilt.py file for this program. Open the quilt folder with the PyCharm open... menu command to get started.


main()

For the quilt program, we provide working main() function. Depending on the command line arguments, main() calls one or another of your functions as shown below.

The quilt drawing is made of three different "patch" drawings: bars, eye and bowtie. Each patch type is drawn by its own function, and these functions are the first three parts of the project.


Overview

Parts (a), (b), and (c) below are going to test your ability to do some arithmetic to determine the dimensions of various lines, ovals, and rectangles. All of your arithmetic should use the parameters passed in to each function – there are no "absolute" coordinates here! Drawing out your thoughts on paper is going to be critical to getting the right solutions without getting too frustrated. You should also test your solutions and if they look incorrect, take the time to figure out what makes them incorrect before trying a new solution. Again: it is very important to understand the arithmetic for these problems!


a. draw_bars()

def draw_bars(canvas, left, top, width, height, n):

This function draws a rectangle and then a series vertical bars (see examples below).

1. Draw a rectangle frame on the passed in canvas with the rectangle's upper left corner at the given left,top and extending the given width and height. Draw the rectangle with color='lightblue'.

2. Draw N black vertical lines on the canvas. The first line should be on the left edge of the patch, covering the left side line of the border rectangle. The last line should be at the right edge, covering the right side line of the border rectangle. All the other lines should be evenly spaced between the 2 sides. The N parameter is guaranteed to be at least 2.

To test that your functions are using parameters properly, main() divides the window into 4 quadrants, and calls your draw_bars() function once to draw in the upper left quadrant, and a second time to draw in the lower right quadrant.

Here is a command line run of the program to draw the bars patch, 300 pixels wide, 200 pixels hight, with N of 10:

$ python3 quilt.py -bars 300 200 10

alt: quilt 10 bars 300x200

Here is a run of the same size, but asking for 17 bars

$ python3 quilt.py -bars 300 200 17

alt: quilt 17 bars 300x200

And here is a run asking for 8 bars with patch size 200x200

$ python3 quilt.py -bars 200 200 8

alt: quilt 8 bars 200x200

Note that if your Python code hits an error, the exception printout will be in the terminal where you launched the program, so be sure to look there if your output is going wrong. Close the graphics window to exit the python program when done.

Divide And Conquer aside: you don't want to type in a big program (or any large project) and then test the whole thing at the end. The best practice is to proceed in smaller pieces — build a part of it, and test that part right away. Then build and test the next part, and so on. CS106A project writeups naturally follow this best practice, talking about project sub-parts, and "milestone" tests of each part as you gradually build up the whole program. These graphics functions do not lend themselves to Doctests, so we are testing them by running them individually and looking at the output.


b. draw_eye()

def draw_eye(canvas, left, top, width, height, n):

1. Draw the lightblue border rectangle, the same as for the bars.

2. Draw a yellow oval, with the upper left of its bounding box at left,top and extending for width,height. The oval should just barely cover up the border rectangle on the four sides.

3. Draw N black lines starting from the center of the oval. Use regular division / to compute the x,y of the center of the oval (recall that the drawing functions work fine with float numbers - if you ask it to draw at x=93.4, it just uses 93). Extend each black line to N points on the bottom edge of the border rectangle, distributed along the edge evenly in the same way as the bars patch.

The following command line calls your draw_eye(), 300 x 200, 10 lines:

$ python3 quilt.py -eye 300 200 10

alt: quilt eye 300x200 10 lines


c. draw_bowtie()

def draw_bowtie(canvas, left, top, width, height, n):

1. Draw the lightblue border rectangle as before.

2. Draw N red lines as follows. N will be 2 or more. The first line is from the upper left corner to the lower right corner of the patch. The next line moves down on the left, and up on the right. Continuing this pattern with the lines evenly spaced until the last line starts from the lower-left corner and goes to the upper right corner. Draw each line in 1 step, from its point on the left edge to its point on the right edge (vs. drawing 2 line segments, each to the middle).

One approach: for each line 0, 1, 2, ... N-1, compute a "y_delta" amount which is the vertical distance from the start point. For the left side, the deltas move down from the upper left corner. For the right side, the deltas move up from the lower right corner.

To draw a line of a specific color, use the color= parameter like this: canvas.draw_line(0, 0, 10, 10, color='red')

The following command line runs draw_bowtie(), 300 x 200, 10 lines:

$ python3 quilt.py -bowtie 300 200 10

alt: quilt bowtie 300x200 10 lines


Milestone: 3x Patches

Build and debug your code until it can draw all 3 patch types correctly for various sizes and values of N.


d. Quilting Bee

The last part uses decomposition to build the whole quilt. See the similar example in lecture.

The draw_quilt() function creates a canvas of the passed in width and height. The quilt will be made of an n-by-n grid of sub-rectangles, each the same size. We'll call the width and height of each sub-rectangle the sub_width and sub_height.

Compute the sub_width and sub_height for the quilt. For example if the overall canvas is 800 pixels wide and N is 9, the sub_width is 88, i.e. 800 divided by 9, rounding down is 88. Use the int division // operator.

Many times we have written a nested y/x loop to go over all the pixels in an image. In this case we want a nested loop over the row/col numbers of the patches, e.g. row=0 is the top row of patches, row=1 is the next row of patches and so on.

    # loop over rows and columns of patches
    for row in range(n):
        for col in range(n):
            # Draw one patch of the grid of patches

What is the x,y pixel coordinate of upper left corner of the patch at row,col? What is its width and height? What do you do to draw a patch there?


Milestone: Eye Test

Inside the row/col loop, call draw_eye() to draw an eye for every patch in the quilt to test that the row/col logic is working. Below is the command line to call your draw_quilt() function, here creating a 500 x 300 canvas with N of 5. That creates 5 x 5 grid of patches, and each patch also uses N of 5 (we're using N for two things here - the number of patches, and the internal N of each patch).

$ python3 quilt.py -quilt 500 300 5

alt: quilt drawing 5 x 5 grid of eye


Patch Rotation

We want a scheme to draw a different patch type in every spot in the grid. There is a standard programming trick for this situation, where we want to cycle through patches like: eye, bars, bowtie, eye, bars, bowtie, eye, ...

Inside the loops, compute a choice int from the row and col numbers like this:

    choice = (row + col) % 3

The % modulo operator divides by 3 and returns the remainder. The result of % n is always in the range 0, 1, .. n-1. In this case choice will cycle through a pattern 0, 1, 2, 0, 1, 2, 0.

Use choice to rotate between the different patch types: 0=eye, 1=bars, 2=bowtie. Change the inside of the row/col loop to draw the right patch type depending on the choice. That is the deliverable state for your quilt program.

Here is the command line to run draw_quilt():

$ python3 quilt.py -quilt 600 400 6

alt: quilt 6 x 6 grid of patches

Here's a quilt with a relatively low value of n=3 giving it a more abstract look. One is reminded of our earlier research in the area of art suitable for hotels.

$ python3 quilt.py -quilt 400 400 3

alt: quilt 3 x 3 grid of patches


All Done

As a final test, try a nice big quilt like this

$ python3 quilt.py -quilt 1200 800 10

quilt-done10.png

When all of the code is working, you should be able to draw a quilt with various sizes and various values of N. This program puts together loops, applied math, and drawing, and leverages decomposition for sub-problems. Save your quilt.py file to turn in with the separate, small HW5b project.


Background

Stanford senior lecturer Julie Zelenski (teaches CS106B) created the original "quilt" assignment, with different quilt-squares each drawn its own function, as a graphical way to play with decomposition. (see the Nifty Assignments archive) Nick Parlante created this version for CS106A-python, adapting the ideas to Python, proportionate drawing, and using the command-line to test milestones.

Homework 5b Data Stripes

This is relatively small project, but with memorable output. For this project you take in a data series of floating point numbers and create visualization of the datas. You have seen many data visualizations where the height of a line or rectangle increases proportionately to represent a value. For this project, your code will use position and computed-color to reflect the underlying data.

Download the data-stripes.zip folder to get started.


Frac Data

We have several interesting data sets for this project, formatted in what we will call "frac" format, geared for this computed-color approach. We'll call each number in the data set a "frac", and the fracs have all been scaled to be in the range -1.0 .. +1.0 inclusive.


draw_stripes() Function

For this project, you need to write code for the draw_stripes() function. The code for main() is already done. The starter code creates a canvas of the requested size. The function takes in a fracs list list [0.5, -0.7, 0.23, ...] and draws a series of colored rectangles on the canvas, one for each frac number, as a way of visualizing the data.

alt: frac data in horizontal series of
rectangles

All the rectangles should be the same int width. Int divide the canvas width by the number of fracs to figure out how wide each rectangle should be.


Red Color

We have the constants BASE and DELTA to feed into the color math as follows.

BASE = 127
DELTA = 127

For each rectangle, we want to figure out a color based on the frac value which is -1.0 .. +1.0. For this milestone, set blue and green to the value BASE, and below we'll discuss computing the value of red, 0..254, based on the value of frac.

We want the red value to vary so that when frac is low, red is low, and when frac is high, red is high. Specifically:

frac is  1.0  -> red is 254 (BASE + DELTA)
frac is  0.0  -> red is 127 (BASE)
frac is -1.0  -> red is 0   (BASE - DELTA)

alt: frac high red high, frac low red
low

In the code, BASE, is the default, neutral value used when the frac value is 0.0. So when frac is 0.0, red should be BASE, aka 127. DELTA is the maximum amount to add or subtract from BASE. When frac is 0.5, red should be BASE + 0.5 * DELTA. Work out code to compute red from frac.


Rect Draw

Use the canvas.fill_rect() function, where the first 4 numbers give the location and size of the rectangle to draw.

canvas.fill_rect(left, top, rect_width, rect_height, color=(r, g, b))

The color= parameter, has a novel feature where it can be assigned to three numbers like this:

canvas.fill_rect(0, 0, 10, 200, color=(200, 127, 127))

The syntax (200, 127, 127) above is a "tuple" in Python, which we will study in more detail later. For now, the tuple simply contains three numbers within parenthesis - a red number, a green number, and a blue number, each must be in the range 0..255 (float values are ok). The fill_rect() function uses those RGB color numbers to set the color of the rectangle.

For this milestone, the blue and green numbers can stay at BASE, while your code computes a red variable based on the frac. The call to fill_rect() would look like:

red = ????
canvas.fill_rect( ...    color=(red, BASE, BASE))

Milestone 1 - Red

For testing we have the following data-test.txt file of frac values from 1.0 to -1.0. The first line of the file is the data set title, and the rest is the floating point values. The provided function read_fracs() reads these numbers into a list.

Test Data Red to Blue
1.0
0.9
0.8
0.7
0.6
0.5
0.4
0.3
0.2
0.1
0.0
-0.1
-0.2
-0.3
-0.4
-0.5
-0.6
-0.7
-0.8
-0.9
-1.0

Try running your program with this data. You should see 21 rectangles. The far left rectangle should be quite red. The middle rectangle should be grayish. Towards the right the rectangles are an indistinct dusky blue/green color. We'll fix them in the next milestone.

$ python3 data-stripes.py data-test.txt

alt:red high at the left

The main() code is provided for this project. It allows you to optionally supply a width and height number of the command line to change the canvas size:

$ python3 data-stripes.py data-test.txt 1200 500

Programming aside: here we've hand-constructed this small data-test.txt file with a very clear pattern in it for testing purposes. If we jumped straight to real data with all its complexities, it would be hard to spot if the code was correct or not. This is also an advantage of text files as a data format - you can just go to your editor and type the data up.


Milestone 2 - Blue

Now add color computation for blue as a function of each frac value. We want blue to move in the opposite direction as red: when frac is high, blue is low; when frac is low, blue is high:

frac is  1.0  -> blue is 0    (BASE - DELTA)
frac is  0.0  -> blue is 127  (BASE)
frac is -1.0  -> blue is 254  (BASE + DELTA)

alt: frac high = red high, blue low

The result is that when frac is high, we get redish stripes since red is high and blue is low. When frac is low, we get blueish stripes, since blue is high and red is low. When frac is near 0.0, the color is grayish, since red, green, and blue will all be around 127 which makes gray.

After drawing the rectangles, call draw_string() like this to draw the title on top of the colored rectangles.

canvas.draw_string(5, 5, title, color='white')

With the blue color computation in, try the data-test.txt data file again.

alt:red high at the left, blue high at the
right

At the left it's bright red, at the right it's bright blue. In the middle it should be grayish.


Real Data

You've got this computed-color visualization machinery. Let's take it for a spin. We'll take a variety of data from the real world and run it through your color-visualization code.


Human Progress is the Trend

Here is an idea to keep in mind when looking at these data sets. Very often the news is depressing. Literally depressing. It's natural for the media to report on injustice and disasters in the world. However, the result is that gradual human progress is underreported. In reality, most measures of human wellbeing have improved hugely over recent decades. Pick a data set - child mortality, starvation, illiteracy .. they are all much improved over the last 50 years.


Child Mortality Data

data-child-mortality.txt - global percentage of children dying before the age of five. The amount of human misery in the left part of this graph is breathtaking. I trimmed the data to start at 1960 as that's when there is data for every year. Data from https://ourworldindata.org/child-mortality

We'll include this graph in the assignment handout, but the rest you should bring to life through your own code. Not many people can say they've built their own numeric-color visualization like this.

alt:red at the left trending to blue at the right


Illiteracy Data

data-illiteracy.txt - global illiteracy rate. https://data.unicef.org/topic/education/literacy/


US Homicides Data

data-homicides.txt - US homicide data. This data has a dramatic bump in it. There was a significant increase in crime in the US from the late 1970's to the early 1990's, peaking in 1991. Since then there has been an equally dramatic decline in crime so now it is historically low. There are many theories about this peak, including the effect of leaded-gasoline on brain development, or the rise and fall of crack cocaine. Data from https://www.kaggle.com/marshallproject/crime-rates/version/1 although in retrospect there were perhaps simpler sources for this data.

Side question: Art reflects life. There must be movies or other works of art that embody the themes of decay and criminality from that historic 1991 crime peak. Perhaps the movie Escape from New York. Or Trainspotting or the show The Wire.


Climate Data

data-climate.txt - this data is quite dramatic. We are a clever and resilient species, and I'm sure we will figure this out eventually. This one looks best with a width of 1200 or more.

Climate scientists measure temperature again a "0" point which is a historical global average temperature. Each year is measured as the "anomaly" from the historical average, so -1.5 C for a cold year or +1.5 C for a hot year. I scaled the climate data to fit in the -1.0 .. +1.0 format, but kept the 0 point intact, so when you see gray years, those were around the long-term average temperature, with red years above average and blue years below. This data set is from https://gmao.gsfc.nasa.gov/reanalysis/MERRA-2/

There will be some whitespace to the right of your graph since the 140 or so rectangles don't divide into the canvas width evenly, leaving some white space at the right.

Terminal protip: The data file names all begin with data-, so in the terminal you can type data-, hit the tab key, and the auto-complete will show you the candidate file names. Then type in 1 more letter and hit tab again to complete the filename. This is why people with a lot of data often name the files according to some pattern, helping to keep things organized and accessible from the command line.


And You're Done

That's a neat bit of real world data visualized with applied math - a spatial dimension of time combined with a dimension in color. Your program should be able to draw any of these frac data sets at various sizes. When your code is all cleaned up and correct, please turn in your quilt.py and your data-stripes.py files on paperless as hw 5.2.


Background

This assignment was created by Nick Parlante in 2019 for the revised Stanford CS106A in Python. The inspiration for the project was this Vox climate stripes article about how the color-stripe representation of climate data was showing up on plates and leggings as a sort of graphic-of-the-moment. The assignment uses the color dimension idea, but feeds through all sorts of data about the world to keep it from being too depressing.