I ended the previous post thinking I’d look at adding this image type, cycling line width using scatter plot (clw_scatter) to the repeat route. But, as I was playing with the app, I found many of these images rather interesting. So, I thought I should look at using this image style with geometric transforms. And, that’s what I am going to do.

Rotational and Linear Transforms

I am, as with all the other image types, going to apply a rotational transform (3 or more times, each at a different angle of rotation) and a linear transform (one for each rotation) to prevent them from overlapping too much. Having reviewed some of the code, looks like I will be writing a new function or two, and refactoring one or two existing functions. More work than I was expecting.

I will add a new route based on the cycling line width using colour between with transforms. New route should be rather similar. I am also going to rename the routes for this type of image. Which will mean a fix or two to the templates: image_links.html and text_links.html. I will not bother showing those changes. They are after rather straightforward.

Note: in the process of getting this working I have decided that another refactoring attempt is called for. So, I am going to cover what I did to get this image type working, then move onto the refactoring.

New Route: sp_clw_scatter_t()

So, as stated, I copied over the clw_btw_t() route function (in the main module) and refactored it. Name change being the most obvious refactoring.

Another was that I was passing the complete x and y data arrays to the curve rendering function. Which meant I was also passing that to the transform generation function. But, in the plotting function I was only using the last element of each of those two arrays. Seems like the transform generation function was doing a lot of unnecessary arithmetic.

So refactored the curve generator to only take a vector for the x and y parameters. And, in the route function I only pass the pertinent data rows to the transform generator.

And because the curve generator was creating new values for maximum marker size and randomly selecting new markers each time it was called I had to prevent that when plotting any transforms after the first one. Also, a couple other values being created each time. I had to refactor the functions signature to accept those values for all but the first transform. And add a number of if statements to ensure subsequent transform calls used the values generated by the first call. You can see the signature modification in the calls in the route function.

@app.route('/spirograph/clw_scatter_t', methods=['GET', 'POST'])
def sp_clw_scatter_t():
  pg_ttl = "Cycling Line Width (Scatter) Spirograph Image with 2D Transforms"
  if request.method == 'GET':
    f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cxsts': g.n_whl is not None,
        'cmaps': g.clr_opt, 'mntr': g.mn_tr, 'mxtr': g.mx_tr, 'subtyp': 'clw_scatter'
    }
    return render_template('get_curve.html', type='basic', f_data=f_data)
  elif request.method == 'POST':
    # store all starting colour numbers
    all_cndx = []

    t_xs, t_ys, *_ = get_curve_data('clw_scatter', request.form, 2048)
    setup_image(ax)

    lld_rot, lld_deg, trdy, y_mlt, do_qd, do_qd2 = get_trfm_params(t_xs, t_ys, 'clw_scatter_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)
    g.mrk_rnd = True

    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:
        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
        m1, m2, mx_sz, t_fst, p_step, hf_frq, x_mrg = sal.cycle_lw(ax, dstx1, dsty1)
      else:
        _, _, _, _, _, hf_frq, x_mrg = sal.cycle_lw(ax, dstx1, dsty1, u_m1=m1, u_m2=m2, u_mx=mx_sz, u_fst=t_fst, u_stp=p_step)

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

      c_cnt += 1

    g.lc_frq = tmp_lcf
    g.hf_frq = tmp_hff

    data = fini_image(fig, ax)

    d_end = "top" if g.drp_f else "bottom"
    c_data = sal.get_curve_dtl()
    i_data = sal.get_image_dtl(pg_ttl)
    i_data['c_ndx'] = all_cndx
    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': x_mrg,
              'ntr': g.nbr_tr, 'trang': lld_deg, 'trpt': f'(0, {round(trdy, 3)}) ({y_mlt})',
              'trord2': do_qd2
              }

    return render_template('disp_image.html', sp_img=data, type='basic', c_data=c_data, i_data=i_data, p_data=p_data)

Refactor Curve Generator: cycle_lw()

This function is in the sp_app_lib module.

The signature went from cycle_lw(axt, rxs, rys) to cycle_lw(axt, rxs, rys, u_m1=None, u_m2=None, u_mx=None, u_fst=None, u_stp=None). The function in whole follows. Any if statements based on one of the named parameters are new changes I made to cover keeping things the same for all transforms.

