Plotting Functions

Well, I think we are ready to write our three plotting functions in the chart module/package. Each one will do all the work necessary to properly plot the specific chart. That will include specifying chart and axis titles, x-axis labels, sorting bar widths and locations (if needed), etc. Some of this code for each case already lives in our test section.

Three New Functions

As they are written, I will re-write plot_population() to use them. The call to plot_population() in the test section will generate the plot, before returning the test data. The test data will be displayed in the terminal once the chart is closed. Perhaps make some changes to the test code, if possible. May need to add some temporary code to the three new functions to facilitate testing. I am calling these functions:

  • Type 1: plot_1ma(plot_dtls)
  • Type 2: plot_m1a(plot_dtls)
  • Type 3: plot_mm1(plot_dtls)

Give it a shot, and come back when you are ready. Don’t forget to commit your changes after each function is written and tested.

Type 1 Plots: plot_1ma(plot_dtls)

Had to fix argument name in test section for Type 1 charts. Also during testing the Type 1 chart I decided to try plotting 6 and 10 years to see how it would look. Had to modify get_xticks() to use its nbr_bars argument to determine how many bars worth of data to generate. Was previously fixed at 5.

Here’s my versions of the new and updated functions, the modified plot_population() code and the modified test code.

def get_xticks(nbr_lbls=21, nbr_bars=1):
  x_ticks = []
  b_width = 0.4

  lbl_x = [[] for _ in range(nbr_bars + 1)] # store positions for each bar, indexed at 1 for first bar
  lbl_x[1] = list(np.arange(nbr_lbls))  # the first bar locations
  # Calculate optimal width
  # How to plot bar graphs with same X coordinates side by side
  # https://stackoverflow.com/questions/10369681/how-to-plot-bar-graphs-with-same-x-coordinates-side-by-side-dodged
  b_width = np.min(np.diff(lbl_x[1]))/(nbr_bars + 1.0)

  # sort x position for all other bars
  for i in range(2, nbr_bars+1):
    lbl_x[i] = [x + b_width for x in lbl_x[i-1]]

  x_ticks = [x + ((nbr_bars-1)/2*b_width) for x in lbl_x[1]]

  return b_width, lbl_x, x_ticks


def plot_1ma(plot_dtls):
  p_nms, p_years, p_grps = plot_dtls
  nbr_bars = len(p_years[0])
  # nbr_lbls = 21, which is get_xticks() default
  bar_wd, x_bars, x_l_ticks = get_xticks(nbr_bars=nbr_bars)
  x_lbls = get_agrp_lbls()

  fig, ax = plt.subplots(figsize=(15,7.5))
  c_rects = []
  ibar = 1
  for pyr in p_years[0].keys():
    c_rects.append(ax.bar(x_bars[ibar], p_years[0][pyr], bar_wd, label=pyr))
    ibar += 1
  # Add some text for labels, title and custom x-axis tick labels, etc.
  ax.set_ylabel('Population (1000s)')
  ax.set_xlabel('Age Groups')
  ax.set_title(f'Population by Age Group for {p_nms[0]}')
  ax.set_xticks(x_l_ticks)
  ax.set_xticklabels(x_lbls)
  ax.legend()

  plt.show()
... in plot_population()
  if len(p_nms) == 1:
    # type 1 chart
    plot_data[0] = p_nms
    plot_data[2] = p_grp
    # get data for each year specified by p_yrs (start yr, nbr yrs)
    strt_yr = int(p_yrs[0])
    end_yr = strt_yr + p_yrs[1]
    for yr in range(strt_yr, end_yr):
      yr_list.append(str(yr))
    dbg_data = pdb.get_1cr_years_all(p_nms[0], yr_list)
    plot_data[1].append(dbg_data)
    plot_1ma(plot_data)
...
... in the test code block
  if do_tst == 1:
    n_lbls = 21
    print(f"args.nbr_yrs {args.nbr_yrs}\n")
    if args.nbr_yrs != 2:
      tst_deets[do_tst][1][1] = args.nbr_yrs
    n_bars = tst_deets[do_tst][1][1]
    print(f"\nchart data: {tst_deets[do_tst]}\n")
    cnms, years, grps = plot_population(tst_deets[do_tst])
    print(cnms, years, grps)
    print('\n')
    labels = get_agrp_lbls()
    print(labels)
...

The next two plotting functions will follow a similar pattern. So, I am not going to discuss them any further. I will just write, test and commit the changes to all related code one by one. Remember, no big code change commits. Even these are perhaps too big.

Remaining Plots: plot_m1a(plot_dtls) and plot_mm1(plot_dtls)

Lots of code duplication, but I want to get it working before I think about tidying things up and writing new functions to reduce the code duplication in these three new functions.

plot_m1a(plot_dtls)

