Okay, a slight change of plan. I am going to continue developing/testing my refactoring ideas in that test module developed in the last post. Not sure why, but it just seems to make some sense to me.

I am likely going to need to look at a few more image types, so I will be copying over some other functions and modify my test code to select and plot them at random. One by one as needed to test changes. Hopefully keeping all the lines showing the various margin areas (base, marker, etc.).

Marker Selection Function

Moving the marker selection code to it’s own function would tidy the CLW (Scatter) generating function. And, help keep the functions (new and old) more focused. We will still need to deal with reusing the same markers for the transform image style, but one thing at a time.

Don’t know that it is appropriate, but I am going to, as well, move the marker size selection to the new function. It will return the two marker types and the maximum marker size.

# if u_m1 specified, u_m2 must also be specified
def get_markers(u_m1=None, u_m2=None, u_mx=None):
  if not u_mx:
    mx_sz = rng.choice(list(range(1500, 10001, 1000)))
  else:
    mx_sz = u_mx
  if not u_m1:
    if g.mrk_rnd:
      m1 = rng.choice(g.poss_mrkrs)
      # 50% of the time use different marker for 2nd plotting loop
      if rng.integers(0, 2) == 1:
        m2 = rng.choice(g.poss_mrkrs)
      else:
        m2 = m1
    else:
      m1= 'o'
      m2 = 'o'
  else:
    m1 = u_m1
    m2 = u_m2
  return m1, m2, mx_sz

And the git diff for cycle_lw() looks like this. A lot of lines replaced with a function call—will be easier to read and understand.

def cycle_lw(axt, rxs, rys, u_m1=None, u_m2=None, u_mx=None, u_fst=None, u_stp=None, u_mrg=0.25):
  # set up data for plot
+  m1, m2, mx_sz = get_markers(u_m1=u_m1, u_m2=u_m2, u_mx=u_mx)
-  if not u_mx:
-    mx_sz = rng.choice(list(range(1500, 10001, 1000)))
-  else:
-    mx_sz = u_mx

  lc_frq = g.lc_frq
  hf_frq = g.hf_frq

  sz_mult = mx_sz // g.hf_frq
-  if not u_m1:
-    if g.mrk_rnd:
-      m1 = rng.choice(g.poss_mrkrs)
-      # 50% of the time use different marker for 2nd plotting loop
-      if rng.integers(0, 2) == 1:
-        m2 = rng.choice(g.poss_mrkrs)
-      else:
-        m2 = m1
-    else:
-      m1= 'o'
-      m2 = 'o'
-  else:
-    m1 = u_m1
-    m2 = u_m2

You’ll just have to take my word, for now, that things work as they did previously.

Another Image Type

I thought I should look at how best to set margins for all, or most, image types. For most of them I should be able to set a default value and not change it except for the special cases (e.g. CLW (scatter)). Let’s try the gnarly style image.

First of all, I am planning to set the default margins in the setup_image() function currently in the app’s main module. I don’t want to change that just yet, so copying it over to the test module, and removing the library prefix, main., from the call to the function in the test module code.

I will randomly select which image to generate. And I will need to put pertinent bits of code in if/else blocks. As I expect to add another image type later, I will actually use an elif block for the second image type. Was going to to show the changes using git diff, but it’s a might ugly. So here’s the pertinent code in full.

i_rnd = rng.integers(0, 2)
do_clw_s = (i_rnd == 0)
do_gnarly = (i_rnd == 1)

f_data = {"shp_mlt": ""}
if do_clw_s:
  t_xs, t_ys, *_ = main.get_curve_data('clw_scatter', f_data, 2048)
  x_mn, x_mx, y_mn, y_mx = sal.get_plot_bnds(t_xs, t_ys, x_adj=0)
elif do_gnarly:
  r_xs, r_ys, m_xs, m_ys, m2_xs, m2_ys = main.get_curve_data('gnarly', f_data, g.t_pts)
  x_mn, x_mx, y_mn, y_mx = sal.get_plot_bnds(r_xs, r_ys, x_adj=0)

setup_image(ax)
# ax.margins(0, 0)
x_mrg, y_mrg = ax.margins()
print(f"\tx_mrg: {x_mrg}; y_mrg: {y_mrg}")

