# Notebook from UIUC-iSchool-DataViz/spring2019online
Path: week06/.ipynb_checkpoints/prep_notebook_week06-checkpoint.ipynb

# Activity #1 - intro to traitlets and object oriented programming
* So first, we'll do the relatively dry stuff - playing with some concepts of object oriented programming_____no_output_____

# it is possible you will have to install this
import traitlets_____no_output_____# ok, lets make a simple object class
class MyObject(traitlets.HasTraits):
    # lets use the unicode trait to let our object have a name
    name = traitlets.Unicode("unnamed")
    # lets also give our object an age
    age = traitlets.Int(0)_____no_output_____# now, lets create an object we can manipulate out of this class
my_obj = MyObject()
my_obj_____no_output_____# so far, nothing is defined yet and the object has the
#  class's defaults
my_obj.name, my_obj.age_____no_output_____# lets create another object
# and here we can show how it can
#  be created and initialized
obj2 = MyObject(name = "Bob", age = 47)
obj2.name, obj2.age_____no_output_____# now lets create a function that can act on
#  one of the variables of our object
#  in particluar, its going to change the name in
#  my object
def name_changed(change):
    print(change['new']) # this is a format that is required of a traitlets class

# lets tell traitlets that we are going to change something
my_obj.observe(name_changed, ['name'])_____no_output_____my_obj.name = "Linda"Linda
# note: right now this seems overly complicated
#  why would we do this?  can't I just simply
# assign the name to the object's name??
#  whats with this whole function name_changed
#  and .observe?

# so, we can actually associate actions with changes
# Behold:
def name_changed2(change):
    print("I just changed '%s' to '%s'" % (change['old'], change['new']))

# lets apply this to our other object we created
obj2.name_____no_output_____obj2.observe(name_changed2, ['name'])_____no_output_____obj2.name = "Gene"I just changed 'Bob' to 'Gene'
# we can even take a look at the nitty gritty
# details of how this change is taking place
# "under the hood" with traitlets

# lets create a new object
obj3 = MyObject()_____no_output_____# and now, instead of specifying an action
#  with our change, lets look at what
# change really is:
def trait_change(change):
    print("Change dictionary:")
    print(change)

obj3.observe(trait_change)_____no_output_____obj3.name = "Something Else"
# so we can see that there are these weird "new" and "old" names
#  this is tracking how things change
#  the "owner" is just pointint to obj3 (of class MyObject)_____no_output_____# lets try another one:
obj3.age = 10Change dictionary:
{'owner': <__main__.MyObject object at 0x10ecfdc10>, 'new': 10, 'old': 0, 'name': 'age', 'type': 'change'}

### An aside: string printing_____no_output_____

# just as an aside, there are fancy ways to print out strings
#  and numbers in one line

# here is one example
"String formatting is sticking stuff %s the middle: %0.5f" % ("in", 10.135)
# so "in" is placed at %s and 10.135 is placed at %0.5f_____no_output_____# we can also insert strings as place holder numbers:
"{2} nicer way {0} to do this, {1}".format("is", "see", "The")_____no_output_____

### Take aways
* this was just a quick overview of traitlets
* traitlets are another way to use interactivity in python - we can create classes and then track changes to those classes with our "change" functions_____no_output_____# Activity #2 - More details of ipywidgets
* We've already done a LOT of things with ipywidgets
* we'll get into some more of the nitty gritty details of different things we can use with widgets now_____no_output_____

# import, per usual
import ipywidgets

# so, this is how we've used widgets before for the most part
@ipywidgets.interact(name = ['Linda', 'Tina', 'Louise'])
def print_name(name):
    print(name) # just a simple print out_____no_output_____# but now, lets go into ipywidgets in a bit more detail
# and look at specific functions

# for example, we can create a little display
# that increments integer numbers
itext = ipywidgets.IntText()_____no_output_____# to be able to display our widget
from IPython.display import display
# note: depending on the version of jupyter
#  you are using, you may or maynot have
#  to use "display" to display things
#  we'll do it both ways, starting
#  with display