def plot_m1a(plot_dtls):
  p_nms, p_years, p_grps = plot_dtls
  nbr_bars = len(p_nms[0])
  # nbr_lbls = 21, which is get_xticks() default
  bar_wd, x_bars, x_l_ticks = get_xticks(nbr_bars=nbr_bars)
  x_lbls = get_agrp_lbls()

  fig, ax = plt.subplots(figsize=(15,7.5))
  c_rects = []
  ibar = 1
  for cnm in p_nms[0].keys():
    c_rects.append(ax.bar(x_bars[ibar], p_nms[0][cnm], bar_wd, label=cnm))
    ibar += 1
  # Add some text for labels, title and custom x-axis tick labels, etc.
  ax.set_ylabel('Population (1000s)')
  ax.set_xlabel('Age Groups')
  ax.set_title(f'Population by Age Group for {p_years[0]}')
  ax.set_xticks(x_l_ticks)
  ax.set_xticklabels(x_lbls)
  ax.legend()

  plt.show()

plot_mm1(plot_dtls)

def plot_mm1(plot_dtls):
p_nms, p_years, p_grps = plot_dtls
x_lbls = list(p_nms[0][list(p_nms[0].keys())[0]].keys())
# nbr of x-labels is the number of years being displayed
nbr_lbls = len(x_lbls)
# nbr of bars in each qroup is the number of countries being displayed
nbr_bars = len(p_nms[0])
bar_wd, x_bars, x_l_ticks = get_xticks(nbr_lbls=nbr_lbls, nbr_bars=nbr_bars)

fig, ax = plt.subplots(figsize=(15,7.5))
c_rects = []
ibar = 1
for cnm in p_nms[0].keys():
  c_rects.append(ax.bar(x_bars[ibar], p_nms[0][cnm].values(), bar_wd, label=cnm))
  ibar += 1
# Add some text for labels, title and custom x-axis tick labels, etc.
ax.set_ylabel('Population (1000s)')
ax.set_xlabel('Years')
ax.set_title(f'Population by Year for the Age Group: {p_grps[0]}')
ax.set_xticks(x_l_ticks)
ax.set_xticklabels(x_lbls)
ax.legend()

plt.show()

And my plot_population() now looks like this:

def plot_population(chart_dtls):
plot_data = [[], [], []]
p_nms, p_yrs, p_grp = chart_dtls
yr_list = []  # to send list of years to population module function if necessary
if len(p_nms) == 1:
  # type 1 chart
  plot_data[0] = p_nms
  plot_data[2] = p_grp
  # get data for each year specified by p_yrs (start yr, nbr yrs)
  strt_yr = int(p_yrs[0])
  end_yr = strt_yr + p_yrs[1]
  for yr in range(strt_yr, end_yr):
    yr_list.append(str(yr))
  dbg_data = pdb.get_1cr_years_all(p_nms[0], yr_list)
  plot_data[1].append(dbg_data)
  plot_1ma(plot_data)

elif p_yrs[1] == 1 and p_grp[0] == 'all':
  # type 2 chart
  plot_data[1] = p_yrs
  plot_data[2] = p_grp
  # get data for each country in p_nms
  dbg_data = pdb.get_crs_1yr_all(p_nms, p_yrs[0])
  plot_data[0].append(dbg_data)
  plot_m1a(plot_data)

elif p_grp[0] != 'all':
  # type 3 chart
  plot_data[1] = p_yrs
  plot_data[2] = p_grp
  strt_yr = int(p_yrs[0])
  end_yr = strt_yr + p_yrs[1]
  for yr in range(strt_yr, end_yr):
    yr_list.append(str(yr))
  # get data for combination of each name in p_nms and each year specified by p_yrs
  dbg_data = pdb.get_crs_years_one(p_nms, yr_list, p_grp[0])
  plot_data[0].append(dbg_data)
  plot_mm1(plot_data)
  
else:
  # wth?!
  pass

I won’t bother with my code for the test section. I am sure your’s is at least as good as mine, if not better.

Updating the Main Application Module

The last thing I want to do today, is to use the menues.py module to get the plot info and generate the 3 types of plots to make sure things work. So, we need to take our development/debug code out of the menu loop for the charting menu and add the line to produce and display the chart given the user data.

... in the loop in the __name__ block
        if do_plot:
          print(f"\n\tPlotting chart for {p_data}")
          chart.plot_population(p_data)
        else:
          print(f"\n\t{TRED}Not{TRESET} provided with {TRED}sufficient parameters{TRESET} to produce a plot!")\
...

Yup, that’s it.

Examples of Testing Chart Types

Before calling it a day, I am going to include the user exchange in the terminal window and the resulting chart for an example of each of the three chart types. All images, 2 for each case, no videos.

Chart Type 1

user terminal interaction with menu system for Type 1 chart resulting chart from above user terminal exchange

Chart Type 2

user terminal interaction with menu system for Type 2 chart resulting chart from above user terminal exchange

Chart Type 3

user terminal interaction with menu system for Type 3 chart resulting chart from above user terminal exchange

So, what do you think? Getting there?

Merging Branch with Master Repository?

Well, I quite honestly don’t really know how to do what is now needed. I have to get my changes in the * menu-development* branch merged into the master branch. May take a bit of trial and error. Though I am going to start by deleting population_by_age.py from the menu-development branch. Then renaming menues.py to population_by_age.py and testing things.

I am adding some reading to the resources section below. Not sure how much any of them are going to help with our situation, as I haven’t yet read them all. Just a google.

Bye for Now

And with that I will call it a day. See you next time — still on the accelerated schedule (twice a week, Monday and Thursday).

Resources

Git Branching and Merge