In the introduction to the last post I said:

I will return to using different marker types in the image type covered last post next time.

Well, I have instead decided to continue with the image type from the last post. I would like to modify it so that each line width cycle has the same maximum width. That means, I need to replace my use of percentages with some fixed increment value. I am going to look at fixing the maximum change at some percentage of the largest y value in the dataset. I expect the value of that percentage may take some tuning.

Equally Sized Line Width Cycles

I want to keep the option of using the percentage line widths; so, I am going to add another parameter to the function. Our data is a Numpy array, so getting the maximum y value should be simple enough. I am going to start by setting the maximum half width to 25% of of the maximum y value. Then a couple of if blocks and some potentially repeated code and we should be good to go.

I also had to deal with the fact that if not using percentages, I would be missing some variables used in the return statement. So, I declared all the necessary values setting them to None before the first if block. I did rename one of them and refactored the function code appropriately. For developement I set the new parameter to not use a fixed maximum width by default. No other code needed changing for now.

Oh yes! While working on this I decided I preferred larger colour blocks. So changed that line of code as well.

def cycle_lw_btw(axt, rxs, rys, u_pcnt=False, n_cyc=8, mx_grw=.5):
  # rxs, rys 1D arrays
  n_dots = len(rxs)
  f_cyc = n_dots // n_cyc
  h_cyc = f_cyc // 2
  # need return values even if not used to generate image
  min_i, mx_sz, c_inc, nbr_max, pp_clr = None, None, None, None, None
  ys_cyc = [[], []]
  if u_pcnt:
    min_i = 1
    c_inc = mx_grw / h_cyc
    mlt_1 = min_i
    mx_sz = mlt_1 + mx_grw
  else:
    mx_sz = rys.max() * 0.25
    c_inc = mx_sz / h_cyc
    f_inc = 0

  i_dir = +1

  c_len = len(g.cycle)
  c_ndx = rng.integers(0, c_len-2)
  c_jmp = c_len // 40
  c_jmp = int(c_len * 0.15)
  if c_jmp % 2 == 0:
    c_jmp += 1
  l_alph = 1

  nbr_max = 0
  for i in range(n_dots):
    if u_pcnt:
      mlt_1 += c_inc * i_dir
      if mlt_1 > mx_sz or mlt_1 < min_i:
        i_dir *= -1
        if mlt_1 > mx_sz:
          nbr_max += 1
        mlt_1 += (c_inc * i_dir)
      ys_cyc[0].append(rys[i] * mlt_1)
      ys_cyc[1].append(rys[i] * (1 / mlt_1))
    else:
      f_inc += c_inc * i_dir
      if f_inc > mx_sz or f_inc < 0:
        if f_inc > mx_sz:
          nbr_max += 1
        i_dir *= -1
        f_inc += c_inc * i_dir
      ys_cyc[0].append(rys[i] + f_inc)
      ys_cyc[1].append(rys[i] - f_inc)

  axt.autoscale()
  pp_clr = int(8 * (n_dots // 512))
  for i in range(0, n_dots, pp_clr):
    c_end = i + pp_clr
    if i == 0:
      axt.fill_between(rxs[i:c_end], ys_cyc[0][i:c_end], ys_cyc[1][i:c_end], alpha=l_alph, color=g.cycle[c_ndx])
    else:
      axt.fill_between(rxs[i-1:c_end], ys_cyc[0][i-1:c_end], ys_cyc[1][i-1:c_end], alpha=l_alph, color=g.cycle[c_ndx])
    c_ndx = (c_ndx + c_jmp) % c_len
    axt.autoscale()
  if c_end < n_dots:
    i += pp_clr - 1
    axt.fill_between(rxs[i:], ys_cyc[0][i:], ys_cyc[1][i:], alpha=l_alph, color=g.cycle[c_ndx])
    axt.autoscale()

  return min_i, mx_sz, c_inc, nbr_max, pp_clr

Examples

And a couple of examples.

Curve Parameters
  Wheels: 8 wheels (shape(s): ellipse (e))
  Symmetry: k_fold = 6, congruency = 2
  Frequencies: [-4, 14, 2, 2, 14, 14, -10, -16]
  Widths: [1, 0.6460666685858634, 0.39157537206628j, 0.3638686776229697, 0.30264421630106547, 0.2558862727326373, 0.17072973799888302, 0.1539712348425335]
  Heights: [1, 0.615297233210046j, 0.3604188463026202j, 0.22737230330140712j, 0.125, 0.125, 0.125, 0.125j]

Drawing Parameters
  Colour map: copper
  BG colour map: gist_earth
  Line width (if used): 6

Cycling Line Width
  Min line width multiplier: None
  Max line width multiplier: 0.295400380468389
  Half cycle frequency: 25
  Multiplier increment: 0.002307815472409289
  Number of maximums: 8
  Datapoints per colour section: 32
basic spirograpn image using colour between idea to simulate a cycling line width

#2 But, wheel shape and other factors can generate surprises. More like the mosiac image type than a cycling line width.

Curve Parameters
  Wheels: 5 wheels (shape(s): square (s))
  Symmetry: k_fold = 5, congruency = 4
  Frequencies: [-1, 24, -16, -1, -6]
  Widths: [1, 0.5962008905353797j, 0.38659779817974277, 0.22545292110383672j, 0.1323641747592296]
  Heights: [1, 0.6824766776944832, 0.3519815189472515, 0.21685340089398758j, 0.14802017615735433]

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

Cycling Line Width
  Min line width multiplier: None
  Max line width multiplier: 0.29991647171163494
  Half cycle frequency: 25
  Multiplier increment: 0.002343097435247148
  Number of maximums: 8
  Datapoints per colour section: 32
basic spirograpn image using colour between idea to simulate a cycling line width

And here’s one using longer colour segments.

Curve Parameters
  Wheels: 5 wheels (shape(s): ellipse (e))
  Symmetry: k_fold = 3, congruency = 2
  Frequencies: [2, -7, 14, -1, 14]
  Widths: [1, 0.532225718076067, 0.34294248161734847, 0.17452123821882892j, 0.14179142916980403j]
  Heights: [1, 0.6685085147743164, 0.6177495671213524, 0.49441345115049806j, 0.437410600176179j]

Drawing Parameters
  Colour map: default
  BG colour map: bone
  Line width (if used): 1

Cycling Line Width
  Min line width multiplier: None
  Max line width multiplier: 0.49925507081393194
  Half cycle frequency: 25
  Multiplier increment: 0.0039004302407338433
  Number of maximums: 8
  Datapoints per colour section: 48
basic spirograpn image using colour between idea to simulate a cycling line width

And one with longer colour segments using a different shape.

Curve Parameters
  Wheels: 6 wheels (shape(s): rhombus (r))
  Symmetry: k_fold = 3, congruency = 1
  Frequencies: [4, -5, 1, -8, 4, 1]
  Widths: [1, 0.6555591391554132, 0.6127847989179834j, 0.531417631008044j, 0.41216159580170314, 0.3209156019155599j]
  Heights: [1, 0.6220375727834137j, 0.5166137410611593j, 0.4308264885884102, 0.2705912199371687, 0.2339175810324277j]

Drawing Parameters
  Colour map: viridis
  BG colour map: YlGnBu
  Line width (if used): 3

Cycling Line Width
  Min line width multiplier: None
  Max line width multiplier: 0.20906275983122719
  Half cycle frequency: 25
  Multiplier increment: 0.0016333028111814624
  Number of maximums: 8
  Datapoints per colour section: 48
basic spirograpn image using colour between idea to simulate a cycling line width

I am liking the longer colour segments. Might need to rethink my current approach. For now, will leave it at the longer length.

That was incredibly painless and a quick refactoring.

Refactor Form and Other Pages

Given how quickly that went, I think I will now add this new image style to the home page. And, refactor the form to allow visitors to control some of the parameters for this image style.

New HTML Component

I decided I wanted to add the links to the image types on the home page to the help page as well. Didn’t want to maintain the list in multiple locations (DRY). So created a new template and moved the links section from the home page into it. Then used a Jinja include to add the link component to the two page templates in the appropriate spot.

    {% include 'image_links.html' %}

I don’t think I’ll bother showing the changes to those templates as pretty straightforward refactoring.

New Section in Curve Paratmeters Form

For now I am going to only offer three new parameters:

  • fixed or percentage increment
  • number of cycles per image (random or 2-16)
  • number of datapoints per colour segment (random or 4-32)

I have also decided to add a sub-type value to the dictionary passed to the template. The new section of the form looks like this.

    {% if f_data['subtyp'] == 'clw_btw' %}
    <div>
      <p style="margin-left:1rem;margin-top:0;font-size:110%;"><b>Cycling Line Width</b> (using fill_between)</p>
      <div class="radio">
        <p style="margin-left:1rem;margin-top:0;"><b>Use percentage line width increments:</b></p>
        <p>
          <label for="pcno">No: </label>
          <input type="radio" name="clw_pct" id="pcno" value="false" />
          <label for="pcyes">Yes: </label>
          <input type="radio" name="clw_pct" id="pcyes" value="true" />
        </p>
      </div>
      <div style="margin-top:-1rem;">
        <label for="clwc">Number of line width cycles: </label><br>
        <input type="text" name="clwc" placeholder="'r' for random, or 2 - 16"/>
      </div>
      <div style="margin-top:-1rem;">
        <label for="clwpp">Number of datapoints per colour segment: </label><br>
        <input type="text" name="clwpp" placeholder="'r' for random, or 1 - 32"/>
      </div>
    </div>
    {% endif %}  

Update Form Processing Functions and Code

Well, we are going to need some more global variables. The first set is those used to track form values. The second is those used to produce images. Which of course are based on the first set. But, they are often of different data types. I.E. string in the first, integer in the second.

... ...
# cycle line width w/fill_between
u_lwpct = None
u_lwcyc = None
u_dppc = None
... ...
# cycling line width related vars
# fill_between specific
use_pct = False   # use percentage based increments
n_lwcyc = 8       # number of line width cycles for image
dpp_clr = 12      # number of datapoints per colour segment

And, we will need to add some more fields to the two dictionaries in the function used to fill out any missing form variables, fini_curve_form(). The existing code should to the rest.

  dflts = {
    ... ...
    'clw_pct': 'false',
    'clwc': '8',
    'clwpp': '12'
  }

  fld_2_glbl = {
    ... ...
    'clw_pct': g.u_lwpct,
    'clwc': g.u_lwcyc,
    'clwpp': g.u_dppc
  }

And, a new section in proc_curve_form() to deal with the new fields.

... ...
    if 'clw_pct' in f_data:
      g.use_pct = True if f_data['clw_pct'] == 'true' else False
    if f_data['clwc'].isnumeric:
      cyc_tmp = int(f_data['clwc'])
      if cyc_tmp >= 2 and cyc_tmp <= 16:
        g.n_cyc = cyc_tmp
    elif f_data['clwc'] == 'r':
      g.n_cyc = rng.integers(2, 16, endpoint=True)
    if f_data['clwpp'].isnumeric:
      cyc_tmp = int(f_data['clwpp'])
      if cyc_tmp >= 1 and cyc_tmp <= 32:
        g.n_cyc = cyc_tmp
    elif f_data['clwpp'] == 'r':
      g.n_cyc = rng.integers(1, 32, endpoint=True)
... ...

Use New Values in Route Code

I have added another parameter to the image drawing function to cover the colour segment size. And refactored the function accordingly.

def cycle_lw_btw(axt, rxs, rys, u_pcnt=True, n_cyc=8, dppc=12, mx_grw=.25):

The route function now looks like this.

@app.route('/spirograph/basic_clw_btw', methods=['GET', 'POST'])
def sp_basic_clw_btw():
  pg_ttl = "Cycling Line Width using fill_between"
  if request.method == 'GET':
    f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cxsts': g.n_whl is not None,
              'cmaps': g.clr_opt, 'subtyp': 'clw_btw'
            }
    return render_template('get_curve.html', type='basic', f_data=f_data)
  elif request.method == 'POST':
    g.t_pts = 2048
    g.rds = np.linspace(0, 2*np.pi, g.t_pts)
    sal.proc_curve_form(request.form, 'basic')
    t_xs, t_ys, t_shp = sal.init_curve()

    ax.cla()
    g.rcm, g.cycle = sal.set_clr_map(ax, g.lc_frq, g.rcm)

    ax.autoscale()
    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)
    m1, m2 = 'o', 'o'
    mx_sz = 6500
    t_fst = 0
    p_step = 1

    ax.autoscale()
    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()

    # set number of datapoints back to default in case another image type is selected
    g.t_pts = 1024
    g.rds = np.linspace(0, 2*np.pi, g.t_pts)

    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 = {'mx_mlt': mx_mlt, 'mn_mlt': mn_mlt, 'm_inc': m_inc,
              'hf_frq': h_cyc, 'n_max': n_max, 'pp_clr': pp_clr,
              'u_pct': g.use_pct, 'n_lwc': g.n_lwcyc
              }

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