# lets show the thing!
display(itext)
# you can see there are little numbers at the
#  end that we can toggle up and down_____no_output_____# note that if I make another display of itext,
# this one and the previous one have values that are
# tied together
display(itext)_____no_output_____# also, the value of itext is then stored - so
#  we could in theory generate a toggle and then
# do stuff with its value
itext.value
# note if I go up and change the toggle value
#  I have to re-run this cell to print out the
# newly stored value_____no_output_____# I can also set the value "by hand"
itext.value = 10
# once I run this cell, now the toggle values are
# updated above_____no_output_____# we can also do fun things like create progress bars
ip = ipywidgets.IntProgress()_____no_output_____display(ip)_____no_output_____# so, its probably hard to see, but there is indeed a
# bar there
# I can show this better by setting the progress
# to 90% by hand:
ip.value = 90_____no_output_____# we can also check out more of the interer counters,
#  like this nifty slider
irange = ipywidgets.IntRangeSlider(min = -10, max = 10, step = 1)_____no_output_____display(irange)_____no_output_____# again, the value of the slider is stored
#  if we want to use it
irange.value_____no_output_____

### BY THEIR POWERS COMBINED!!_____no_output_____

# we can also mix traitlets & ipython widgets
#  in neat ways
#  here, we'll use the ipywidgets function
#  link to associate 2 different things

# a new instance of my class as m
m = MyObject(name = "Bob", age = 47)
# lets link an widget label with a traitlet class value
l = ipywidgets.Label()

# lets link the traitelet name with the
# widget label, l
ipywidgets.link( (m, "name"), (l, "value") )_____no_output_____# now if we display this widget label
#  we see the traitlet object's name
display(l)_____no_output_____# we can even update the traitlet name & the
# widget label updates as well
m.name = "Linda"_____no_output_____

### More ipywidgets fun_____no_output_____

# we can use ipywidgets to make clickers
button1 = ipywidgets.Button(description = "I am a Clicker")_____no_output_____display(button1)
# we note that nothing happens when we press this button_____no_output_____# now, lets make a function that can act when we press
# this button
def say_click(event):
    print("I have clicked. Click.")_____no_output_____# now we can tell our button that we've created to
# say click when we click
button1.on_click(say_click)
# we'll note that this is "back reactive" - so now
# if we go up to our displayed button and press click
# it says it has clicked were before it did nothing_____no_output_____# we can use ipywidgets to create text boxes
ta1 = ipywidgets.Textarea("Hi, this is a box of text. (1)")

ta2 = ipywidgets.Textarea("Hi, this is a box of text. (2)")

ta3 = ipywidgets.Textarea("Hi, this is a box of text. (3)")

ta4 = ipywidgets.Textarea("Hi, this is a box of text. (4)")_____no_output_____# first we'll just display the first one
display(ta1)_____no_output_____# we can organize these text boxes in a few ways
#  one way is to use ipywidgets to display things
#  in tabular form:
tabs = ipywidgets.Tab([ta1, ta2, ta3, ta4])_____no_output_____display(tabs)_____no_output_____# or we can "accordian" them:
acc = ipywidgets.Accordion([ta1, ta2, ta3, ta4])_____no_output_____display(acc)_____no_output_____# or we can organize them into horizontal boxes
ipywidgets.HBox([ta1, ta2, ta3, ta4])
# note this means a few of them are off screen so
# this might not be the best option_____no_output_____# or we can organize them in a vertical manner
ipywidgets.VBox([ta1, ta2, ta3, ta4])_____no_output_____# or, if we want to be super fancy we
# can organize them in a verticle box of
# sets of horizontal boxes
ipywidgets.VBox( [ipywidgets.HBox([ta1, ta2]),
                  ipywidgets.HBox([ta3, ta4])] )_____no_output_____# we can even insert a label in our array of
# horizontal boxes and see how ipywidgets would
#  place this text
ipywidgets.VBox( [ipywidgets.HBox([ta1, ta2]),
                  ipywidgets.Label("Hello there!"),
                  ipywidgets.HBox([ta3, ta4])] )_____no_output_____

* So, those were just some toy examples
* Lets see how we can create something interactive with some of these tools that will update a variable based on the interactivity that we have built_____no_output_____

