I did try getting the repeat route to use the same increment value used to generate the cycling line width style images. But, that just seemed to make things worse. And, because my code is really not properly documented, I am not sure what I am missing. And since I want to be able to do repeats on any of the image types (excluding animations), I have decided to do that refactoring I keep talking about; but, never quite getting around to it. Likely a waste of time for an app no one else will ever use. But, the experience might be good for my coding skills and my mental health. (I am starting this draft the day after we lost a family member pretty much liked by anyone who ever met him. RIP S.)

Do note, this series of posts may never get published. But I expect documenting what I do will help me document the app and deepen my learning experience. (Guess you can tell it did get published.)

Where to Start

I expect for something as cluttered as my app that is always an issue. But as I have been working on reproducing (repeating) cycling line width (clw) and mosaic images I thought I’d start with those. Those two paths both use the colouring between function provided by matplotlib. The idea is to refactor them into one or more functions, that I can call from the repeat path function as needed. Without needing to duplicate that code in the repeat path function. And, also call from the original path functions as well.

Hopefully there are pieces of code in both path functions that I can put into a new function(s) to make the code in the path functions substantially more DRY. And, that some of those functions will work to reduce the duplication in the other image generation paths when I get working on them. Going to take some thinking and repeated refactoring. But I expect it will pay off in the long run.

Let’s See What We Can See

I will start by looking at both route functions searching for similarities. And, see if I can move that into individual functions that I can call from both paths. Then I will look at moving the code that actually generates the image into individual or shared functions. For example, for the paths creating transforms it might be possible to write a single function they can all use to draw the transformed spirographs. But, that’s perhaps getting ahead of myself. Then again, seems to be a good idea to think about things like that as I go along.

Do note that I am only looking a refactoring the POST block of the routes.

What Is Being Duplicated

In every case we have to generate the curve data. There are already functions for portions of that. But I expect there may be some duplication in the two functions of the code that does that work. Let’s have a look.

Looks to me that these two routes and the one for the repeats all have the from processing and curve data generation repeated. As, similarly, is the code setting up the image and colour cycle. And, after plotting the curve, they all print the copyright message and plot the background.

Though the cycling line width route does mess around with the number of plotting points. Sets relevant variable(s) to 2,048 if the number is currently lower. First saving the old value so it can reset at the end of the route.

Most of the above is handled by a series of function calls. But perhaps we can write some new functions that make all those calls as necessary. Rather than repeating all the calls in each route. Right now I am thinking three functions. The first will process the form data, generate the curve data and return that as well as any other data needed by the route. The second will do the image set up prior to the curve being plotted. The third will do the image closing steps. The third may also do the image conversion to base 64; returning that to the route.

Quick look at some of the other routes says something similar happens in most of them. Though the gnarly style needs more curve data. And, the mosaic and cycling line width routes use different image data sets. Might present a bit of a problem.

Anyway, let’s give it a shot.

Form Processing and Curve Data Generation

Think this one will be called get_curve_data. It will take at least the form data, image type, and the number of plotting points as parameters.

First steps. I am starting with the calls from both the clw and mosaic paths.

def get_curve_data(i_type, f_data, p_pts):
  # cycling line width (clw)
  sal.proc_curve_form(request.form, 'basic')

  if g.t_pts < 2048:
    g.u_pts = str(g.t_pts)
    g.t_pts = 2048
  # g.t_pts = 2048
  g.rds = np.linspace(0, 2*np.pi, g.t_pts)

  t_xs, t_ys, t_shp = sal.init_curve()

  # mosaic
  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, g.r_skp, g.drp_f, g.t_sy)

  return ???

Okay, form processing same for both. And, curve initialization same for both. But we have that plotting point issue in the cycling line width (clw) route. And the mosaic route uses the reduced gnarly dataset as plotting between all the rows of the basic dataset did not produce nice images. And the clw route just uses the last row of data from the curve initialization call. Here’s what I think I will do.

One issue while testing. The original clw route used the t_xs and t_ys datasets. Not ther r_ versions. And I expect that will be the case for other spirograph types; so, an if block that will likely change over time

