I have decided I do not like the way the form allowing users to specify curve/image parameters currently works. I would like to be able to go to, any version of, the form, change one or more parameters and generate an image. But, I want to keep the previous values for any fields I do not specify. That is not how things currently work.

I also wish to add a new field to specify whether or not the same curve should be used again for subsequent images.

My initial impression is that I really only have two ways to do this. One is use current values to set the form fields’ values before displaying to the user. The other is to track all the values (globals) and then fill in any that are missing when the form data is submitted to the route code. I have seen the former more often than the latter. Likely with good reason. But for whatever reason, I currently—for better or worse—favour the latter.

Sorry, might not have any images in this one. But, if the refactoring section is not too long, I will look at adding another image variation to the app. The one I have in mind is a variation on the mosaic with 2D transform currently available.

Update: yes there are images.

Refactor Form Processing

New Form Field

I have added a use again field to the top of the form. But only display it if a set of curve data already exists. However since I do check for the field, I wanted to make sure it is present. Hence the hidden field if curve data does not exist.

    {% if f_data['cxsts'] %}
    <div class="radio">
      <p style="margin-left:1rem;margin-top:0;"><b>Use same curve again:</b></p>
      <p>
        <label for="uano">No: </label>
        <input type="radio" name="u_agn" id="uano" value="false" />
        <label for="uayes">Yes: </label>
        <input type="radio" name="u_agn" id="uayes" value="true" />
      </p>
    </div>
    {%  else %}
    <input type="hidden" value="false" name="u_agn" />
    {% endif %}

New Global Variables

I am going to add a new global variable for each potential form field value. Keeping in mind that they are not all used for any given image type. Then I am going to write a function that uses those values to replace any missing values in the submitted form data before processing the form data further.

# variables for partially empty forms and u_again to work properly
# u_agn, wheels, shape, shp_mlt, torb, drows, c_nbr, r_mlt, ln_sz, cmap, do_bg, ntr
# initially all set to None regardless of actual type used
u_again = None   # use same curve data again
# curve basics
u_whl = None
u_shp = None    # these two are mutually exclusive
u_shps = None   # these two are mutually exclusive
# gnarly
u_torb = None
u_drws = None
# mosaic
u_cnbr = None
r_mlt = None
# image
u_lw = None
u_cmap = None
u_bg = None
# transforms
u_ntr = None

Form Completion Function

When the function gets the data returned from the form, it will check for missing fields. If a field is missing, it will either replace with the appropriate global above (if not None) or assign a default. I am going to hard code the defaults in the function as that is the only place I expect to use them.

Okay, I also created a dictionary keyed on the names of the form fields with the item value being the related global variable. Allowed for simpler code. Except for one case: single versus multiple wheel shapes. I dealt with filling those two related fields outside my main field filling loop. Took a bit of debugging as I was getting some of the logic wrong. But, code below appears to work as desired.

Without those dictionaries and the extra code for the wheel shape fields, it would have been pretty short.