# lets start by making a progress bar again:
ip = ipywidgets.IntProgress()
# now, lets add in a button that will add 10
button_plus = ipywidgets.Button(description = "+10")
# and one that will subtract 10
button_minus = ipywidgets.Button(description = "-10")

# lets see how this looks in a horizontal box row:
ipywidgets.HBox([button_minus, ip, button_plus])
# we note if we click these, nothing happens
#  this is because we haven't associated actions
#  to our clicks_____no_output_____# so, no matter what we press, we don't see anything happening
ip.value_____no_output_____# so, lets now associate a change in the value of our
# progress bar when we click the down button:
def click_down(event):
    ip.value -= 10

# and lets tie this change in value to
# the click with the "on_click" function of our down button
button_minus.on_click(click_down)_____no_output_____# same type of thing, but for our up button
def click_up(event):
    ip.value += 10
button_plus.on_click(click_up)_____no_output_____# lets try this again:
ipywidgets.HBox([button_minus, ip, button_plus])
# note also again that these associations with
# these up and down click functions are in a sense
# "back reactive" - so now our previous instance of this
# clicker and progress bar updates as well!_____no_output_____

* ok, lets make some more fun linking examples_____no_output_____

# lets first make a slider
islider = ipywidgets.IntSlider(min = 0, max = 10, step = 1, orientation = 'vertical')_____no_output_____# lets give this slider a base color that
# is sort of purple-y, using a hex code
islider.style.handle_color = "#750075"_____no_output_____islider # note here (and above with our boxes) I'm not using "display"
# note this slider slides up and down,
# nothing too exciting_____no_output_____# lets create a new widget object called a color picker:
cp = ipywidgets.ColorPicker()
cp
# when we show this we can click on the little
# box and it popus up a color picker we can mess around with_____no_output_____# now, lets link the slider and the color picker
ipywidgets.link( (cp, 'value'), (islider.style, 'handle_color'))_____no_output_____ipywidgets.VBox([cp, islider])
# note how previous instances of these
# objects are tied together
#  also note how the .link function
# sort of intuitievly knows how to link these
#  two interactive widgets together_____no_output_____# just a few more fun examples and then we'll move
# on to bqplot

# we can create a date picker:
ipywidgets.DatePicker()_____no_output_____# we can also make a widget that does allows
#  us to "press play" or interact with
# playable things like music or animation
ipywidgets.Play()
# of course, we haven't linked any functions to
# it now, but lets try that_____no_output_____# as a quick example, lets link an integer slider to
# our play button

# first, lets define the range of our play button
play = ipywidgets.Play(interval = 50, value = 50, min = 1, max = 100, step = 1, description = "Press Play")
# so, we start at 50, and create min and max value around that

# now, lets create our slider
slider = ipywidgets.IntSlider()

# now, lets associate the min and max of the play interval
#  with the min and max of the integer slider
ipywidgets.link((play, 'min'), (slider, 'min'))
ipywidgets.link((play, 'max'), (slider, 'max'))
ipywidgets.link((play, 'value'), (slider, 'value'))
# note that we don't have to tell the .link function
# how to link these objects - it knows intuatively
# how to link the play widget min & max to the
# slider min and max

# lets display them side by side
ipywidgets.HBox([play, slider])_____no_output_____# note if we display the slider on its own,
# its still linked to the play button above
slider_____no_output_____

### Take aways
* so, we went through some toy examples for how we can link widgets together
* these may have seemed like some silly examples, but these basic ideas are used to build up higher-level interactivity packages like bqplot that we are going to use now_____no_output_____# Activity #3: Bqplot!_____no_output_____

# note: it is possible you will have to pip-install this or anaconda install this
#  also: you might have to close and reopen this notebook if when we get to
# "display" below, no figure pops up

# hopefully, you won't have to restart jupyter but maybe

import bqplot
import numpy as np_____no_output_____

Now we are going to mess around with some of the declaritive programming type options that bqplot can use.  This will rely heavily on the "Grammar of Graphics" constructs._____no_output_____