def get_curve_data(i_type, f_data, p_pts):
  if p_pts != g.t_pts:
    g.u_pts = str(g.t_pts)
    g.t_pts = p_pts
    g.rds = np.linspace(0, 2*np.pi, g.t_pts)

  t_xs, t_ys, t_shp = sal.init_curve()

  if i_type == 'mosaic':
    r_skp = 0
  else:
    r_skp = g.r_skp

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

  if i_type in ['gnarly', 'mosaic']:
    return r_xs, r_ys, m_xs, m_ys, m2_xs, m2_ys
  else:
    return t_xs, t_ys, m_xs, m_ys, m2_xs, m2_ys

The clw route still gets the basic dataset in r_xs and r_ys. And the mosaic route will get the reduced dataset. Of course the clw route will need to ignore a bunch of the returned data. For now the above function is going in the main module.

Time to test. Let’s refactor the routes.

# the pertinent part of clw route
  elif request.method == 'POST':
    t_xs, t_ys, t_shp, *_ = get_curve_data('clw_btw', request.form, 2048)

    ax.cla()

# the pertinent part of mosaic route
  elif request.method == 'POST':
    sal.cnt_btw = 0

    r_xs, r_ys, *_ = get_curve_data('clw_btw', request.form, g.t_pts)

    ax.clear()

# the pertinent part of the repeat route
 if request.method == 'GET' or request.method == 'POST':
    set_rpt_data()
    sal.cnt_btw = 0

    r_xs, r_ys, m_xs, m_ys, m2_xs, m2_ys = get_curve_data(r.d_rpt[r.do_nbr]['i_typ'], r.d_rpt[r.do_nbr], g.t_pts)

    ax.clear()

And, you know, that seems to work just fine.

Image Initialization

Once again here’s the current code from both routes, clw and mosaic. This is everything between the call to get_curve_data and the call to the function that plots the spirograph curve to the image.

def setup_image(ax):
# clw route
  ax.cla()
  g.rcm, g.cycle = sal.set_clr_map(ax, g.lc_frq, g.rcm)
  ax.autoscale()

# mosaic route
  ax.clear()
  ax.set_xlim(-0.1, 0.1)
  ax.set_ylim(-0.1, 0.1)
  if g.u_fgsz and g.fig_sz > 8:
    ax.margins(g.sz2mrg[g.u_fgsz])
  g.rcm, g.cycle = sal.set_clr_map(ax, g.mx_s, g.rcm)
  ax.autoscale()

  if g.u_again:
    sal.cnt_btw = 1
  else:
    sal.cnt_btw = 0

First of all, ax.cla() and ax.clear() both do the same thing. I will use cla(). No comments, so no longer know why I mess with the axis limits and margins in the mosaic route. And only the mosaic route has that code. So going to drop it for now. The if block setting or resetting the counter, cnt_btw, can also likely be left out. As it will only have any affect where used by any given route. Though it may turn out that it will need to be put in an appropriate if block down the road.

Had a look at other routes and a good number of them have the if block regarding setting the axis margins. Don’t know why clw route does not. So that one likely should stay. It is tied to some code I added allowing me to increase the size and dpi for generated images. I.E. images I am thinking about having printed. Also ended up leaving in the if block setting/unsetting the cnt_btw variable.

Do note, the function is all about the side-effects. I.E. changing the matplotlib figure/axes. And changing some global variables. Nothing gets returned. This is considered, by those that know, bad practice. As is the fact that the function is influenced by the contextual state (i.e. the current values of all the global variables I created. Bad, bad, bad!). Unfortunately I am not quite ready to tackle refactoring those issues; as that would take a complete redesign and recoding of the app.

That said, I am passing in the axis to be modified. And, we need to make sure we don’t change the curve colour map or the figures colour cycle when doing a repeat.

(When working on a later post had a bug using u.again in the following code. Changed it to u_agn. Problem gone!)

So we end up with the following.