Refactor the Display Page

I have slightly modified the section showing the parameter values for this image type. Here’s the relevant bit.

    {% if 'mx_mlt' in p_data %}
    <h3>Cycling Line Width</h3>
    <ul class="dots">
      <li>Use percentage increment: {{ p_data['u_pct'] }}</li>
      <li>Number cycles: {{ p_data['n_lwc'] }}</li>
      {% if p_data['u_pct'] %}
      <li>Min line width multiplier: {{ p_data['mn_mlt'] }}</li>
      <li>Max line width multiplier: {{ p_data['mx_mlt'] }}</li>
      {% endif %}
      <li>Half cycle points: {{ p_data['hf_frq'] }}</li>
      {% if not p_data['u_pct'] %}
      <li>Multiplier increment: {{ p_data['m_inc'] }}</li>
      {% endif %}
      <li>Number of maximums: {{ p_data['n_max'] }}</li>
      <li>Datapoints per colour section: {{ p_data['pp_clr'] }}</li>
    </ul>
    {% endif %}

Examples

Here are a couple of examples using user (me) selected values.

Example 1

Curve Parameters
    Wheels: 8 wheels (shape(s): circle (c))
    Symmetry: k_fold = 7, congruency = 2
    Frequencies: [9, 9, -26, -26, -26, -26, 16, -19]
    Widths: [1, 0.7459570768371838j, 0.4460017106097187j, 0.28999672571075485, 0.21476466627835047j, 0.154609291279263j, 0.125, 0.125j]
    Heights: [1, 0.615632169910534, 0.37883408535269614j, 0.3072677096788647j, 0.1572460043773938j, 0.13931507369062132, 0.125, 0.125j]