# lets first start by creating data elements for our graphic
# just some random numbers:
x = np.arange(100)
y = np.random.random(100) + 5_____no_output_____# now we'll define some scale objects which will
# determine how lines will be drawn on our canvas
x_sc = bqplot.LinearScale()
y_sc = bqplot.LinearScale()_____no_output_____# now we are going to use GoG type calls to
# define what lines to actually draw combining
# information about our data and our scales
lines = bqplot.Lines(x = x, y = y, scales = {'x': x_sc, 'y': y_sc})_____no_output_____# now, we are going to define what axis we want placed around
# the lines that we draw
# we'll draw both x & y axis
ax_x = bqplot.Axis(scale = x_sc, label = 'X Value')
ax_y = bqplot.Axis(scale = y_sc, label = 'Y Value', orientation = 'vertical')_____no_output_____# finally, we combine all these things together into a
# bonified figure:
fig = bqplot.Figure(marks = [lines], axes = [ax_x, ax_y])
#display(fig)
# if you don't see the following fig, here is where
#  you might have to close and reopen your notebook
fig # note: just "fig" instead of "display(fig)" may also be an option for you_____no_output_____# ok, but this isn't interactive in anyway
# lets make it!!
pz = bqplot.interacts.PanZoom( scales = {'x': [x_sc], 'y': [y_sc]})_____no_output_____fig = bqplot.Figure(marks = [lines], axes = [ax_x, ax_y], interaction = pz)
display(fig)
# note that if I pan and zoom, the figure updates. Ooooo. fancy
#  note also, that the above figure also reacts as well
# this is because we are using the same lines & ax's objects (I think)_____no_output_____# lets see an example of where this can fail

# first lets make an x from 0-10 in 100 steps
x = np.mgrid[0.0:10.0:100j]
# and 2 y variables
y1 = x * 2
y2 = x**2_____no_output_____x_sc = bqplot.LinearScale(min = 1, max = 10)
# lets do one y-scale over linear and 1 over log
y_sc1 = bqplot.LinearScale(min = 1, max = 20)
y_sc2 = bqplot.LogScale(min = 1, max = 100)_____no_output_____# lets genrate lines for each y value
lines1 = bqplot.Lines(x = x, y = y1, scales = {'x': x_sc, 'y': y_sc1})
lines2 = bqplot.Lines(x = x, y = y2, scales = {'x': x_sc, 'y': y_sc2})_____no_output_____# and lets plot an x axis like before
ax_x = bqplot.Axis(scale = x_sc, label = 'X Value')
# and one y axis on the left
ax_y1 = bqplot.Axis(scale = y_sc1, label = 'Y1 Value',
                    orientation = 'vertical')
# and one y-axis on the right
ax_y2 = bqplot.Axis(scale = y_sc2, label = 'Y2 Value',
                    orientation = 'vertical', side = 'right')_____no_output_____# lets allow pan and zoom
pz = bqplot.interacts.PanZoom(scales = {'x': [x_sc],
                                        'y': [y_sc1, y_sc2]})
#bqplot.interacts.PanZoom?
fig = bqplot.Figure(marks = [lines1, lines2],
                    axes = [ax_x, ax_y1, ax_y2], interaction=pz)
#display(fig)
fig
# now we note if we zoom out too far, or pan to too negative of the x-axis
# we lose a line

# why? because the line is log-scaled, and log(numbers < 0) is undefined

# this is a way in which declaritive programming can fail because there
#  aren't obvious options to inhibit pan&zoom to a positive range_____no_output_____

FYI lots of more fun notebooks here: https://github.com/dmadeka/PyGotham-2017

From video tutorial here: https://www.youtube.com/watch?v=rraXF0EjRC8_____no_output_____

# ok, lets do another quick interactive example using a scatter plot
x = np.random.random(100)
y = np.random.random(100)_____no_output_____x_sc = bqplot.LinearScale()
y_sc = bqplot.LinearScale()

x_ax = bqplot.Axis(scale = x_sc, label = 'X')
y_ax = bqplot.Axis(scale = y_sc, label = 'Y', orientation = 'vertical')_____no_output_____scatters = bqplot.Scatter(x = x,
                          y = y,
                          scales = {'x': x_sc, 'y': y_sc})_____no_output_____# now, lets create a selector to select points along the x-axis
selector = bqplot.interacts.FastIntervalSelector(
            scale = x_sc, marks = [scatters])