def cycle_lw(axt, rxs, rys, u_m1=None, u_m2=None, u_mx=None, u_fst=None, u_stp=None):
  # set up data for plot
  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
  
  # try to get some decent space around the curve
  m_pts = mx_sz**0.5
  ppd= 72. / axt.figure.dpi
  img_ext = axt.get_window_extent().width
  x_mrg = m_pts / img_ext
  y_mrg = x_mrg
  axt.margins(x_mrg, y_mrg, tight=None)

  t_fst = 0
  p_step = 1
  if not u_fst:
    if g.t_lfr:
      t_fst = rng.choice(g.c_strt)
      if g.DEBUG:
        print(f"\tcycle_lw: t_fst = {t_fst}, ")
  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

  axt.autoscale()
  for i in range (s1, e1, p_step):
    axt.scatter(rxs[i::g.lc_frq], rys[i::g.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::g.lc_frq], rys[i::g.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

Examples

These images take quite some time to generate on my development system.


1) CLW Scatter with Transforms
cycling line width using scatter plot with transforms
Curve Parameters
    Wheels: 3 wheels (shape(s): circle (c))
    Symmetry: k_fold = 3, congruency = 1
    Frequencies: [4, 4, 10]
    Widths: [1, 0.6519993889021068j, 0.36465048370574j]
    Heights: [1, 0.742499616853406j, 0.7299894274934756]
    Data points: 1024
Drawing Parameters
    Colour map: nipy_spectral (0.75, [1, 1, 1, 1])
    BG colour map: inferno (0.22)
    Line width (if used): 9
    Image size: 8
    Image DPI: 72
Transformations
    Number of transforms: 4
    Angles: [45.0, 135.0, 225.0, 315.0]
    Base linear translation point: (0, 0.882) (0.875)
    Order: [0, 3, 2, 1]
Cycling Line Width
    Marker 1: d
    Marker 2: d
    Maximum marker size: 5500
    Half marker cycle frequency: 170 based on 6 cycles
    Starting position: 0
    Axes margin: 0.12875344595652194

2) CLW Scatter with Transforms
cycling line width using scatter plot with transforms
Curve Parameters
    Wheels: 5 wheels (shape(s): ellipse (e))
    Symmetry: k_fold = 5, congruency = 2
    Frequencies: [-3, 7, -8, 12, 2]
    Widths: [1, 0.6626670550992193, 0.5116208434455054, 0.3666591325449057, 0.24841382003910775]
    Heights: [1, 0.5346387390687265j, 0.5110969778264297, 0.3722460473069662, 0.22476701516983086j]
    Data points: 1024
Drawing Parameters
    Colour map: Dark2 (0.75, [1, 1, 1])
    BG colour map: tab20c (0.09)
    Line width (if used): 1
    Image size: 8
    Image DPI: 72
Transformations
    Number of transforms: 3
    Angles: [29.999999999999996, 149.99999999999997, 270.0]
    Base linear translation point: (0, 1.05) (0.5)
    Order: [2, 0, 1]
Cycling Line Width
    Marker 1: 3
    Marker 2: 3
    Maximum marker size: 6500
    Half marker cycle frequency: 256 based on 4 cycles
    Starting position: 0
    Axes margin: 0.1399697525746276

3) CLW Scatter with Transforms
cycling line width using scatter plot with transforms
Curve Parameters
    Wheels: 3 wheels (shape(s): ellipse (e))
    Symmetry: k_fold = 2, congruency = 1
    Frequencies: [3, -7, -7]
    Widths: [1, 0.632431226905058, 0.3538708837145652j]
    Heights: [1, 0.540420693934405j, 0.5069820992143683]
    Data points: 1024
Drawing Parameters
    Colour map: plasma (0.75, [1, 1, 1, 1, 1])
    BG colour map: cubehelix (0.25)
    Line width (if used): 4
    Image size: 8
    Image DPI: 72
Transformations
    Number of transforms: 5
    Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
    Base linear translation point: (0, 1.202) (0.875)
    Order: [2, 0, 4, 3, 1]
Cycling Line Width
    Marker 1: h
    Marker 2: h
    Maximum marker size: 2500
    Half marker cycle frequency: 51 based on 10 cycles
    Starting position: 0
    Axes margin: 0.08680555555555555

4) CLW Scatter with Transforms
cycling line width using scatter plot with transforms
Curve Parameters
    Wheels: 10 wheels (shape(s): equilateral triangle (q))
    Symmetry: k_fold = 8, congruency = 2
    Frequencies: [2, 26, 2, 34, 2, 2, -14, 10, 10, -14]
    Widths: [1, 0.5143382091142977j, 0.45539625330321326, 0.44291262611022136j, 0.3179329214655646j, 0.31604870193744455j, 0.274026450896413, 0.20505239504154282j, 0.125, 0.125j]
    Heights: [1, 0.5114801226157368j, 0.37762181938592365j, 0.20060537243697513j, 0.14817810353662547, 0.14745742600904316j, 0.14006458640185868, 0.125, 0.125, 0.125]
    Data points: 1024
Drawing Parameters
    Colour map: twilight_shifted (0.75, [1, 1, 1, 1, 1])
    BG colour map: brg (0.17)
    Line width (if used): 3
    Image size: 8
    Image DPI: 72
Transformations
    Number of transforms: 5
    Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
    Base linear translation point: (0, 1.18) (0.625)
    Order: [4, 0, 3, 2, 1]
Cycling Line Width
    Marker 1: o
    Marker 2: 3
    Maximum marker size: 7500
    Half marker cycle frequency: 42 based on 12 cycles
    Starting position: 0
    Axes margin: 0.15035163260146503

Done

Unfortunately, images I did like were not generated with any great frequency. And all too many did not appeal to me at all. Might have to fine tune the allowable marker types and/or maximum sizes. However, when I reduced the number of data points per wheel to 1024 from 2048, things did speed up somewhat.

That said, better to have tried than to have passed up a possible opportunity/success.

As mentioned above, some more refactoring to be done. I’ll look at that next time.

Until then, happy tapping.