Drawing Parameters
    Colour map: RdPu
    BG colour map: bone
    Line width (if used): 3

Cycling Line Width
    Use percentage increment: False
    Number cycles: 4
    Half cycle points: 256
    Multiplier increment: 0.0014781988436145018
    Number of maximums: 4
    Datapoints per colour section: 24
basic spirograpn image using colour between idea to simulate a cycling line width

Example 2

Curve Parameters
    Wheels: 5 wheels (shape(s): ellipse (e))
    Symmetry: k_fold = 3, congruency = 2
    Frequencies: [2, -10, -10, -4, -1]
    Widths: [1, 0.6206348044931083j, 0.5224670360187306j, 0.5160080173997058, 0.46791568050491883]
    Heights: [1, 0.5429491216677412, 0.28485682765812603, 0.2823193027830464j, 0.23856599792725142j]

Drawing Parameters
    Colour map: default
    BG colour map: Greys
    Line width (if used): 3

Cycling Line Width
    Use percentage increment: True
    Number cycles: 9
    Min line width multiplier: 1
    Max line width multiplier: 1.25
    Half cycle points: 113
    Number of maximums: 9
    Datapoints per colour section: 12
basic spirograpn image using colour between idea to simulate a cycling line width

Example 3

Curve Parameters
    Wheels: 9 wheels (shape(s): tetracuspid (t))
    Symmetry: k_fold = 7, congruency = 1
    Frequencies: [8, 29, 29, 29, -6, 8, -6, -20, 8]
    Widths: [1, 0.625643801461417, 0.44957486228566523, 0.30484575444061335, 0.29787513712228886j, 0.24525929557904047, 0.13775195974593035j, 0.125j, 0.125]
    Heights: [1, 0.6316339863188647, 0.4470661690206191, 0.4210251588204347j, 0.23883896810098482j, 0.125, 0.125j, 0.125j, 0.125j]
    Data points: 2048

Drawing Parameters
    Colour map: jet
    BG colour map: rainbow
    Line width (if used): 4
    Image size: 8
    Image DPI: 72

Cycling Line Width
    Use percentage increment: False
    Number cycles: 8
    Half cycle points: 128
    Multiplier increment: 0.0030519623553217318
    Number of maximums: 8
    Datapoints per colour section: 48
basic spirograpn image using colour between idea to simulate a cycling line width

Done

Until next time, keep practicing the happy finger drumming.

Resources