# we can also highlight what points we are selecting
scatters.unselected_style={'opacity': 0.8}
scatters.selected_style={'fill': 'red', 'stroke': 'yellow'}
# note we are selecing along the x-scale
fig = bqplot.Figure(marks = [scatters], axes = [x_ax, y_ax], interaction = selector)
display(fig)
# this might depend on what computer you are in, but on my mac, I click
#  to start selecting and then double click to "lock in" my selected
# region_____no_output_____# we can then print out what region is selected
selector.selected_____no_output_____# now, lets try some interactive histogramming of our buildings data
import pandas as pd
buildings = pd.read_csv("/Users/jillnaiman1/Downloads/building_inventory.csv",
                        na_values = {'Year Acquired': 0,
                                     'Year Constructed': 0,
                                     'Square Footage': 0})_____no_output_____# since buildings is our data, we don't have to do anything
#  with that, but we do need to create our scales and
# axes like we've been doing before:
# (1)
x_sc = bqplot.LinearScale()
y_sc = bqplot.LinearScale()
x_ax = bqplot.Axis(scale = x_sc)
y_ax = bqplot.Axis(scale = y_sc, orientation = 'vertical')

# (2) now, lets do an interactive rebinning, but lets
# use bqplot and a slider widget to do it
hist = bqplot.Hist(sample = buildings["Year Acquired"],
            scales = {'sample': x_sc, 'count': y_sc},
                   bins = 128, normalized = True,
                   colors = ["#FFFFFF"])

# lets also create a slider like we've done before
islider = ipywidgets.IntSlider(min = 8, max = 128, step = 1)
# and lets link our sider and our bins of our histogram
ipywidgets.link((islider, 'value'), (hist, 'bins'))
# construct a fig
#fig = bqplot.Figure(marks = [hist], axes = [x_ax, y_ax])
# ***RUN NEXT CELL BEFORE ADDING 2ND HIST

# (3) ok, but maybe we want to see our original histogram
#  underneath, lets add this to our figure
hist2 = bqplot.Hist(sample = buildings["Year Acquired"],
                   opacity = 0.1, normalized = True,
            scales = {'sample': x_sc, 'count': y_sc},
                  bins = 128)
fig = bqplot.Figure(marks = [hist, hist2], axes = [x_ax, y_ax])

# for 2 & 3
#display(ipywidgets.VBox([fig, islider]))
ipywidgets.VBox([fig, islider])_____no_output_____

# Activity #4: Wealth of Nations plot
* originially from the TedTalk: https://www.ted.com/talks/hans_rosling_shows_the_best_stats_you_ve_ever_seen
* found on Rosling's website: https://www.ted.com/talks/hans_rosling_shows_the_best_stats_you_ve_ever_seen
* We're going to make a tool similar to GapMinders:https://www.gapminder.org/world/
* Much of this is, in more detail, in the PyGothum-2017 github: https://github.com/dmadeka/PyGotham-2017
* This will talk to javascript on the backend to mimic the output of another plotting package d3.js, but we don't have to learn about d3.js (just now) and can instead rely on our current Python knowledge_____no_output_____

# import pandas if we have not
import pandas as pd

# lets start off our plot at the initial year of 1800
initial_year = 1800_____no_output_____# we'll read in our datafile and apply
# some pre-written cleaning routines
# get out the data we want for our plotting
from wealth_of_nations import process_data, get_min_max, get_data

# grab data
data = process_data('/Users/jillnaiman1/Downloads/nations.json')

data_____no_output_____# grab min & max values of our variables of interest
income_min, income_max, life_exp_min, life_exp_max, pop_min, pop_max = get_min_max(data)_____no_output_____# lets allow for a mouse-over interaction
# for silly:
import bqplot
tt = bqplot.Tooltip(fields=['name', 'x', 'y'],
                    labels=['Country Name',
                            'Income per Capita', 'Life Expectancy'])
#bqplot.Tooltip?_____no_output_____# we will label what year is being plotted, just like in the Gabminder plot
year_label = bqplot.Label(x=[0.75], y=[0.10],
                   font_size=52, font_weight='bolder',
                   colors=['orange'],
                   text=[str(initial_year)], enable_move=True)_____no_output_____# we'll define our scales like before
