Okay, looking at the documentation for matplotlib.pyplot.scatter, it seems I can pass an array-like list of marker sizes to the method. Ditto for colour. So I am wondering, for CLW (scatter) spirograph images with transforms, can I generate those cycles once and reuse for subsequent transform plots. Right now that cycle is re-generated for every transform plotted. Seems like a waste of CPU cycles. So, let’s see if I can get that to work somehow.

Not going to show the code at the moment but I am going to add code to generate CLW (scatter) with transforms. A lot of copying from the app modules I expect. I will modify the random selection of image types, for development, to select the image with transforms more frequently than the other two types. I continue to select the other two types as a precaution—want to know if I introduce a bug that affects one or both of them.

Generate Marker Size Cycle

I had originally planned to leave the cycle generation functionality in the cycle_lw() function, store it in a list as it was generated, and return that along with everything else. In the end, I decided to create a new function to create the cycle. It is called from within and returned by cycle_lw() because it needs a lot of information not currently available outside that function. (I am thinking this may perhaps also need refactoring.)

Basically I just moved the base code from cycle_lw() and added the list and return related bits.

def get_marker_cycle(sz_mult, p_step, lc_frq, hf_frq, s1, e1, m_sz1, s2, e2, m_sz2):
  # create a list of 0s of a suitable size, so can use index rather than appending to an empty list
  c_m_sz = [0] * lc_frq
  for i in range (s1, e1, p_step):
    c_m_sz[i] = m_sz1
    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):
    c_m_sz[i] = m_sz2
    if i < hf_frq:
      m_sz2 += sz_mult * p_step
    else:         
      m_sz2 -= sz_mult * p_step

  return c_m_sz

Then I refactored cycle_lw(). New parameter, new return value and an if to decide what to if argument not passed in. Here are, I believe, the pertinent bits.

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

... ...

  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)
 
  if not m_sz_cyc:
    c_m_sz = get_marker_cycle(sz_mult, p_step, lc_frq, hf_frq, s1, e1, m_sz1, s2, e2, m_sz2)
  else:
    c_m_sz = m_sz_cyc

  for i in range (s1, e1, p_step):
    axt.scatter(rxs[i::lc_frq], rys[i::lc_frq], s=c_m_sz[i], alpha=g.alph, marker=m1, clip_on=False)

  for i in range(s2, e2, p_step):
    axt.scatter(rxs[i::lc_frq], rys[i::lc_frq], s=c_m_sz[i], alpha=g.alph, marker=m2, clip_on=False)

  return m1, m2, mx_sz, t_fst, p_step, hf_frq, x_mrg, c_m_sz

Now ain’t that looking rather tidy. A quick test to generate a CLW (scatter) spirograph image worked just fine.

Add CLW (scatter) with Transforms Image Style

Okay, now let’s see if we can get this working with transforms. In the main code section of the module, I changed or added the following bits.

... ...
# modified
i_rnd = rng.integers(0, 5)
do_clw_s = (i_rnd == 0)
do_gnarly = (i_rnd == 1)
do_clw_s_t = (i_rnd >= 2)
... ...
# modified
if do_clw_s or do_clw_s_t:
  if do_clw_s:
    n_pts = 2048
  else:
    n_pts = 1024
  t_xs, t_ys, *_ = main.get_curve_data('clw_scatter', f_data, n_pts)
  x_mn, x_mx, y_mn, y_mx = sal.get_plot_bnds(t_xs, t_ys, x_adj=0)
... ...
# added
elif do_clw_s_t:
  pg_ttl = "Cycling Line Width (Scatter) with Transforms"
  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

  all_cndx = []

  lld_rot, lld_deg, trdy, y_mlt, do_qd, do_qd2 = main.get_trfm_params(t_xs, t_ys, 'clw_btw_t')

  tmp_rot = 0
  # Generate  and plot transformed datasets
  c_cnt = 0
  m1, m2, mx_sz, t_fst, p_step = (None, None, None, None, None)

  for i in do_qd2:
    if lld_deg[i] in [0.0, 90.0, 180.0, 270.0]:
      tmp_rot = 1.2 * lld_rot[i]
      dstx1, dsty1 = sal.get_transform_data(t_xs[-1], t_ys[-1], lld_rot[i], lld_rot[i], trdy)
    else:
      dstx1, dsty1 = sal.get_transform_data(t_xs[-1], t_ys[-1], lld_rot[i], lld_rot[i], trdy)
  
    if g.DEBUG:
      if lld_deg[i] in [0.0, 90.0, 180.0, 270.0]:
        print(F"\t\t==> {math.degrees(tmp_rot)}")
    if c_cnt == 0:
      m1, m2, mx_sz, t_fst, p_step, hf_frq, ax_mrgn, cyc_m_sz = cycle_lw(ax, dstx1, dsty1)
    else:
      _, _, _, _, _, hf_frq, x_mrg, _ = cycle_lw(ax, dstx1, dsty1, u_m1=m1, u_m2=m2, u_mx=mx_sz, u_fst=t_fst, u_stp=p_step, m_sz_cyc=cyc_m_sz)

    all_cndx.append(g.c_ndx)
    ax.autoscale()

    c_cnt += 1

  g.lc_frq = tmp_lcf
  g.hf_frq = tmp_hff

  data = main.fini_image(fig, ax)

  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,
            'ntr': g.nbr_tr, 'trang': lld_deg, 'trpt': f'(0, {round(trdy, 3)}) ({y_mlt})',
            'trord2': do_qd2
            }