ax.autoscale()
if do_clw_s:
  pg_ttl = "Cycling Line Width (Scatter)"
  tmp_lcf = g.lc_frq
  tmp_hff = g.hf_frq
  c_div = rng.choice([4, 6, 8, 10, 12, 14, 16, 18, 20])
  g.lc_frq = g.t_pts // c_div
  g.hf_frq = g.lc_frq // 2
  g.mrk_rnd = True
  m1, m2, mx_sz, t_fst, p_step, hf_frq, ax_mrgn = cycle_lw(ax, t_xs[-1], t_ys[-1], u_mrg=use_mrgn)
  g.lc_frq = tmp_lcf
  g.hf_frq = tmp_hff
  p_data = {'m1': m1, 'm2': m2, 'mx_sz': mx_sz, 't_fst': t_fst, 'p_step': p_step, 'c_nbr': c_div, 'hf_frq': hf_frq, 'ax_mrgn': ax_mrgn}
elif do_gnarly:
  pg_ttl = "Gnarly Spirograph Image"
  ax.plot(r_xs, r_ys, lw=g.ln_w, alpha=g.alph, zorder=g.pz_ord)
  ax.plot(m_xs, m_ys, lw=g.ln_w, alpha=g.alph, zorder=g.pz_ord)
  ax.plot(m2_xs, m2_ys, lw=g.ln_w, alpha=g.alph, zorder=g.pz_ord)
  d_end = "top" if g.drp_f else "bottom"
  p_data = {
            'd_end': d_end, 'd_nbr': g.r_skp
            }

It ain’t pretty, but to show you that I am now getting a gnarly image periodically here’s a sample.

example of gnarly image using previous margin rules
Gnarly spirograph image using old margin margin rules
x_mrg: 0.05; y_mrg: 0.05
data lower left: (-1.4354582009917347, -2.035901839899604); upper right: (2.0859942103925424, 2.0359018398996045)
        axes lower left: (-1.6115308215609485, -2.2394920238895644); upper right: (2.262066830961756, 2.239492023889565)
        x_mrg: 0.05; y_mrg: 0.05
        axes_coord upper right curve coords: [0.95454545 0.95454545]; % of data limits 0.09999999999999998
rt: 0.975, (ix, iy): (2.1652268896486886, 2.127517422695086)
lf: 0.025, (ix, iy): (-1.514690880247881, -2.127517422695086)

Refactor setup_image()

Okay time to refactor setup_image() to use the revised margin function and rules. I had thought at one point about including the code to decide whether or not to use a base ¼ inch or ⅜ inch margin in set_margins(). But decided that decision really didn’t belong in that function. Would effectively been a side effect (those are generally frowned upon). So for now, I am going to include that in the setup_image() function.

I am going to chose a ¼ inch margin if the image DPI value is 72, ⅜ inch otherwise. I am assuming a DPI of 72 is for on-screen display, and any other value is to generate images to eventually be used for printing/framing. That may change down the road, but for now should work just fine.

In my current code, if the CLW (scatter) image is generated I never reset the margins. But since I call setup_image() in every route, setting the default margin there will effectively undo setting a different margin for a given image type.

Well this didn’t quite go the way I expected. Made a number of changes in a couple of additional functions, function calls, added new global variable, etc. Hopefully I can recall everything I have changed so I can explain my choices.

The new global variable, and its default value, is b_mrgn = 0.25. It is used to track the current base margin value (either 0.25 or 0.375). It will be set on each call to setup_image(). Once I did that I had to rework functions to use that value more often than the one passed as a parameter. So, I changed the default value of the margin parameter to None.

... ...

def set_margins(axt, u_mrg=None, m_sz=None):
  x_ext = axt.get_window_extent().width
  y_ext = axt.get_window_extent().height
  print(f"\tx_ext: {x_ext}; y_ext: {y_ext}")

  if u_mrg:
    b_mrgn = u_mrg
  else:
    b_mrgn = g.b_mrgn
  m_pts, mx_pts = b_mrgn * 72, 0
  if m_sz:
    mx_pts = (m_sz**0.5) * 0.5
... ...
  print(f"\tb_mrgn: {b_mrgn} ({m_pts} pts), m_sz: {m_sz} (1/2 size {mx_pts} pts), x_mrg: {x_mrg}, y_mrg: {y_mrg}")

... ...

def cycle_lw(axt, rxs, rys, u_m1=None, u_m2=None, u_mx=None, u_fst=None, u_stp=None, u_mrg=None):

... ...

  # remove u_mrg parameter from call
  m1, m2, mx_sz, t_fst, p_step, hf_frq, ax_mrgn = cycle_lw(ax, t_xs[-1], t_ys[-1])