def fini_curve_form(f_data):
  dflts = {
    'u_agn': 'false',
    'wheels': 'r',
    'shape': 'x',
    'shp_mlt': 'rnd',
    'torb': 'top',
    'drows': '1',
    'c_nbr': '16',
    'r_mlt': 'false',
    'ln_sz': 'r',
    'cmap': 'r',
    'do_bg': 'true',
    'ntr': 'r'
  }
  fld_2_glbl = {
    'u_agn': g.u_again,
    'wheels': g.u_whl,
    'shape': g.u_shp,
    'shp_mlt': g.u_shps,
    'torb': g.u_torb,
    'drows': g.u_drws,
    'c_nbr': g.u_cnbr,
    'r_mlt': g.u_rmlt,
    'ln_sz': g.u_lw,
    'cmap': g.u_cmap,
    'do_bg': g.u_bg,
    'ntr': g.u_ntr
  }
  cmplt = {}

  # deal with shape vs shp_mlt before going any further
  cmplt['shape'] = ''
  cmplt['shp_mlt'] =  ''
  if 'shape' in f_data:
    cmplt['shape'] = f_data['shape']
    cmplt['shp_mlt'] = f_data['shp_mlt']
  else:
    if f_data['shp_mlt'] != '':
      cmplt['shp_mlt'] = f_data['shp_mlt']
    else:
      if fld_2_glbl['shape'] is None:
        if fld_2_glbl['shp_mlt'] is not None:
          cmplt['shp_mlt'] = fld_2_glbl['shp_mlt']
        else:
          cmplt['shape'] = dflts['shape']
      else:
        cmplt['shape'] = fld_2_glbl['shape']

  for fld in dflts:
    if fld == 'shape' or fld == 'shp_mlt':
      continue
    do_fill = False

    if fld in f_data:
      cmplt[fld] = f_data[fld]
      if f_data[fld] is None or f_data[fld] == '':
        do_fill = True
    else:
      do_fill = True

    if do_fill:
      if fld_2_glbl[fld] is not None:
        if g.DEBUG and fld == 'wheels':
          print(f"; fld_2_glbl[{fld}]: {fld_2_glbl[fld]}")
        cmplt[fld] = fld_2_glbl[fld]
      else:
        cmplt[fld] = dflts[fld]
        if g.DEBUG and fld == 'wheels':
          print(f"; dflts[{fld}]: {dflts[fld]}")

      if g.DEBUG and fld == 'wheels':
        print(f"; cmplt[{fld}]: {cmplt[fld]}")

    fld_2_glbl[fld] = cmplt[fld]

  return cmplt

Help Page

I added a new page to the app. Sort of an help page. And, I modified the base.html template to add a link to the page in the header bar. Had to play with CSS as well. Using flex to do the positioning in the header. Won’t bother showing those changes or the help page at this time.

Short enough; let’s add a new image variation.

Plot Between 2D Transforms

Really not too sure what to call this one. The idea is to generate the requested number of transforms. But, rather than plotting between the lines in each transform separately, I will plot between lines in different transforms.

In my initial work on this image type, I went through a few different approaches. I started by colouring between the same row in each transform. But in the end didn’t like the result. So, I tried colouring between a random row in the first transform and a random row in the second. Much better, especially with more colour sections for each row pairing. But, I figured there was likely a great deal of overwriting while generating the image. So in the end, I select a row at random in the first transform and colour between it and every row in the second transform. Let’s get that working.

New Image Drawing Function

Added another function to the app library module, sp_app_lib.py, to do the actual colouring between two transforms. You will need to refer to my earlier posts to get some idea of how that works. Needless to say, another globabl variable as I want to use the same row in the first transform and the same random order for the second for each pair of transforms for a single image. Didn’t want to generate a new random set of rows for each pairing.

New Route

I then added a new route so I could test the above function. Pretty repetitive. More refactoring likely required to reduce the repetition.