# here we scale our x & y axis to the scales of the min and max of our data
x_sc = bqplot.LogScale(min=income_min, max=income_max)
y_sc = bqplot.LinearScale(min=life_exp_min, max=life_exp_max)

# this is just something to color-code each circle by the region it corresponds to
#  (for example, asia, south america, africa, etc)
# the colors call is just mapping each catagorical variable to a color
c_sc = bqplot.OrdinalColorScale(domain=data['region'].unique().tolist(),
                                colors=bqplot.CATEGORY10[:6])

# finally, we want the size of each of our dots to correspond to the population of
# each country
#size_sc = bqplot.LinearScale(min=pop_min, max=pop_max)#, mid_range=0.1)
size_sc = bqplot.LinearScale(max=1326856173.0, min=2128.0)
#bqplot.LinearScale?_____no_output_____# create and label our x & y axis
ax_y = bqplot.Axis(label='Life Expectancy', scale=y_sc,
                   orientation='vertical', side='left',
                   grid_lines='solid')
ax_x = bqplot.Axis(label='Income per Capita', scale=x_sc,
                   grid_lines='solid')_____no_output_____# now we'll use another little function from our library above to grab
# data for our initial setup (year = 1800)
# Start with the first year's data
cap_income, life_exp, pop = get_data(data,initial_year,initial_year)_____no_output_____# now lets make our scatter plot!
wealth_scat = bqplot.Scatter(x=cap_income, y=life_exp,
                             color=data['region'], size=pop,
                      names=data['name'], display_names=False,
                      scales={'x': x_sc, 'y': y_sc, 'color': c_sc,
                              'size': size_sc},
                      default_size=4112, tooltip=tt,
                             animate=True, stroke='Black',
                      unhovered_style={'opacity': 0.5})
# much of these calls are things we've seen before, others will allow fun things
#  like animation and also the ability to click on our plot and interact with it_____no_output_____# for our initial, 1800 view, we'll just allow the first "line" of the evolution of the
# each nation's track to be displayed... this is essentially a place holder (visible = false)
nation_line = bqplot.Lines(x=data['income'][0],
                           y=data['lifeExpectancy'][0],
                           colors=['Gray'],
                       scales={'x': x_sc, 'y': y_sc}, visible=False)_____no_output_____# milliseconds of time between changes we make
time_interval = 10_____no_output_____# create the figure &
fig = bqplot.Figure(marks=[wealth_scat, year_label, nation_line],
                    axes=[ax_x, ax_y],
             title='Health and Wealth of Nations',
                    animation_duration=time_interval)

# lets control the size in pixels too
fig.layout.min_width = '960px'
fig.layout.min_height = '640px'_____no_output_____# we'll use our friend the int slider to slide through years
# for silly:
import ipywidgets
year_slider = ipywidgets.IntSlider(min=1800, max=2008, step=1, description='Year', value=initial_year)_____no_output_____# make sure we define what happens when we change the year on our slider
def year_changed(change):
    wealth_scat.x, wealth_scat.y, wealth_scat.size = get_data(data,year_slider.value,initial_year)
    #wealth_scat.size+=1000
    year_label.text = [str(year_slider.value)]

year_slider.observe(year_changed, 'value')_____no_output_____# now we'll say what happens when we hover over an object
# we'll use "change" again to make it such that if a
# user hovers over a country, the countries "life line"
#  is visible
def hover_changed(change):
    if change.new is not None:
        nation_line.x = data['income'][change.new + 1]
        nation_line.y = data['lifeExpectancy'][change.new + 1]
        nation_line.visible = True
    else:
        nation_line.visible = False

wealth_scat.observe(hover_changed, 'hovered_point')_____no_output_____# finally, lets add a little play button so we can animate
#  what happens in time, just like on the d3.js plot
play_button = ipywidgets.Play(min=1800, max=2008, interval=time_interval)
# note, we use "jslink" because the "backend" here is javascript
#  bqplot is just interacting with javascript
ipywidgets.jslink((play_button, 'value'), (year_slider, 'value'))_____no_output_____# finally, lets put it all together!!

ipywidgets.VBox([ipywidgets.HBox([play_button, year_slider]), fig])_____no_output_____pop_min, pop_max_____no_output_____