... ...

Finally, the refactored setup_image() (or at least the relevant bits).

def setup_image(ax):
  ax.cla()
  if g.i_dpi == 72:
    set_margins(ax, u_mrg=0.25)
    g.b_mrgn = .25
  else:
    set_margins(ax, u_mrg=0.375)
    g.b_mrgn = .375
  g.rcm, g.cycle = sal.set_clr_map(ax, g.mx_s, g.rcm)
  ax.autoscale()
  ... ...

You will have to take my word that it appears to work as planned.

Segment Generation Function

I want to cut down some more on the code in cycle_lw() (the version in the test module). There is a whole bunch of code determining the cycle frequency, the step size and the various start and end points for the segments. I think this is code that should be moved to a new function. Making cycle_lw() cleaner and perhaps more readable.

I will copy the code to get_cycle_params(mx_sz, u_fst=None, u_stp=None). Then refactor it and return the values that cycle_lw() will need to finish the job.

def get_cycle_params(mx_sz, u_fst=None, u_stp=None):
  lc_frq = g.lc_frq
  hf_frq = g.hf_frq

  sz_mult = mx_sz // hf_frq

  t_fst = 0
  p_step = 1
  if not u_fst:
    if g.t_lfr:
      t_fst = rng.choice(g.c_strt)
  else:
    t_fst = u_fst
    p_step = u_stp

  if t_fst == 0:
    s1 = 0
    e1 = hf_frq
    s2 = hf_frq
    e2 = lc_frq
    m_sz1 = sz_mult * p_step
    m_sz2 = hf_frq * sz_mult * p_step
  elif t_fst == 50:
    s1 = hf_frq
    e1 = lc_frq
    s2 = 0
    e2 = hf_frq
    m_sz1 = hf_frq * sz_mult * p_step
    m_sz2 = sz_mult * p_step
  else:
    s1 = int(g.lc_frq * t_fst / 100)
    e1 = lc_frq
    s2 = 0
    e2 = s1
    m_sz1 = s1 * sz_mult * p_step
    m_sz2 = sz_mult * p_step

  return t_fst, p_step, lc_frq, hf_frq, sz_mult, s1, e1, m_sz1, s2, e2, m_sz2

And, cycle_lw() now looks like this.

# if u_fst specified, u_stp must also be specified
def cycle_lw(axt, rxs, rys, u_m1=None, u_m2=None, u_mx=None, u_fst=None, u_stp=None, u_mrg=None):
  # set up data for plot
  m1, m2, mx_sz = get_markers(u_m1=u_m1, u_m2=u_m2, u_mx=u_mx)
  # set margins appropriately
  x_mrg = set_margins(axt, u_mrg=u_mrg, m_sz=mx_sz)
  # get marker cycle parameters
  t_fst, p_step, lc_frq, hf_frq, sz_mult, s1, e1, m_sz1, s2, e2, m_sz2 = get_cycle_params(mx_sz, u_fst=u_fst, u_stp=u_stp)

  # generate plot
  for i in range (s1, e1, p_step):
    axt.scatter(rxs[i::lc_frq], rys[i::lc_frq], s=m_sz1, alpha=g.alph, marker=m1, clip_on=False)
    if i < hf_frq:
      m_sz1 += sz_mult * p_step
    else:         
      m_sz1 -= sz_mult * p_step

  for i in range(s2, e2, p_step):
    axt.scatter(rxs[i::lc_frq], rys[i::lc_frq], s=m_sz2, alpha=g.alph, marker=m2, clip_on=False)
    if i < hf_frq:
      m_sz2 += sz_mult * p_step
    else:         
      m_sz2 -= sz_mult * p_step
  
  return m1, m2, mx_sz, t_fst, p_step, hf_frq, x_mrg

And regardless of what you may think of the image, the refactored code appears to work as expected. I have since producing the one below generated a number of others to be certain.

example of CLW (scatter) image after refactoring the generation of the marker cycle code
CLW (scatter) spirograph image generated after above refactoring

Including blank lines there are now 25 lines in cycle_lw(). There were previously 90 lines. It’s not that those lines have disappeared. But the new and refactored functions are all more focused and I believe more readable.

Done

And this post is getting to be lengthy and perhaps as unreadable as the old version of cycle_lw(). So, I think it is time to consider it finished. More I wish to do and/or try, so we will continue with this test module in the next post.

Until then, may your refactoring bring you a feeling of success.