@app.route('/spirograph/mosaic_t2t', methods=['GET', 'POST'])
def sp_mosaic_t2t():
  pg_ttl = "Transform to Transform Mosaic Like Spirograph Image"
  if request.method == 'GET':
    f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cxsts': g.n_whl is not None,
              'cmaps': g.clr_opt, 'mxs': g.mx_s, 'mntr': g.mn_tr, 'mxtr': g.mx_tr
    }
    return render_template('get_curve.html', type='mosaic', f_data=f_data)
  elif request.method == 'POST':
    sal.proc_curve_form(request.form, 'mosaic')
    t_xs, t_ys, t_shp = sal.init_curve()

    r_xs, r_ys, m_xs, m_ys, m2_xs, m2_ys = sal.get_gnarly_dset(t_xs, t_ys, 0, g.drp_f, g.t_sy)

    ax.clear()
    g.rcm, g.cycle = sal.set_clr_map(ax, g.mx_s, g.rcm)

    # get angles and such
    lld_rot, lld_deg, trdy, y_mlt = sal.get_transform_base(r_xs, r_ys)

    # drop first data row?
    if g.n_whl == 3:
      src_st = 0
    else:
      src_st = 1
    src = np.array([r_xs[src_st:], r_ys[src_st:]], dtype="float")
    if g.DEBUG:
      print(f"len(src[0]): {len(src[0])} -> {len(src[0][0])}, len(src[1]): {len(src[1])} -> {len(src[1][0])}")

    # get rand quadrant order
    do_qd = sal.get_rnd_qds(list(range(g.nbr_tr)))
    do_qd2 = sal.get_rnd_qds(do_qd)

    # only change multipliers the first time, not on later iterations
    sal.cnt_btw = 0

    # Generate transformed datasets and colour between them
    for i in range(g.nbr_tr):
      if do_qd[i] == do_qd2[i]:
        if g.DEBUG:
          print(f"\tDEBUG: dropping transform {do_qd[i]} == {do_qd2[i]}")
        continue

      dstx1, dsty1 = sal.get_transform_data(src[0], src[1], lld_rot[do_qd[i]], trdy)
      _, dsty2 = sal.get_transform_data(src[0], src[1], lld_rot[do_qd2[i]], trdy)

      rys2i = sal.btw_qds(ax, dstx1, dsty1, dsty2, mlt=g.bw_m, sect=g.bw_s)    
      ax.autoscale()
      sal.cnt_btw += 1

    ax.text(0.5, 0.01, "© koach (barkncats) 2022", ha="center", transform=ax.transAxes,
            fontsize=10, alpha=.3, c=g.cycle[len(g.cycle)//2])
    ax.autoscale()

    # add background
    bax2, _ = sal.set_bg(ax)

    # convert image to base64 for embedding in html page
    data = sal.fig_2_base64(fig)

    c_data = sal.get_curve_dtl()
    i_data = sal.get_image_dtl(pg_ttl)
    p_data = {'bdx': g.bw_dx, 'bol': g.bw_o, 'bcs': g.bw_s,
              'bmlt': g.bw_m, 'mlts': sal.c_mlts,
              'ntr': g.nbr_tr, 'trang': lld_deg, 'trpt': f'(0, {round(trdy, 3)}) ({y_mlt})',
              'trord': do_qd, 'trord2': do_qd2, 'trws': g.bw_trws
             }

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

And, of course there was a fair bit of debugging and fixing before things worked as expected. One such bug was using the values of a list to index back into itself. Lots of IndexError: list index out of range messages. Took me awhile to see what I was doing. A result of refactoring code that was previously running in a very different environment. But, it finally appears to work as expected.

Page Templates

I did not add any new fields to the curve parameters form. So no changes there. I did add another image type link to the home page. As well as to the image display page. And, I did add some additional bits of info to the page’s output. You can see that in the last row of the declaration of p_data above. I won’t bother with showing you the actual templates. Should all be pretty straightforward.

Minor Refactor

But during further playing/testing I would occasionally end up with out any image, just the background. In the image drawing function, sal.btw_qds(), if the current index for both quadrants is the same, they are skipped. Nothing is added to the image, since it would be drawing between rows in the same quadrant. Occasionally, do_qd and do_qd2 would be identical. Consequently, nothing at all related to the curve was drawn on the image.

So, I set out to write a function to determine if the two lists overlapped. I had decided I’d actually like to avoid skipping any pairings. So, equality was only part of the issue. I needed to know if any position in the two lists pointed to the same transform. So, a function to do so. But, I have to admit I didn’t work out the solution myself. Internet search to the rescue. The solution I used was the one I found most interesting. And, I finally have some idea of what zip() does and why you might want to use it. And, in this case I could perhaps have skipped checking that the two lists are of equal length as I know they are. But…

I really love this bit of code. For equality, rather than > 0, one would use == len(l1).

def lists_overlap(l1, l2):
  return len(l1) == len(l2) and sum([1 for i, j in zip(l1, l2) if i == j]) > 0

zip iterates over the two lists it is given (can be more) and returns a tuple of values, (i, j), one from each list. We use a comprehension to store 1 in the list if the two elements are equal. Then sum up the list. If there are any matching elements the sum will be greater than 0.

I then modified the appropriate section of the code in the route. I also added a counter. I didn’t want the loop to run too often. I figured after 5 attempts, I would go with what was there.

    # get rand quadrant order
    do_qd = sal.get_rnd_qds(list(range(g.nbr_tr)))
    do_qd2 = sal.get_rnd_qds(do_qd)
    qlcnt = 0
    while sal.lists_overlap(do_qd, do_qd2) and qlcnt < 5:
      do_qd2 = sal.get_rnd_qds(do_qd)
      qlcnt += 1

Example Images

Probably too many images, but…

This one has overlap issues. And, it coloured between the same two transforms twice. But in opposite directions.

Curve Parameters
  Wheels: 4 wheels (shape(s): rhombus (r))
  Symmetry: k_fold = 4, congruency = 3
  Frequencies: [-1, -5, -13, 15]
  Widths: [1, 0.7083316355849565j, 0.6722628619691791, 0.4704579209315706j]
  Heights: [1, 0.5886618572957001j, 0.5851006789386932, 0.42151135774185294j]

Image Type Parameters
  Colouring: between transforms
  Sections: 16 colour sections per datarow pairing

Drawing Parameters
  Colour map: jet
  BG colour map: bone
  Line width (if used): 7

Transformations
  Number of transforms: 3
  Angles: [29.999999999999996, 149.99999999999997, 270.0]
  Base linear translation point: (0, 0.535) (0.375)
  Order for 1st Transform: [0, 1, 2]
  Order for 2nd Transform: [0, 2, 1]
  Colouring between row 2 and [1, 0]
mosaic style spirograpn image using colouring between 2D transforms

This one also had overlapping transform order lists. And it also only coloured between the same two transforms twice.

Maybe I do need to increase that counter for the loop checking for overlaps. That said, I do not check for the case where two transforms are coloured between twice.

Curve Parameters
  Wheels: 5 wheels (shape(s): circle (c))
  Symmetry: k_fold = 2, congruency = 1
  Frequencies: [-1, 5, -3, -3, -3]
  Widths: [1, 0.6135514273098623, 0.39079809644202923, 0.36040164219465387, 0.3004665663232805j]
  Heights: [1, 0.5369394330666124, 0.41355280607674894, 0.3893382597187634j, 0.31024382720911564]

Image Type Parameters
  Colouring: between transforms
  Sections: 16 colour sections per datarow pairing

Drawing Parameters
  Colour map: inferno
  BG colour map: Greys
  Line width (if used): 6

Transformations
  Number of transforms: 4
  Angles: [45.0, 135.0, 225.0, 315.0]
  Base linear translation point: (0, 1.166) (0.875)
  Order for 1st Transform: [2, 3, 0, 1]
  Order for 2nd Transform: [2, 0, 3, 1]
  Colouring between row 1 and [0, 3, 2]
mosaic style spirograpn image using colouring between 2D transforms

Finally, no overlaps and no repeated transforms.

Curve Parameters
  Wheels: 3 wheels (shape(s): circle (c))
  Symmetry: k_fold = 2, congruency = 1
  Frequencies: [3, 1, -1]
  Widths: [1, 0.6146456349033801j, 0.5967309007907682]
  Heights: [1, 0.502991808206289j, 0.4490941183250003]

Image Type Parameters
  Colouring: between transforms
  Sections: 16 colour sections per datarow pairing

Drawing Parameters
  Colour map: gnuplot
  BG colour map: viridis
  Line width (if used): 8

Transformations
  Number of transforms: 7
  Angles: [25.714285714285715, 77.14285714285714, 128.57142857142858, 180.0, 231.42857142857142, 282.85714285714283, 334.2857142857143]
  Base linear translation point: (0, 0.829) (0.75)
  Order for 1st Transform: [5, 3, 2, 1, 4, 6, 0]
  Order for 2nd Transform: [0, 4, 3, 5, 6, 1, 2]
  Colouring between row 2 and [1, 0]
mosaic style spirograpn image using colouring between 2D transforms

Let’s use multpliers on the second quadrant’s data.

Curve Parameters
  Wheels: 6 wheels (shape(s): equilateral triangle (q))
  Symmetry: k_fold = 4, congruency = 1
  Frequencies: [5, 13, 9, -7, -3, -11]
  Widths: [1, 0.504386052247972j, 0.38479228054707987, 0.1951965956155163, 0.125, 0.125j]
  Heights: [1, 0.5457999123877229, 0.28118895439187847j, 0.271641059153169j, 0.25769072589381214, 0.17413729046348428]

Image Type Parameters
  Colouring: between transforms
  Sections: 16 colour sections per datarow pairing
  Multipliers: [1.2 1.8 1.2 2.4 1.8]

Drawing Parameters
  Colour map: BuPu
  BG colour map: PuBuGn
  Line width (if used): 4

Transformations
  Number of transforms: 5
  Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
  Base linear translation point: (0, 0.729) (0.625)
  Order for 1st Transform: [3, 1, 4, 2, 0]
  Order for 2nd Transform: [2, 4, 1, 0, 3]
  Colouring between row 3 and [2, 1, 0, 4]
mosaic style spirograpn image using colouring between 2D transforms

Not sure why this one isn’t more symmetrical. Expect the multipliers have something to do with that.

Curve Parameters
  Wheels: 5 wheels (shape(s): ellipse (e))
  Symmetry: k_fold = 4, congruency = 2
  Frequencies: [2, 10, -6, 6, -10]
  Widths: [1, 0.6780861407698705, 0.5832492588753071, 0.570941172382802, 0.43443416942374957]
  Heights: [1, 0.5872171301500295, 0.5041877690290625, 0.3206537437040633j, 0.22584468295138332]

Image Type Parameters
  Colouring: between transforms
  Sections: 16 colour sections per datarow pairing
  Multipliers: [1.6 1.4 2.4 2.6]

Drawing Parameters
  Colour map: jet
  BG colour map: turbo
  Line width (if used): 5

Transformations
  Number of transforms: 6
  Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
  Base linear translation point: (0, 1.59) (0.75)
  Order for 1st Transform: [4, 0, 2, 1, 5, 3]
  Order for 2nd Transform: [3, 4, 5, 0, 2, 1]
  Colouring between row 1 and [0, 3, 2]
mosaic style spirograpn image using colouring between 2D transforms

And finally, another image that pretty much fills the available space.

Curve Parameters
  Wheels: 7 wheels (shape(s): rhombus (r))
  Symmetry: k_fold = 5, congruency = 1
  Frequencies: [-4, 1, 16, -9, 1, -19, 11]
  Widths: [1, 0.605109801904009j, 0.5158619524193508, 0.487708296658051, 0.3824170131638924, 0.30108604195726935, 0.26751562879634766j]
  Heights: [1, 0.6493942159505244, 0.504421898587099, 0.45684674843923145j, 0.44283812003230216j, 0.36305323358257374j, 0.32146630200255766j]

Image Type Parameters
  Colouring: between transforms
  Sections: 16 colour sections per datarow pairing
  Multipliers: [2.2 1.6 1.6 1.8 1.8 1.4]

Drawing Parameters
  Colour map: jet
  BG colour map: jet
  Line width (if used): 8

Transformations
  Number of transforms: 8
  Angles: [45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0, 360.0]
  Base linear translation point: (0, 0.667) (0.375)
  Order for 1st Transform: [5, 4, 0, 7, 1, 6, 2, 3]
  Order for 2nd Transform: [7, 1, 6, 4, 0, 2, 3, 5]
  Colouring between row 4 and [0, 2, 5, 1, 3]
mosaic style spirograpn image using colouring between 2D transforms

Done

I think that’s enough for this post. Some thoughts, some code, some images—what more could one ask for.

But, I looked back at my earlier work on this image type, and realized some of the images I liked best had overlapping transform pairs that were not drawn to the images. In fact some of them only had half the possible pairings drawn to the image.

So, I think I will add a form field allowing the user to chose whether or not to try and eliminate overlapping transform pairs. But that is for another day.

Take care and may your keyboard sing.

Resources