Latest Blog Posts http://localhost/latest.atom/ 2023-02-11T12:00:00Z http://localhost/ Werkzeug Vector graphics wallpapers with python http://localhost/blog/svg-wallpapers-with-python/ 2023-02-11T12:00:00Z 2023-02-11T12:00:00Z <p><img class="rounded mx-auto shadow-lg d-block" src="../../static/img/hexsin.svg" alt="sin wave on hexagonal lattice"></p> <p class="text-muted small text-end">Graphic by author </p> <hr> <h1 id="tldr">TL;DR</h1> <p>If you are not interested in any of the code or math but only want to see pretty graphics - jump straight to the <a href="#graphics">graphics</a> section.</p> <h1 id="introduction">Introduction</h1> <p>In the past months I've been working with a lot with vector graphics. Although I wouldn't call myself a graphic designer, I was mistaken for one a couple of times at tech events. I quite enjoy creative design work since it's a really refreshing mode change from writing code or looking at data. I've always enjoyed making things look aesthetically pleasing but in my work that was mainly limited around creating charts and data visualizations, arranging those into dashboards and laying it out in some app. Occasionally I'm creating diagrams, but that's mostly for technical purposes.</p> <p>Most of the work with vector graphics I've done to date was in <a href="https://inkscape.org/">Inkscape</a> which I fell most comfortable with since I've been using on and off for a few good years. Although I've tried more modern, web based tools like Canva or Figma but whenever I had something a bit more advanced I always went back to Inkscape. All tools have their strengths, but they all require some learning investment before becoming productive. Inkscape turned out to be a great investment for me, but I feel I could do much more with vector graphics if I could tap into my python skills. </p> <p>For some time I wanted to learn how to create vector graphics with python. I've watched a few conference talks on creative coding and the whole concept resonated with me instantly. I started exploring what's possible around the stack I'm familiar with. A good place to start was learning a bit more about <a href="https://developer.mozilla.org/en-US/docs/Web/SVG">SVG</a> itself which stands for <strong>Scalable Vector Graphics</strong>, and it is an XML-based markup language that describes 2D vector graphics. For our purposes we won't need to go very deep and only explore a few <code>elements</code> and <code>attributes</code> and that'll be sufficient to start creating graphics.</p> <p>What I would like to do is to be able to create repeated patterns (or tilings) of simple geometric shapes and apply a few effects to make it a bit less boring. I am aware that that Inkscape has a tool to create tilings based on repeating patterns, but I couldn't fully do what I had in mind and I wanted to try creating graphics programmatically. There is also a practical aspect to this project since I would like to come up with a design to cover a few windows with in a restaurant to increase privacy and comfort for guests.</p> <p>I'll start with a simple square lattice<sup id="fnref:1"><a class="footnote-ref" href="#fn:1">1</a></sup> and insert simple shapes like squares on the lattice positions. The plan is to start modifying the shapes to create interesting effects. In the end I'll repeat the whole set of designs on a hexagonal to compare the results.</p> <h2 id="creating-svg-graphics-with-python">Creating SVG graphics with python</h2> <p>As you might've expected, there are python packages out there, that can help you create SVGs without having to write raw XML and worrying about matching tags and debugging typos. I'll use <a href="https://svgwrite.readthedocs.io/en/latest/">svgwrite</a> since it works out of the box with JupyterLab. Alternatively you can try <a href="https://github.com/cduck/drawSvg">drawSvg</a>, they both seem to have a very similar API.</p> <p>I'm doing most of my coding here in a jupyter notebook it might be worth mentioning that notebooks can display SVGs natively. They can display SVG content directly in a cell either from a file or from SVG source with the handy <code>%%SVG</code> cell magic</p> <div class="codehilite"><pre><span></span><code>%%SVG <span class="nt">&lt;svg</span> <span class="na">height=</span><span class="s">&quot;210&quot;</span> <span class="na">width=</span><span class="s">&quot;500&quot;</span><span class="nt">&gt;</span> <span class="nt">&lt;polygon</span> <span class="na">points=</span><span class="s">&quot;200,10 300,180 100,180&quot;</span> <span class="na">style=</span><span class="s">&quot;fill:darkcyan;stroke:tomato;stroke-width:2&quot;</span> <span class="nt">/&gt;</span> <span class="nt">&lt;/svg&gt;</span> </code></pre></div> <p>should give you the following in the output cell:</p> <p><svg height="210" width="500"> <polygon points="200,10 300,180 100,180" style="fill:darkcyan;stroke:tomato;stroke-width:2" /> </svg> </p> <p>Alternatively you can also use the builtin display system of IPython:</p> <div class="codehilite"><pre><span></span><code><span class="kn">from</span> <span class="nn">IPython.core.display</span> <span class="kn">import</span> <span class="n">SVG</span> <span class="n">SVG</span><span class="p">(</span><span class="s2">&quot;&quot;&quot;&lt;svg height=&quot;210&quot; width=&quot;500&quot;&gt;</span> <span class="s2">&lt;polygon</span> <span class="s2"> points=&quot;100,200 200,200 150.0,113.39745962155614&quot;</span> <span class="s2"> style=&quot;fill:darkcyan;stroke:tomato;stroke-width:2&quot;</span> <span class="s2">/&gt;</span> <span class="s2">&lt;polygon</span> <span class="s2"> points=&quot;200,200 300,200 250.0,113.39745962155614&quot;</span> <span class="s2"> style=&quot;fill:darkcyan;stroke:tomato;stroke-width:2&quot;</span> <span class="s2">/&gt;</span> <span class="s2">&lt;/svg&gt;&quot;&quot;&quot;</span><span class="p">)</span> </code></pre></div> <p>should in turn produce:</p> <p><svg height="110" width="500"> <polygon points="100,100 200,100 150.0,13.39745962155614" style="fill:darkcyan;stroke:tomato;stroke-width:2" /> <polygon points="200,100 300,100 250.0,13.39745962155614" style="fill:darkcyan;stroke:tomato;stroke-width:2" /> </svg></p> <p>As you can see SVG is quite straightforward. The drawing in enclosed in the <code>&lt;svg&gt;...&lt;/svg&gt;</code> tags, and I'm using the <code>polygon</code> element to draw the shapes by specifying the coordinates of all vertices. The appearance is then modifies with the <code>style</code> attribute in a CSS-like syntax.</p> <p>Turning to python and making graphics with <code>svgwrite</code> requires creating python representation of the drawing and adding shapes to it. I'm only going to use <code>rect</code>, <code>circle</code> and <code>polygon</code> elements with a few standard attributes like: <code>fill</code>, <code>stroke</code> and <code>opacity</code>. Here's an example that illustrates basic functionality:</p> <div class="codehilite"><pre><span></span><code><span class="kn">import</span> <span class="nn">svgwrite</span> <span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="o">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">filename</span><span class="o">=</span><span class="s2">&quot;example.svg&quot;</span><span class="p">,</span> <span class="n">debug</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="mi">400</span><span class="p">,</span> <span class="mi">140</span><span class="p">))</span> <span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="o">.</span><span class="n">rect</span><span class="p">(</span> <span class="n">insert</span><span class="o">=</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span> <span class="mi">20</span><span class="p">),</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">50</span><span class="p">),</span> <span class="n">fill</span><span class="o">=</span><span class="s2">&quot;darkcyan&quot;</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">&quot;none&quot;</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="mf">0.7</span><span class="p">))</span> <span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="o">.</span><span class="n">rect</span><span class="p">(</span> <span class="n">insert</span><span class="o">=</span><span class="p">(</span><span class="mi">180</span><span class="p">,</span> <span class="mi">20</span><span class="p">),</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span> <span class="mi">100</span><span class="p">),</span> <span class="n">fill</span><span class="o">=</span><span class="s2">&quot;darkcyan&quot;</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">&quot;none&quot;</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="mf">1.0</span><span class="p">))</span> <span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="o">.</span><span class="n">circle</span><span class="p">(</span> <span class="n">center</span><span class="o">=</span><span class="p">(</span><span class="mi">290</span><span class="p">,</span> <span class="mi">60</span><span class="p">),</span> <span class="n">r</span><span class="o">=</span><span class="mi">40</span><span class="p">,</span> <span class="n">fill</span><span class="o">=</span><span class="s2">&quot;tomato&quot;</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">&quot;none&quot;</span><span class="p">))</span> <span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">dwg</span><span class="o">.</span><span class="n">polygon</span><span class="p">(</span> <span class="n">points</span><span class="o">=</span><span class="p">[(</span><span class="mi">350</span><span class="p">,</span> <span class="mi">120</span><span class="p">),</span> <span class="p">(</span><span class="mi">400</span><span class="p">,</span> <span class="mi">120</span><span class="p">),</span> <span class="p">(</span><span class="mf">375.0</span><span class="p">,</span> <span class="mf">13.5</span><span class="p">)],</span> <span class="n">fill</span><span class="o">=</span><span class="s2">&quot;tomato&quot;</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="s2">&quot;none&quot;</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="mf">0.8</span><span class="p">))</span> </code></pre></div> <p><svg baseProfile="full" height="140" version="1.1" width="400" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs /><rect fill="darkcyan" height="50" opacity="0.7" stroke="none" width="100" x="50" y="20" /><rect fill="darkcyan" height="100" opacity="1.0" stroke="none" width="50" x="180" y="20" /><circle cx="290" cy="60" fill="tomato" r="40" stroke="none" /><polygon fill="tomato" opacity="0.8" points="350,120 400,120 375.0,13.5" stroke="none" /></svg></p> <p>That's it! We now know enough about SVG to start producing more advanced designs programmatically.</p> <h2 id="square-lattice">Square lattice</h2> <p>Square lattice is the best place to start since it's the simplest. What I want, specifically is a regular grid of points whose coordinates will be expressed as the SVG canvas coordinates<sup id="fnref:2"><a class="footnote-ref" href="#fn:2">2</a></sup>. Once we have the coordinates we can start putting shapes in those locations. To define a square lattice it is sufficient to define a single parameter <code>a</code> which is the lattice parameter<sup id="fnref:3"><a class="footnote-ref" href="#fn:3">3</a></sup>. Defining the lattice and size of our graphics will allow us to determine how many tiles we'll be able to fit on the graphic of a given size. Here's how it might look in code:</p> <div class="codehilite"><pre><span></span><code><span class="nd">@dataclass</span> <span class="k">class</span> <span class="nc">SquareLattice</span><span class="p">:</span> <span class="s2">&quot;Square Lattice in 2D&quot;</span> <span class="n">a</span><span class="p">:</span> <span class="nb">float</span> <span class="k">def</span> <span class="nf">coords</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">row</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">col</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">center</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">True</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">:</span> <span class="s2">&quot;Coordinates for a lattice position (row, col)&quot;</span> <span class="k">if</span> <span class="n">center</span><span class="p">:</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span> <span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="p">(</span><span class="n">row</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">),</span> <span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="p">(</span><span class="n">col</span> <span class="o">+</span> <span class="mf">0.5</span><span class="p">)],</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">)</span> <span class="k">else</span><span class="p">:</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="n">row</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="n">col</span><span class="p">],</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">float</span><span class="p">)</span> <span class="k">def</span> <span class="nf">size</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">:</span> <span class="s2">&quot;Size of the lattice in lattice postions&quot;</span> <span class="n">s</span> <span class="o">=</span> <span class="n">math</span><span class="o">.</span><span class="n">ceil</span><span class="p">(</span><span class="n">width</span> <span class="o">/</span> <span class="bp">self</span><span class="o">.</span><span class="n">a</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="n">s</span><span class="p">,</span> <span class="n">s</span><span class="p">],</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">int</span><span class="p">)</span> </code></pre></div> <p><code>SquareLatttice</code> takes a single parameter <code>a</code> and with the <code>coords</code> method will generate <code>(x, y)</code> coordinates for each of the integer lattice positions <code>(i, j)</code>. To see how it works let's generate a few coordinate pairs:</p> <div class="codehilite"><pre><span></span><code><span class="n">lat</span> <span class="o">=</span> <span class="n">SquareLattice</span><span class="p">(</span><span class="n">a</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;coordiantes for position (0, 0): </span><span class="si">{</span><span class="n">lat</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;coordiantes for position (0, 0): </span><span class="si">{</span><span class="n">lat</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;coordiantes for position (0, 0): </span><span class="si">{</span><span class="n">lat</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span> <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;coordiantes for position (0, 0): </span><span class="si">{</span><span class="n">lat</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span> <span class="n">coordiantes</span> <span class="k">for</span> <span class="n">position</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">):</span> <span class="p">[</span><span class="mf">5.</span> <span class="mf">5.</span><span class="p">]</span> <span class="n">coordiantes</span> <span class="k">for</span> <span class="n">position</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">):</span> <span class="p">[</span><span class="mf">15.</span> <span class="mf">5.</span><span class="p">]</span> <span class="n">coordiantes</span> <span class="k">for</span> <span class="n">position</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">):</span> <span class="p">[</span> <span class="mf">5.</span> <span class="mf">15.</span><span class="p">]</span> <span class="n">coordiantes</span> <span class="k">for</span> <span class="n">position</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">):</span> <span class="p">[</span><span class="mf">25.</span> <span class="mf">25.</span><span class="p">]</span> </code></pre></div> <p>By default, we get coordinates for the center of each cell. It will only be important later on, but it's good to notice. </p> <p>Now that we have the lattice positions we can put some elements there. For a square lattice, squares are a good starting point. Here's how we can create the drawing and repeat squares on the predefined lattice:</p> <div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">draw_squares</span><span class="p">(</span> <span class="n">lattice</span><span class="p">:</span> <span class="n">Lattice</span><span class="p">,</span> <span class="n">side</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">15</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span> <span class="n">fill</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;darkcyan&quot;</span><span class="p">,</span> <span class="n">stroke</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;none&quot;</span><span class="p">,</span> <span class="n">opacity</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span> <span class="p">):</span> <span class="s2">&quot;Draw SVG squares on a lattice&quot;</span> <span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="o">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span> <span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span> <span class="o">=</span> <span class="n">lattice</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">xsize</span><span class="p">):</span> <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">ysize</span><span class="p">):</span> <span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span> <span class="n">dwg</span><span class="o">.</span><span class="n">rect</span><span class="p">(</span> <span class="n">insert</span><span class="o">=</span><span class="n">lattice</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">side</span><span class="p">,</span> <span class="n">side</span><span class="p">),</span> <span class="n">fill</span><span class="o">=</span><span class="n">fill</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="n">stroke</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="n">opacity</span><span class="p">,</span> <span class="p">)</span> <span class="p">)</span> <span class="k">return</span> <span class="n">dwg</span> </code></pre></div> <p>Since we can independently control the lattice spacing and size of the squares let's see a few variants:</p> <div class="row justify-content-center py-3"> <div class="col-4 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a40-square-5.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Lattice a=40, square size=5</figcaption> </figure> </div> <div class="col-4 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a40-square-20.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Lattice a=40, square size=20</figcaption> </figure> </div> <div class="col-4 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a90-square-80.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Lattice a=90, square size=80</figcaption> </figure> </div> </div> <p>Nothing spectacular yet but we need this foundation to be able to quickly modify the designs.</p> <h3 id="making-things-more-interesting">Making things more interesting</h3> <p>There are many ways to modify the grids we've produced so far to make it more interesting. Obvious example is probably changing the color channel but before going there I will try changing the size of the squares. The effect that I think it will have to imitate brightness where smaller shapes will be less intense and bigger elements more intense. Alternatively I would try to modify <em>opacity</em> in a similar fashion, but I think encoding brightness with size will be a nicer aesthetic. </p> <p>I want to simulate a fade effect with a square size gradient. I would like to try out a few different gradient functions, but I'll start with the linear gradient along the <em>y</em> axis, that we'll use to modify elements.</p> <p>With the function below I'm going to be able to create an array with coefficients <code>[0.0, 1.0]</code> to scale the squares by. Since the gradient will be applied along the <em>y</em> axis the size of the gradient array is the same as the number of lattice points in the <em>y</em> direction.</p> <div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">linear_gradient</span><span class="p">(</span> <span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">offset</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">,</span> <span class="n">geom</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span><span class="p">,</span> <span class="n">start</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.01</span> <span class="p">):</span> <span class="n">c</span> <span class="o">=</span> <span class="n">math</span><span class="o">.</span><span class="n">floor</span><span class="p">(</span><span class="n">size</span> <span class="o">*</span> <span class="n">offset</span><span class="p">)</span> <span class="k">if</span> <span class="n">geom</span><span class="p">:</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">hstack</span><span class="p">((</span> <span class="n">np</span><span class="o">.</span><span class="n">geomspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="n">c</span><span class="p">,</span> <span class="n">endpoint</span><span class="o">=</span><span class="kc">False</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="p">))</span> <span class="k">else</span><span class="p">:</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">hstack</span><span class="p">((</span> <span class="n">np</span><span class="o">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="n">c</span><span class="p">,</span> <span class="n">endpoint</span><span class="o">=</span><span class="kc">False</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="p">))</span> </code></pre></div> <p>To make it work we'll need a small modification to the drawing function to take <code>gradient</code> argument and scale the squares with it. Here's the updated function:</p> <div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">draw_squares</span><span class="p">(</span> <span class="n">lattice</span><span class="p">:</span> <span class="n">Lattice</span><span class="p">,</span> <span class="n">side</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">15</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span> <span class="n">fill</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;darkcyan&quot;</span><span class="p">,</span> <span class="n">stroke</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;none&quot;</span><span class="p">,</span> <span class="n">opacity</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span> <span class="hll"> <span class="n">gradient</span><span class="p">:</span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span> </span><span class="p">):</span> <span class="s2">&quot;Draw SVG squares on a lattice with gradient&quot;</span> <span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="o">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span> <span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span> <span class="o">=</span> <span class="n">lattice</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">xsize</span><span class="p">):</span> <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">ysize</span><span class="p">):</span> <span class="hll"> <span class="n">size</span> <span class="o">=</span> <span class="n">side</span> <span class="o">*</span> <span class="n">gradient</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> </span> <span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span> <span class="n">dwg</span><span class="o">.</span><span class="n">rect</span><span class="p">(</span> <span class="n">insert</span><span class="o">=</span><span class="n">lattice</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="hll"> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">size</span><span class="p">,</span> <span class="n">size</span><span class="p">),</span> </span> <span class="n">fill</span><span class="o">=</span><span class="n">fill</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="n">stroke</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="n">opacity</span><span class="p">,</span> <span class="p">)</span> <span class="p">)</span> <span class="k">return</span> <span class="n">dwg</span> </code></pre></div> <p>Putting it all together and generating the grid with the gradient gives the following result</p> <div class="row justify-content-center py-3"> <div class="col text-center"> <figure class="figure"> <img src="../../static/img/square-lattice-a40-square-15-gradient.svg" width="75%" class="mb-2"> <figcaption class="figure-caption">Lattice a=40, square size=15 with a linear gradient</figcaption> </figure> </div> </div> <p>If we draw squares that are a bit larger we'll notice a slight problem with the current approach since the default <code>rect</code> element is drawn with respect to top left corner which results in an alignment to the left.</p> <div class="row justify-content-center py-3"> <div class="col text-center"> <figure class="figure"> <img src="../../static/img/square-lattice-a80-square-75-gradient-left.svg" width="75%" class="mb-2"> <figcaption class="figure-caption">Lattice a=80, square size=75 with a linear gradient</figcaption> </figure> </div> </div> <p>Although it might be desirable is some cases I would rather have all the squares centered with respect to the lattice positions. To make that happen I created a custom <code>Square</code> class that I'll use with the <code>polygon</code> element instead of <code>rect</code>. Here's the <code>Square</code> class and updated <code>draw_squares</code> function</p> <div class="codehilite"><pre><span></span><code><span class="nd">@dataclass</span> <span class="k">class</span> <span class="nc">Square</span><span class="p">:</span> <span class="s2">&quot;Square&quot;</span> <span class="n">side</span><span class="p">:</span> <span class="nb">int</span> <span class="k">def</span> <span class="nf">coords</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="s2">&quot;Vertex coordinates of a square with repect to its center (0, 0)&quot;</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span> <span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span> <span class="p">])</span> <span class="k">def</span> <span class="nf">draw_squares</span><span class="p">(</span> <span class="n">lattice</span><span class="p">:</span> <span class="n">Lattice</span><span class="p">,</span> <span class="n">side</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">15</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">800</span><span class="p">,</span> <span class="n">fill</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;darkcyan&quot;</span><span class="p">,</span> <span class="n">stroke</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;none&quot;</span><span class="p">,</span> <span class="n">opacity</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">1.0</span><span class="p">,</span> <span class="n">gradient</span><span class="p">:</span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span> <span class="p">):</span> <span class="s2">&quot;Draw SVG squares on a lattice&quot;</span> <span class="n">dwg</span> <span class="o">=</span> <span class="n">svgwrite</span><span class="o">.</span><span class="n">Drawing</span><span class="p">(</span><span class="n">debug</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">size</span><span class="o">=</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">))</span> <span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span> <span class="o">=</span> <span class="n">lattice</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">xsize</span><span class="p">):</span> <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">ysize</span><span class="p">):</span> <span class="n">square</span> <span class="o">=</span> <span class="n">Square</span><span class="p">(</span><span class="n">side</span><span class="o">=</span><span class="n">side</span> <span class="o">*</span> <span class="n">gradient</span><span class="p">[</span><span class="n">j</span><span class="p">])</span> <span class="n">points</span> <span class="o">=</span> <span class="n">square</span><span class="o">.</span><span class="n">coords</span><span class="p">()</span> <span class="o">+</span> <span class="n">lattice</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">)</span> <span class="n">dwg</span><span class="o">.</span><span class="n">add</span><span class="p">(</span> <span class="n">dwg</span><span class="o">.</span><span class="n">polygon</span><span class="p">(</span> <span class="n">points</span> <span class="o">=</span> <span class="n">points</span><span class="p">,</span> <span class="n">fill</span><span class="o">=</span><span class="n">fill</span><span class="p">,</span> <span class="n">stroke</span><span class="o">=</span><span class="n">stroke</span><span class="p">,</span> <span class="n">opacity</span><span class="o">=</span><span class="n">opacity</span><span class="p">,</span> <span class="p">)</span> <span class="p">)</span> <span class="k">return</span> <span class="n">dwg</span> </code></pre></div> <p>Redrawing the previous graphic with the updates gives the expected result where all the squares are now centered around lattice positions.</p> <div class="row justify-content-center py-3"> <div class="col text-center"> <figure class="figure"> <img src="../../static/img/square-lattice-a80-square-75-gradient-center.svg" width="75%" class="mb-2"> <figcaption class="figure-caption">Lattice a=80, square size=75 with a linear gradient centered</figcaption> </figure> </div> </div> <h3 id="ridge">Ridge</h3> <p>With a slight modification to the <code>linear_gradient</code> function we can make the gradient symmetric with respect to the center. I'm calling it <code>ridge</code> gradient, and it should create a fade effect going both up and down.</p> <div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">ridge</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">geom</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span><span class="p">,</span> <span class="n">start</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.01</span><span class="p">):</span> <span class="n">c</span> <span class="o">=</span> <span class="nb">round</span><span class="p">(</span><span class="n">size</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> <span class="k">if</span> <span class="n">geom</span><span class="p">:</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">hstack</span><span class="p">((</span> <span class="n">np</span><span class="o">.</span><span class="n">geomspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="n">c</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">geomspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">c</span><span class="p">)[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="p">))</span> <span class="k">else</span><span class="p">:</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">hstack</span><span class="p">((</span> <span class="n">np</span><span class="o">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">size</span> <span class="o">-</span> <span class="n">c</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">linspace</span><span class="p">(</span><span class="n">start</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">c</span><span class="p">)[::</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="p">))</span> </code></pre></div> <p>Applying the ridge gradient give a nicely symmetric pattern.</p> <div class="row justify-content-center py-3"> <div class="col text-center"> <figure class="figure"> <img src="../../static/img/square-lattice-a20-square-15-gradient-ridge.svg" width="75%" class="mb-2"> <figcaption class="figure-caption">Lattice a=20, square size=15 with a ridge gradient</figcaption> </figure> </div> </div> <p>I guess it's easy to see how to modify the current code to change the direction or produce a different fade pattern. In the next step we're going to update the gradient from 1D to 2D to allow for more sophisticated patterns.</p> <h3 id="radial-gradient">Radial gradient</h3> <p>So far we've applied gradient along a single axis, but there is nothing preventing us from extending the gradient to 2D. One familiar concept we can try to implement is radial gradient. We'll need to modify the function for drawing the squares to modify the size of squares based on both <code>i</code> and <code>j</code> lattice positions, and we'll need a 2D array with sizes representing brightness. The <code>radial_gradient</code> function evaluates the distance between the <code>origin</code> of the gradient and given lattice position and scales the value to <code>[0.0, 1.0]</code> range within the specified <code>radius</code>. </p> <div class="codehilite"><pre><span></span><code><span class="k">def</span> <span class="nf">radial_gradient</span><span class="p">(</span> <span class="n">lattice</span><span class="p">:</span> <span class="n">Lattice</span><span class="p">,</span> <span class="n">origin</span><span class="p">:</span> <span class="n">np</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">,</span> <span class="n">radius</span><span class="p">:</span> <span class="nb">float</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">height</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">:</span> <span class="s2">&quot;Compute brightness for a radial gradient&quot;</span> <span class="k">def</span> <span class="nf">radial_distance</span><span class="p">(</span><span class="n">origin</span><span class="p">,</span> <span class="n">coords</span><span class="p">,</span> <span class="n">radius</span><span class="p">):</span> <span class="n">distance</span> <span class="o">=</span> <span class="n">euclidean</span><span class="p">(</span><span class="n">origin</span><span class="p">,</span> <span class="n">coords</span><span class="p">)</span> <span class="k">return</span> <span class="nb">min</span><span class="p">(</span><span class="n">distance</span> <span class="o">/</span> <span class="n">radius</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">)</span> <span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span> <span class="o">=</span> <span class="n">lattice</span><span class="o">.</span><span class="n">size</span><span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span> <span class="n">gradient</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">ones</span><span class="p">((</span><span class="n">xsize</span><span class="p">,</span> <span class="n">ysize</span><span class="p">))</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">xsize</span><span class="p">):</span> <span class="k">for</span> <span class="n">j</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">ysize</span><span class="p">):</span> <span class="n">gradient</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span> <span class="n">gradient</span><span class="p">[</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">],</span> <span class="n">radial_distance</span><span class="p">(</span><span class="n">origin</span><span class="p">,</span> <span class="n">lattice</span><span class="o">.</span><span class="n">coords</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="n">radius</span><span class="p">)</span> <span class="p">)</span> <span class="k">return</span> <span class="n">gradient</span> </code></pre></div> <p>Now the returned array is 2D and can be applied to our grid. The modification to <code>draw_squares</code> is straightforward, so I'll not repeat the code here. Since the number in the gradient array are scaled to <code>[0.0, 1.0]</code> we can also compute the inverse gradient by subtracting the gradient from 1. Here are the two variants we get:</p> <div class="row justify-content-center py-3"> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a20-square-15-radial-grad.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Lattice a=20, square size=15, radial gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a20-square-15-radial-grad-inverse.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Lattice a=20, square size=15, inverse radial gradient</figcaption> </figure> </div> </div> <p>A nice effect here is that side by side these look like the right hand side one is "taken out" from the left hand side one. </p> <h2 id="hexagonal-lattice">Hexagonal lattice</h2> <p>Square lattice is not the only choice for a lattice. It's the most symmetric looking, and therefore it has an artificial or dry aesthetic. To check the effect that a lattice might have on the overall graphic I'll try also a hexagonal lattice also known as honeycomb structure.</p> <p>Just as in the case of a square lattice we need only a single parameter <code>a</code> to define the lattice. Since for the hexagonal lattice the translation vectors are no longer parallel we need to take that into account when computing lattice coordiantes<sup id="fnref:4"><a class="footnote-ref" href="#fn:4">4</a></sup>.</p> <div class="codehilite"><pre><span></span><code><span class="nd">@dataclass</span> <span class="k">class</span> <span class="nc">HexLattice</span><span class="p">:</span> <span class="s2">&quot;Hexagonal lattice&quot;</span> <span class="n">a</span><span class="p">:</span> <span class="nb">float</span> <span class="nd">@property</span> <span class="k">def</span> <span class="nf">vectors</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([[</span><span class="bp">self</span><span class="o">.</span><span class="n">a</span><span class="p">,</span> <span class="mi">0</span><span class="p">],</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">a</span> <span class="o">*</span> <span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="o">/</span> <span class="mi">2</span><span class="p">]])</span> <span class="k">def</span> <span class="nf">coords</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">row</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">col</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">npt</span><span class="o">.</span><span class="n">ArrayLike</span><span class="p">:</span> <span class="s2">&quot;Coordinates for a lattice position (row, col)&quot;</span> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">vectors</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">row</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">vectrors</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="n">col</span> </code></pre></div> <p>With the lattice defined I would like to have regular hexagons in cell positions in analogy to having squares on a square lattice. For a regular hexagon with the side <code>a</code> here is a diagram with the coordinates of the vertices with respect to the center of the hexagon<sup id="fnref:5"><a class="footnote-ref" href="#fn:5">5</a></sup>.</p> <div class="row justify-content-center py-3"> <div class="col text-center"> <figure class="figure"> <img src="../../static/img/hexagon.svg" width="500px" class="mb-2"> <figcaption class="figure-caption">Coordinates of the hexagon vertices with respect to its center</figcaption> </figure> </div> </div> <p>Based on that we can code up the <code>Hexagon</code> class to be drawn on the canvas as the <code>polygon</code> element.</p> <div class="codehilite"><pre><span></span><code><span class="nd">@dataclass</span> <span class="k">class</span> <span class="nc">Hexagon</span><span class="p">:</span> <span class="s2">&quot;Regular Hexagon&quot;</span> <span class="n">side</span><span class="p">:</span> <span class="nb">int</span> <span class="nd">@property</span> <span class="k">def</span> <span class="nf">height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="s2">&quot;Height of a flat top regular hexagon&quot;</span> <span class="k">return</span> <span class="n">math</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> <span class="o">*</span> <span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="k">def</span> <span class="nf">coords</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="s2">&quot;Coordinates of vertices with repect to hexagon center (0, 0)&quot;</span> <span class="k">return</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span> <span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">side</span> <span class="o">/</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="bp">self</span><span class="o">.</span><span class="n">height</span> <span class="o">/</span> <span class="mi">2</span><span class="p">),</span> <span class="p">])</span> </code></pre></div> <p>With that we should be able to recreate the analogues of the graphics we created with squares on a square lattice. We'll also need a <code>draw_hexagons</code> function, but that again is pretty analogous to <code>draw_squares</code>, so I'll leave it for you to figure adapt.</p> <h2 id="graphics">Graphics</h2> <p>With all things in place here is a side by side comparison of the graphics with and without different gradient effects. You might notice that there is an extra wavy pattern generated which is also at the head of this post. It's essentially a sin wave with a fade based on distance from the curve. If you're curious about it let me in the comments and I'll more.</p> <div class="row justify-content-center py-3"> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a33-square-25.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Square lattice a=40, square side=20</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/hex-lattice-side20-padding5.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Hex lattice padding=5, hex side=20</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a33-square-25-gradient.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Sq lattice a=33, square side=25, linear gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/hex-lattice-side20-padding5-gradient.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Hex lattice padding=5, hex side=20, linear gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a33-square-25-gradient-ridge.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Sq lattice a=33, square side=25, ridge gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/hex-lattice-side20-padding5-ridge-gradient.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Hex lattice a=20, hex side=20, ridge gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a33-square-25-sin-gradient.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Sq Lattice a=33, square size=25, sin gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/hex-lattice-side20-padding5-sin-gradient.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Hex lattice a=20, hex side=20, sin gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a33-square-25-radial-grad.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Sq lattice a=33, square side=25, radial gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/hex-lattice-side20-padding5-radial-gradient.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Hex lattice a=20, hex side=20, radial gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/square-lattice-a33-square-25-radial-grad-inverse.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Sq lattice a=33, square side=25, inverse radial gradient</figcaption> </figure> </div> <div class="col-6 py-2"> <figure class="figure"> <img src="../../static/img/hex-lattice-side20-padding5-radial-gradient-inverse.svg" width="100%" class="mb-2"> <figcaption class="figure-caption text-center">Hex lattice a=20, hex side=20, inverse radial gradient</figcaption> </figure> </div> </div> <h2 id="summary">Summary</h2> <p>Generating vector graphics programmatically was a lot more exciting than I expected and I was surprised how much I got sucked into it. Once it was clear how to create shapes and position them on the canvas create variations on the designs was quite easy. I am quite happy with the results so far and definitely prefer the hex-on-hex variant, but I would like to explore further and try out a few more things to make the designs a bit more organic and less perfect.</p> <p>To take it further I would probably explore ways of introducing randomness in various channels and to encode different attributes. So far all the designs were quite simple and deterministic and randomness would take them to next level. Breaking the perfect symmetries either by deforming the lattice of making the shapes a bit irregular (or both) might also produce an interesting result.</p> <p>On top of that I think that introducing various kinds of defects and imperfections inspired by real crystal structures where those phenomena occur would add yet another dimension to explore. </p> <h2 id="footnotes">Footnotes</h2> <div class="footnote"> <hr /> <ol> <li id="fn:1"> <p>This is going to be a nice journey back in to my crystallgraphy lectures.&#160;<a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text">&#8617;</a></p> </li> <li id="fn:2"> <p>By default SVG coordinates start in the top-left corner at <code>(0, 0)</code> and go to <code>(width, height)</code> with the pixels as unit.&#160;<a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text">&#8617;</a></p> </li> <li id="fn:3"> <p>In general there are 5 types <a href="https://en.wikipedia.org/wiki/Bravais_lattice">Bravais lattices</a> in 2D, and they can be specified by defining the parameters of the unit cell.&#160;<a class="footnote-backref" href="#fnref:3" title="Jump back to footnote 3 in the text">&#8617;</a></p> </li> <li id="fn:4"> <p>If you want to know learn more about hexagonal coordinates check out <a href="https://www.redblobgames.com/grids/hexagons/">this post</a>.&#160;<a class="footnote-backref" href="#fnref:4" title="Jump back to footnote 4 in the text">&#8617;</a></p> </li> <li id="fn:5"> <p>I choose the flat top orientation of the hexagon, an alternative would be the pointy top hexagon orientation. They differ by a 90-degree rotation of all vertices, so it's quite straightforward to switch between them.&#160;<a class="footnote-backref" href="#fnref:5" title="Jump back to footnote 5 in the text">&#8617;</a></p> </li> </ol> </div> Better select forms with django and Bootstrap 5 http://localhost/blog/better-select-form-django/ 2022-10-16T23:55:00Z 2022-10-16T23:55:00Z <p><img class="rounded mx-auto shadow-lg d-block" src="../../static/img/kelly-sikkema-hLit2zL-Dhk-unsplash-small.jpg" alt="web design"></p> <p class="text-muted small text-end">Photo by <a href="https://unsplash.com/@kellysikkema?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Kelly Sikkema</a> on <a href="https://unsplash.com/s/photos/user-interaction?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a> </p> <hr> <p>I enjoy working with <a href="https://www.djangoproject.com/">Django</a> for many reasons but mostly since I can focus on what I want my app to do and django happily takes care of the rest. In 2022 it's probably not the sexiest choice when it comes to web frameworks, but there are good reasons it is still as popular as it is. There is a recent wave of excitement about server side rendering frameworks thanks to a new batch of tools like <a href="https://htmx.org/">HTMX</a>, <a href="https://www.django-unicorn.com/">django-unicorn</a> and <a href="https://github.com/jonathan-s/django-sockpuppet">django-sockpuppet</a> that give django superpowers and put into question the <code>React</code> based paradigm for building apps. But that's a digression for another time.</p> <p>Django takes care of a lot of stuff behind the scenes and that is a solid productivity boost. Being a mature framework means that is has opinions (mostly for very good reasons) and provides you with defaults that are sensible in most o the cases. There are however cases when it's worth to make some changes to improve end-user experience.</p> <p>One such aspect is the default UI widget for selecting items. I am of course talking about <a href="https://en.wikipedia.org/wiki/Drop-down_list">dropdowns</a>. They have been around since forever and although being familiar they are usually not the best choice when it comes to modern UX standards. Out in the wild you can find professionals suggesting <a href="https://www.nngroup.com/articles/drop-down-menus/">caution when using dropdowns</a> and calls to <a href="https://adrianroselli.com/2020/03/stop-using-drop-down.html">stop using drop-downs</a> at all. </p> <p>Personally I would like to adhere to good UX practices as far as I can and although my level of comfort increases with proximity to backend side of things I cannot be annoyed by all the low hanging fruit that sometimes plague otherwise good products. Wanting things to look good without a massive design effort is one of the reasons I usually reach for a frontend framework, most often <a href="https://getbootstrap.com/">bootstrap</a>. With django that means using <a href="https://github.com/django-crispy-forms/django-crispy-forms">django crispy forms</a> to get the forms looking sharp and consistent with an extra line in the template.</p> <h2 id="making-choices">Making choices</h2> <p>One of the things I was recently working on is a django-based dashboard where I wanted to have the option to choose the aggregation level between: <code>daily</code>, <code>weekly</code>, <code>monthly</code> aggregates. Of course my first pass was the default <code>forms.Select</code> widget that is a dropdown. Once I saw the result on the screen though, I felt something is off, and that this should probably be improved. Usually I try to move on until I have a working prototype that I can iterate over. Following the Kent Beck's principle</p> <blockquote class="blockquote text-center py-5">make it work, make it right, make it fast</blockquote> <p>When I got everything wired up and working it seemed like a good time get back to the select widget and scratch that itch. </p> <p>On the django side I had a form to represent the choices </p> <div class="codehilite"><pre><span></span><code><span class="kn">from</span> <span class="nn">django</span> <span class="kn">import</span> <span class="n">forms</span> <span class="k">class</span> <span class="nc">FrequencyForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span> <span class="n">choices</span> <span class="o">=</span> <span class="p">[</span> <span class="p">(</span><span class="s2">&quot;1D&quot;</span><span class="p">,</span> <span class="s2">&quot;Daily&quot;</span><span class="p">),</span> <span class="p">(</span><span class="s2">&quot;1W&quot;</span><span class="p">,</span> <span class="s2">&quot;Weekly),</span> <span class="p">(</span><span class="s2">&quot;1M&quot;</span><span class="p">,</span> <span class="s2">&quot;Monthly&quot;</span><span class="p">)</span> <span class="p">]</span> <span class="n">freq</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">ChoiceField</span><span class="p">(</span><span class="n">label</span><span class="o">=</span><span class="s2">&quot;Frequency&quot;</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="n">choices</span><span class="p">)</span> </code></pre></div> <p>which was being rendered in a template with pretty standard django</p> <div class="codehilite"><pre><span></span><code>{% load crispy_forms_tags %} ... {{ form|crispy }} </code></pre></div> <p>The effect isn't surprising either</p> <div class="row mb-5"> <div class="col-md-4 mx-auto"> <div id="div_id_freq" class="mb-3"> <label for="id_freq" class="form-label requiredField"> Frequency<span class="asteriskField">*</span> </label> <select name="freq" class="select form-select" id="id_freq"> <option value="1D">Daily</option> <option value="1W" selected="">Weekly</option> <option value="1M">Monthly</option> </select> </div> </div> </div> <p>I didn't want for the options to be hidden, so the next best thing was to switch to a radio select by changing the for widget to <code>RadioSelect</code> and keeping the template as is</p> <div class="codehilite"><pre><span></span><code><span class="kn">from</span> <span class="nn">django</span> <span class="kn">import</span> <span class="n">forms</span> <span class="k">class</span> <span class="nc">FrequencyForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span> <span class="n">choices</span> <span class="o">=</span> <span class="p">[</span> <span class="p">(</span><span class="s2">&quot;1D&quot;</span><span class="p">,</span> <span class="s2">&quot;Daily&quot;</span><span class="p">),</span> <span class="p">(</span><span class="s2">&quot;1W&quot;</span><span class="p">,</span> <span class="s2">&quot;Weekly),</span> <span class="p">(</span><span class="s2">&quot;1M&quot;</span><span class="p">,</span> <span class="s2">&quot;Monthly&quot;</span><span class="p">)</span> <span class="p">]</span> <span class="n">freq</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">ChoiceField</span><span class="p">(</span> <span class="n">label</span><span class="o">=</span><span class="s2">&quot;Frequency&quot;</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="n">choices</span><span class="p">,</span> <span class="n">widget</span><span class="o">=</span><span class="n">forms</span><span class="o">.</span><span class="n">RadioSelect</span><span class="p">(),</span> <span class="p">)</span> </code></pre></div> <p>This gives</p> <div class="row mb-5"> <div class="col-md-4 mx-auto"> <div id="div_id_freq" class="mb-3"> <label class="form-label requiredField">Frequency<span class="asteriskField">*</span></label> <div> <div class="form-check"> <input type="radio" class="form-check-input" name="freq" value="1D" id="id_freq_0" required=""> <label for="id_freq_0" class="form-check-label"> Daily </label> </div> <div class="form-check"> <input type="radio" class="form-check-input" name="freq" value="W" id="id_freq_1" required="" checked=""> <label for="id_freq_1" class="form-check-label"> Weekly </label> </div> <div class="form-check"> <input type="radio" class="form-check-input" name="freq" value="MS" id="id_freq_2" required=""> <label for="id_freq_2" class="form-check-label"> Monthly </label> </div> </div> </div> </div> </div> <p>Bettter but I still wasn't happy with the result. When looking for inspiration I found <a href="https://www.learnui.design/blog/4-rules-intuitive-ux.html">this post</a> suggesting a segmented button that reminded me of what I saw in latest <a href="https://getbootstrap.com/docs/5.0/components/button-group/#checkbox-and-radio-button-groups">Bootstrap 5 docs</a> so I decided to give it a go since I was already using Bootstrap.</p> <p>Looking at the example from Bootstrap it became clear that I would have to render the form manually. The example I started from was this:</p> <div class="codehilite"><pre><span></span><code><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn-group&quot;</span> <span class="na">role</span><span class="o">=</span><span class="s">&quot;group&quot;</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&quot;Basic radio toggle button group&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&quot;radio&quot;</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn-check&quot;</span> <span class="na">name</span><span class="o">=</span><span class="s">&quot;btnradio&quot;</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;btnradio1&quot;</span> <span class="na">autocomplete</span><span class="o">=</span><span class="s">&quot;off&quot;</span> <span class="na">checked</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn btn-outline-primary&quot;</span> <span class="na">for</span><span class="o">=</span><span class="s">&quot;btnradio1&quot;</span><span class="p">&gt;</span>Radio 1<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&quot;radio&quot;</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn-check&quot;</span> <span class="na">name</span><span class="o">=</span><span class="s">&quot;btnradio&quot;</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;btnradio2&quot;</span> <span class="na">autocomplete</span><span class="o">=</span><span class="s">&quot;off&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn btn-outline-primary&quot;</span> <span class="na">for</span><span class="o">=</span><span class="s">&quot;btnradio2&quot;</span><span class="p">&gt;</span>Radio 2<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&quot;radio&quot;</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn-check&quot;</span> <span class="na">name</span><span class="o">=</span><span class="s">&quot;btnradio&quot;</span> <span class="na">id</span><span class="o">=</span><span class="s">&quot;btnradio3&quot;</span> <span class="na">autocomplete</span><span class="o">=</span><span class="s">&quot;off&quot;</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn btn-outline-primary&quot;</span> <span class="na">for</span><span class="o">=</span><span class="s">&quot;btnradio3&quot;</span><span class="p">&gt;</span>Radio 3<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> </code></pre></div> <p>which looks like </p> <div class="row justify-content-center py-5"> <div class="col text-center"> <div class="btn-group" role="group" aria-label="Basic radio toggle button group"> <input type="radio" class="btn-check" name="btnradio" id="btnradio1" autocomplete="off" checked> <label class="btn btn-outline-primary" for="btnradio1">Radio 1</label> <input type="radio" class="btn-check" name="btnradio" id="btnradio2" autocomplete="off"> <label class="btn btn-outline-primary" for="btnradio2">Radio 2</label> <input type="radio" class="btn-check" name="btnradio" id="btnradio3" autocomplete="off"> <label class="btn btn-outline-primary" for="btnradio3">Radio 3</label> </div> </div> </div> <p>Pretty nice! I went ahead and updated my template and instead of the <code>{{ form|crispy }}</code> elements I went ahead and unfolded the form and added the classes and attributes to render the button group:</p> <div class="codehilite"><pre><span></span><code><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn-group&quot;</span> <span class="na">role</span><span class="o">=</span><span class="s">&quot;group&quot;</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&quot;Basic radio toggle button group&quot;</span><span class="p">&gt;</span> {% for choice in form.freq %} {{ choice.tag }} <span class="p">&lt;</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn btn-outline-primary&quot;</span> <span class="na">for</span><span class="o">=</span><span class="s">&quot;{{ choice.id_for_label }}&quot;</span><span class="p">&gt;</span>{{ choice.choice_label }}<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span> {% endfor %} <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> </code></pre></div> <p>Everything looked nice when rendered however when clicking on the buttons they wouldn't stay selected as they should. They would change back to unselected which was annoying. I had to compare the original HTML with my rendered version to realize that the <code>&lt;input&gt;</code> tags in my case were missing s <code>class="btn-check"</code> attribute, so I modified the form to have that added </p> <div class="codehilite"><pre><span></span><code><span class="k">class</span> <span class="nc">FrequencyForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span> <span class="n">choices</span> <span class="o">=</span> <span class="p">[</span> <span class="p">(</span><span class="s2">&quot;1D&quot;</span><span class="p">,</span> <span class="s2">&quot;Daily&quot;</span><span class="p">),</span> <span class="p">(</span><span class="s2">&quot;1W&quot;</span><span class="p">,</span> <span class="s2">&quot;Weekly),</span> <span class="p">(</span><span class="s2">&quot;1M&quot;</span><span class="p">,</span> <span class="s2">&quot;Monthly&quot;</span><span class="p">)</span> <span class="p">]</span> <span class="n">freq</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">ChoiceField</span><span class="p">(</span> <span class="n">label</span><span class="o">=</span><span class="s1">&#39;Category&#39;</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="n">choices</span><span class="p">,</span> <span class="n">widget</span><span class="o">=</span><span class="n">forms</span><span class="o">.</span><span class="n">RadioSelect</span><span class="p">(</span><span class="n">attrs</span><span class="o">=</span><span class="p">{</span><span class="s1">&#39;class&#39;</span><span class="p">:</span><span class="s1">&#39;btn-check&#39;</span><span class="p">}),</span> <span class="p">)</span> </code></pre></div> <p>Adding that attribute to the form did the trick. With that little tweak I was able to achieve way better UX and eliminate and unnecessary dropdown. </p> <div class="row justify-content-center py-5"> <div class="col text-center"> <div class="btn-group" role="group" aria-label="Basic radio toggle button group"> <input type="radio" class="btn-check" name="freqbtnradio" id="freqbtnradio1" autocomplete="off" checked> <label class="btn btn-outline-primary" for="freqbtnradio1">Daily</label> <input type="radio" class="btn-check" name="freqbtnradio" id="freqbtnradio2" autocomplete="off"> <label class="btn btn-outline-primary" for="freqbtnradio2">Weekly</label> <input type="radio" class="btn-check" name="freqbtnradio" id="freqbtnradio3" autocomplete="off"> <label class="btn btn-outline-primary" for="freqbtnradio3">Monthly</label> </div> </div> </div> <p>This was it for me but in case you have a few more choices to select from (maybe 5-9 items) and you still would like to present them to the user this might not be a good choice since button group will most likely be too wide for small screens and overflow. </p> <p>In this case you can switch to splitting choices into individual buttons and arranging them in a grid rather than a single row. Then your CSS framework can take over and manage the responsive layout for smaller screens where you probably want to either switch to a more vertical layout or a grid layout.</p> <p>Let me add two more choices to my original form to illustrate how could it look like</p> <div class="codehilite"><pre><span></span><code><span class="k">class</span> <span class="nc">FrequencyForm</span><span class="p">(</span><span class="n">forms</span><span class="o">.</span><span class="n">Form</span><span class="p">):</span> <span class="n">choices</span> <span class="o">=</span> <span class="p">[</span> <span class="p">(</span><span class="s2">&quot;1D&quot;</span><span class="p">,</span> <span class="s2">&quot;Daily&quot;</span><span class="p">),</span> <span class="p">(</span><span class="s2">&quot;1W&quot;</span><span class="p">,</span> <span class="s2">&quot;Weekly),</span> <span class="p">(</span><span class="s2">&quot;1M&quot;</span><span class="p">,</span> <span class="s2">&quot;Monthly&quot;</span><span class="p">)</span> <span class="p">(</span><span class="s2">&quot;1Q&quot;</span><span class="p">,</span> <span class="s2">&quot;Quarterly&quot;</span><span class="p">)</span> <span class="p">(</span><span class="s2">&quot;1Y&quot;</span><span class="p">,</span> <span class="s2">&quot;Yearly&quot;</span><span class="p">)</span> <span class="p">]</span> <span class="n">freq</span> <span class="o">=</span> <span class="n">forms</span><span class="o">.</span><span class="n">ChoiceField</span><span class="p">(</span> <span class="n">label</span><span class="o">=</span><span class="s1">&#39;Category&#39;</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="n">choices</span><span class="p">,</span> <span class="n">widget</span><span class="o">=</span><span class="n">forms</span><span class="o">.</span><span class="n">RadioSelect</span><span class="p">(</span><span class="n">attrs</span><span class="o">=</span><span class="p">{</span><span class="s1">&#39;class&#39;</span><span class="p">:</span><span class="s1">&#39;btn-check&#39;</span><span class="p">}),</span> <span class="p">)</span> </code></pre></div> <p>In the template we need to wrap all choice fields in a <code>row</code> and each input + label tag in a <code>col</code> with appropriate widths for each display breakpoint. This is bootstrap specific so check out <a href="https://getbootstrap.com/docs/5.2/layout/breakpoints/">bootstrap layout</a> if you want to know more. </p> <div class="codehilite"><pre><span></span><code><span class="p">&lt;</span><span class="nt">form</span><span class="p">&gt;</span> <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;row&quot;</span><span class="p">&gt;</span> {% for choice in form.freq %} <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;col-4 col-md-3 col-lg-2&quot;</span><span class="p">&gt;</span> {{ choice.tag }} <span class="p">&lt;</span><span class="nt">label</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;btn btn-outline-primary&quot;</span> <span class="na">for</span><span class="o">=</span><span class="s">&quot;{{ choice.id_for_label }}&quot;</span><span class="p">&gt;</span>{{ choice.choice_label }}<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span> <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span> {% endfor %} <span class="p">&lt;/</span><span class="nt">form</span><span class="p">&gt;</span> </code></pre></div> <p>This is now rendered as independent radio buttons </p> <div class="row justify-content-center py-5"> <div class="col text-center"> <form> <div class="row justify-content-center"> <div class="col-4 col-md-3 col-lg-2 py-2"> <input type="radio" class="btn-check" name="choice" id="radio-1" value="1D"> <label class="btn btn-outline-primary btn-lg" for="radio-1">Daily</label> </div> <div class="col-4 col-md-3 col-lg-2 py-2"> <input type="radio" class="btn-check" name="choice" id="radio-2" value="1W"> <label class="btn btn-outline-primary btn-lg" for="radio-2">Weekly</label> </div> <div class="col-4 col-md-3 col-lg-2 py-2"> <input type="radio" class="btn-check" name="choice" id="radio-3" value="1M"> <label class="btn btn-outline-primary btn-lg" for="radio-3">Monthly</label> </div> <div class="col-4 col-md-3 col-lg-2 py-2"> <input type="radio" class="btn-check" name="choice" id="radio-4" value="1Q"> <label class="btn btn-outline-primary btn-lg" for="radio-4">Quartely</label> </div> <div class="col-4 col-md-3 col-lg-2 py-2"> <input type="radio" class="btn-check" name="choice" id="radio-5" value="1Y"> <label class="btn btn-outline-primary btn-lg" for="radio-5">Yearly</label> </div> </div> </form> </div> </div> <h2 id="summary">Summary</h2> <p>If you're using django and a css framework like bootstrap it does not take much effort to adapt your forms and improve overall user experience and at the same time conform to modern standard for look and feel of your web app. If you have similar quick improvements please share below since I'm always on the lookout for how to improve usability.</p> <hr> Datasets beat Models http://localhost/blog/datasets-beat-models/ 2021-06-24T12:00:00Z 2021-06-24T12:00:00Z <p><img class="rounded mx-auto shadow-lg d-block" src="../../static/img/1624540361257.jpeg" alt="datasets beat models"></p> <hr> <p>I know it’s easy to get carried away by all the shiny, sophisticated ML models out there, but we need to have a serious conversation about datasets. In most industrial applications of ML it’s the datasets that limit performance not model complexity. </p> <p>Lack of high quality training datasets is the biggest challenge for industrial machine learning right now. That should not be surprising if you consider how much bigger the supply of models is compared to datasets. A big portion of the ML algorithms development happens thanks to open source and academic communities which gladly publish their work in exchange for reputation points. In contrast, developing and curating datasets is often less valued and not as glamorous. </p> <p>My experience from working on data-driven solutions for industrial problems is that there is no shortage of data and huge volumes are available in raw form; however converting them into training and validation datasets is what usually blocks ML/AI progress. </p> <p>I won't be controversial if I attribute 80% of a data scientist's time to what is collectively termed preprocessing, cleaning or structuring the data. The result of all of those activities is a dataset that hopefully accurately represents reality and can serve as ground truth for model development and evaluation. Creating high quality datasets however takes effort, skills and a right blend of domain knowledge and experience with data.</p> <p>This is exactly what we are passionate about at unifai. We are approaching dataset development as an engineering practice and we are creating high quality datasets to increase adoption of machine learning in the industry. Our experience in data science, data engineering, software development and heavy asset industry puts us in a unique position to deliver datasets with a perfect mix of domain knowledge.</p> <p>Unifai is not alone in betting on datasets, one of the biggest names in AI: Andrew Ng recently started promoting a more data-centric view of AI instead of the model-centric view. In a recent announcement together with landing.ai and deeplearning.ai he launched a competition that focuses on improving data while keeping the model fixed to inspire new methods for improving data.</p> <p>Get in touch with me if you share similar challenges or if you're sitting on piles of raw data and your organization wants to get some quick wins with machine learning.</p> <hr> <p>This post was originally published on <a href="https://www.linkedin.com/pulse/datasets-beat-models-lukasz-mentel">LinkedIn</a>.</p> Zeolite pore size oddity http://localhost/blog/zeolite-pore-size-oddity/ 2017-10-31T12:00:00Z 2017-10-31T12:00:00Z <p>One of the ways to classify zeolites is usually by the size of their pore openings given in the peculiar unit of T atom count. T atom stands for tetrachedrally coordinated atoms, usually the term lumps together silicon and aluminum in case of zeolites or aluminum and phosphorus in case of aluminumphpsphates. The structural units characterising pores are usually rings from which the tube-like channels are formed (as shown below) and therefore the as described as <em>n</em> membered rings or <em>n</em>MR in short. Where <em>n</em> can be as small as 4 and as large as 20.</p> <p>[picture of 8MR, 10MR and 12MR with a corresponding tube]</p> <p>By inspecting the figure below showing the distribution of ring sizes among the known zeolite structures it is easy to see that 8, 10, and 12 MR structures dominate over the remaining topologies. Which justifies the why this parameter might serve as a good classification. It should also be taken into account that for most of the interesting applications topologies with MR &lt; 8 are not intersting since molecules cannot enter the small channels and channels larger than 14 would have very similar properties.</p> <p>IUPAC classification into nano, micro and mesoporous structures in a <a href="https://doi.org/10.1351/pac199466081739">technical report</a>.</p> <p>It is customary to call 8, 10 and 12MR, <em>small</em>, <em>medium</em> and <em>large</em> pore zeolites suggesting that this classification provides distinct groups of similar characteristing being size. While intuitively and qualitatively correct, applying it directly can lead to incorrect insights since this MR measure provides limited structural information. A closer inspection of each class revelas that in each class there is a distribution of sizes as evaluated by different criteria actually associated with a more functional definition of pore size.</p> <p>apply clustering based on size largst cavity diameter (LCD) and some other features related to functional size and compare them to the classification based on MR and see which are misclassified.</p> Superimpose chemical structures http://localhost/blog/superimpose-structures/ 2017-09-28T12:00:00Z 2017-09-28T12:00:00Z <p>I was struggling recently how to present multiple chemical structures that differed slightly in their geometry but </p> <div class="codehilite"><pre><span></span><code><span class="kn">import</span> <span class="nn">os</span> <span class="kn">import</span> <span class="nn">re</span> <span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span> <span class="kn">from</span> <span class="nn">ase</span> <span class="kn">import</span> <span class="n">Atoms</span> <span class="kn">import</span> <span class="nn">ase.io</span> <span class="k">def</span> <span class="nf">read_structures</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">fpatt</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span> <span class="sd">&#39;&#39;&#39;</span> <span class="sd"> Read all the structures from `path` that match the `fpatt` pattern</span> <span class="sd"> &#39;&#39;&#39;</span> <span class="n">fpatt</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="n">fpatt</span><span class="p">)</span> <span class="n">out</span> <span class="o">=</span> <span class="p">{</span><span class="s1">&#39;II&#39;</span><span class="p">:</span> <span class="n">OrderedDict</span><span class="p">(),</span> <span class="s1">&#39;III&#39;</span><span class="p">:</span> <span class="n">OrderedDict</span><span class="p">(),</span> <span class="s1">&#39;IV&#39;</span><span class="p">:</span> <span class="n">OrderedDict</span><span class="p">()}</span> <span class="k">for</span> <span class="n">fname</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">listdir</span><span class="p">(</span><span class="n">path</span><span class="p">):</span> <span class="n">m</span> <span class="o">=</span> <span class="n">fpatt</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="n">fname</span><span class="p">)</span> <span class="k">if</span> <span class="n">m</span><span class="p">:</span> <span class="n">symbol</span><span class="p">,</span> <span class="n">oxs</span> <span class="o">=</span> <span class="n">m</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="n">m</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="n">out</span><span class="p">[</span><span class="n">oxs</span><span class="p">][</span><span class="n">symbol</span><span class="p">]</span> <span class="o">=</span> <span class="n">ase</span><span class="o">.</span><span class="n">io</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">fname</span><span class="p">))</span> <span class="k">else</span><span class="p">:</span> <span class="k">if</span> <span class="n">verbose</span><span class="p">:</span> <span class="nb">print</span><span class="p">(</span><span class="s1">&#39;ERROR &#39;</span><span class="p">,</span> <span class="n">fname</span><span class="p">)</span> <span class="nb">print</span><span class="p">(</span><span class="s1">&#39;Read: &#39;</span><span class="p">,</span> <span class="s1">&#39;, &#39;</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="s1">&#39;</span><span class="si">{}</span><span class="s1">: </span><span class="si">{}</span><span class="s1">&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">v</span><span class="p">))</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">out</span><span class="o">.</span><span class="n">items</span><span class="p">()]))</span> <span class="k">return</span> <span class="n">out</span> <span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span> <span class="n">path</span> <span class="o">=</span> <span class="s1">&#39;AlPO-5-H&#39;</span> <span class="n">patt</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">&#39;([A-Za-z]+)\(([IV]+)\)-AlPO-5-O2-H\.traj&#39;</span> <span class="n">halpo</span> <span class="o">=</span> <span class="n">read_structures</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">patt</span><span class="p">)</span> <span class="n">atoms</span> <span class="o">=</span> <span class="n">Atoms</span><span class="p">()</span> <span class="k">for</span> <span class="n">me</span><span class="p">,</span> <span class="n">a</span> <span class="ow">in</span> <span class="n">halpo</span><span class="p">[</span><span class="s1">&#39;IV&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">items</span><span class="p">():</span> <span class="n">atoms</span> <span class="o">=</span> <span class="n">atoms</span> <span class="o">+</span> <span class="n">a</span> <span class="n">transmittances</span> <span class="o">=</span> <span class="p">[</span><span class="mf">0.8</span><span class="p">]</span> <span class="o">*</span> <span class="nb">len</span><span class="p">(</span><span class="n">atoms</span><span class="p">)</span> <span class="n">ase</span><span class="o">.</span><span class="n">io</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s1">&#39;Me-AlPO-5-H.pov&#39;</span><span class="p">,</span> <span class="n">atoms</span><span class="p">,</span> <span class="n">rotation</span><span class="o">=</span><span class="s1">&#39;-15y&#39;</span><span class="p">,</span> <span class="n">show_unit_cell</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">run_povray</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">display</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">pause</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">canvas_width</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span> <span class="n">camera_type</span><span class="o">=</span><span class="s1">&#39;perspective&#39;</span><span class="p">,</span> <span class="n">transmittances</span><span class="o">=</span><span class="n">transmittances</span><span class="p">)</span> <span class="n">ase</span><span class="o">.</span><span class="n">io</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s1">&#39;Me-AlPO-5-H_0y.pov&#39;</span><span class="p">,</span> <span class="n">atoms</span><span class="p">,</span> <span class="n">rotation</span><span class="o">=</span><span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="n">show_unit_cell</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">run_povray</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">display</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">pause</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">canvas_width</span><span class="o">=</span><span class="mi">1024</span><span class="p">,</span> <span class="n">camera_type</span><span class="o">=</span><span class="s1">&#39;perspective&#39;</span><span class="p">,</span> <span class="n">transmittances</span><span class="o">=</span><span class="n">transmittances</span><span class="p">)</span> </code></pre></div> <p>Rendering the scenes for multiple structures was a bit too slow for my laptop so I had to use a supercomputer to speed things up.</p> <p>This is a bit of a special case since the structures are taken from a periodic crystals and therefore all have the same unit cell size. It simpifies things a lot since then all the coordinates are given with respect to origin of the unit cell which is the same for all the materials. For (non-periodic) molecules we would have to align the molecules first since their coordinates can be defined with respect to different origins. One possible solution is to minimize the <a href="https://en.wikipedia.org/wiki/Root-mean-square_deviation_of_atomic_positions">root mean square deviation</a> between the structures in order to realign them which is usually done using the <a href="https://doi.org/10.1107/S0567739476001873">Kabsch</a> algorithm. For those curious to see it in practice e is also a python available <a href="https://github.com/charnley/rmsd">rmsdpy</a>.</p>