... ...
i_data = sal.get_image_dtl(pg_ttl)
# added
if do_clw_s_t:
  i_data['c_ndx'] = all_cndx
... ...

Example Image with Transforms

example of CLW (scatter) style spirograph with transforms
CLW (scatter) spirograph image with transforms
{'m1': '3', 'm2': '3', 'mx_sz': 8500, 't_fst': 0, 'p_step': 1, 'c_nbr': 20, 'hf_frq': 25, 'ax_mrgn': 0.08902461428675615, 'ntr': 3, 'trang': [29.999999999999996, 149.99999999999997, 270.0], 'trpt': '(0, 0.422) (0.375)', 'trord2': [2, 0, 1]}

The default .25 inch margin is in place. The “data” margins are marked off. But the box is much smaller as those margins are based on the minimum and maximum of what would have been a single CLW curve centered in the plot axis. Gives you some idea of how much each transform was linerarly displaced.

But more importantly, the CLW (scatter) with transforms image style appears to work. And, based on personal memory I believe the image is generated just a touch quicker.

Move Cycle Frequency Calculation to Its Own Function

This code is in both CLW (scatter) generator functions (‘routes’ in the app’s main module). And, there is similar code in the CLW (between) routes and functions. So a good candidate for is own function. In the CLW (routes) I keep copies of the current global variables for some of these values and restore them before exiting the route’s function. Not sure why, so for now I am not going to bother with that. But the function will have a ‘side effect’ as it will not only generate the values, but set the relevant global variables accordingly. Am still working in my ’test’ module. No attempt yet to refactor the app modules.

def set_cycle_freq():
  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
  return c_div, g.lc_frq, g.hf_frq

But, I now getting hf_frq returned from two functions. That, with this function being called in the route’s function, is definitely not necessary. So, going to remove the return value from cycle_lw() and modify the lines calling that function accordingly. The modified bits look as follows.

... ...

  return m1, m2, mx_sz, t_fst, p_step, x_mrg, c_m_sz

... ...

if do_clw_s:
  pg_ttl = "Cycling Line Width (Scatter)"
  g.mrk_rnd = True
  c_div, lc_frq, hf_frq = set_cycle_freq()
  m1, m2, mx_sz, t_fst, p_step, ax_mrgn, cyc_m_sz = cycle_lw(ax, t_xs[-1], t_ys[-1])

... ...

elif do_clw_s_t:
  pg_ttl = "Cycling Line Width (Scatter) with Transforms"
  g.mrk_rnd = True
  c_div, lc_frq, hf_frq = set_cycle_freq()

  all_cndx = []

  lld_rot, lld_deg, trdy, y_mlt, do_qd, do_qd2 = main.get_trfm_params(t_xs, t_ys, 'clw_btw_t')
  tmp_rot = 0
  # Generate  and plot transformed datasets
  c_cnt = 0

  for i in do_qd2:
    dstx1, dsty1 = sal.get_transform_data(t_xs[-1], t_ys[-1], lld_rot[i], lld_rot[i], trdy)
  
    if c_cnt == 0:
      m1, m2, mx_sz, t_fst, p_step, ax_mrgn, cyc_m_sz = cycle_lw(ax, dstx1, dsty1)
      print(f"cycle marker sizes ({len(cyc_m_sz)}):\n{cyc_m_sz}")
    else:
      _, _, _, _, _, x_mrg, _ = cycle_lw(ax, dstx1, dsty1, u_m1=m1, u_m2=m2, u_mx=mx_sz, u_fst=t_fst, u_stp=p_step, m_sz_cyc=cyc_m_sz)

And I can assure you, everything continues to work as it did previously and as expected.

Done

I think that’s it for this one. I have been kicking around some ideas, but I think next time I will go over refactoring the app with the changes covered in the last few posts. Then look at adding the two CLW (scatter) image types to my repeat route. Which will, of course, involve further refactoring of the functions added/modified in this series of posts.

I do feel like I am getting somewhere. Tidier code and a new image type or two. A pleasant feeling that. Hope you find pleasure in your time coding.