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.
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
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
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
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.