def setup_image(ax):
  ax.cla()
  if g.u_fgsz and g.fig_sz > 8:
    ax.margins(g.sz2mrg[g.u_fgsz])
  g.rcm, g.cycle = sal.set_clr_map(ax, g.mx_s, g.rcm)
  ax.autoscale()

  if g.u_agn:
    sal.cnt_btw = 1
  else:
    sal.cnt_btw = 0

Let’s refactor the routes and test.

# pertinent part of clw route
  elif request.method == 'POST':
    t_xs, t_ys, *_ = get_curve_data('clw_btw', request.form, 2048)
    setup_image(ax)

    h_cyc, mn_mlt, mx_mlt, m_inc, n_max, pp_clr = sal.cycle_lw_btw(ax, t_xs[-1], t_ys[-1], u_pcnt=g.use_pct, n_cyc=g.n_lwcyc, dppc=g.dpp_clr)

# pertinent part of mosaic route
  elif request.method == 'POST':
    r_xs, r_ys, *_ = get_curve_data('clw_btw', request.form, g.t_pts)
    setup_image(ax)

    sal.btw_n_apart(ax, r_xs, r_ys, dx=g.bw_dx, ol=g.bw_o, r=g.bw_r, fix=None, mlt=g.bw_m, sect=g.bw_s)

# pertinent part of repeat route
  if request.method == 'GET' or request.method == 'POST':
    set_rpt_data()
    r_xs, r_ys, m_xs, m_ys, m2_xs, m2_ys = get_curve_data(r.d_rpt[r.do_nbr]['i_typ'], r.d_rpt[r.do_nbr], g.t_pts)
    setup_image(ax)

    if r.d_rpt[r.do_nbr]['i_typ'] == 'mosaic':
      sal.btw_n_apart(ax, r_xs, r_ys, dx=g.bw_dx, ol=g.bw_o, r=g.bw_r, fix=None, mlt=g.bw_m, sect=g.bw_s)
    elif r.d_rpt[r.do_nbr]['i_typ'] == 'clw_btw':
      h_cyc, mn_mlt, mx_mlt, m_inc, n_max, pp_clr = sal.cycle_lw_btw(ax, r_xs[-1], r_ys[-1], u_pcnt=g.use_pct, n_cyc=g.n_lwcyc, dppc=g.dpp_clr)

Didn’t quite work. The call to set_clr_map in setup_image was not using the colourmap and colour cycle specified by the repeat data. So, a wee refactoring. Changed the following to the version below.

  # original
  if u_cmap in ['default', 'tab20', 'tab20c', 'twilight_shifted']:
    c_cycle = [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, n_vals+1)][:-1]
  else:
    if c_rng % 2 == 0:
      c_cycle = [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, c_rng+1)] + [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, c_rng+4)][2:-2][::-1]
    else:
      c_cycle = [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, c_rng+1)] + [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, c_rng+4)][2:-2][::-1]
  c_cycle = c_cycle[:n_vals]

  ax.set_prop_cycle('color', c_cycle)
  # modified version
  if not g.do_rpt:
    if u_cmap in ['default', 'tab20', 'tab20c', 'twilight_shifted']:
      c_cycle = [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, n_vals+1)][:-1]
    else:
      if c_rng % 2 == 0:
        c_cycle = [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, c_rng+1)] + [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, c_rng+4)][2:-2][::-1]
      else:
        c_cycle = [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, c_rng+1)] + [bgcm_mpl[u_cmap](i) for i in np.linspace(0, 1, c_rng+4)][2:-2][::-1]
    c_cycle = c_cycle[:n_vals]
  else:
    u_cmap = g.rcm
    c_cycle = g.cycle

  ax.set_prop_cycle('color', c_cycle)

And that seemed to fix things. The repeat is coming out very close to the original.

attempt to repeat the test image
Attempt to repeat a cycling line width spirograph image.

Done

This is getting to be a bit lengthy. So I think I will take a break and continue in the next post.

It is a little satisfying getting this started and the initial refactoring to work reasonably well.

Hope your time at the keyboard is at least as satisfying as mine was this last day or two.