<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jeff Lowery</title>
    <description>The latest articles on DEV Community by Jeff Lowery (@jeffml).</description>
    <link>https://dev.to/jeffml</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F58098%2F418b8827-facc-49cd-8561-e9d39d544001.jpeg</url>
      <title>DEV Community: Jeff Lowery</title>
      <link>https://dev.to/jeffml</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jeffml"/>
    <language>en</language>
    <item>
      <title>Size Matters: Squeezing a 17,000-Edge Graph Into a Mobile-Friendly JSON</title>
      <dc:creator>Jeff Lowery</dc:creator>
      <pubDate>Fri, 24 Jul 2026 20:24:49 +0000</pubDate>
      <link>https://dev.to/jeffml/size-matters-squeezing-a-17000-edge-graph-into-a-mobile-friendly-json-2c6a</link>
      <guid>https://dev.to/jeffml/size-matters-squeezing-a-17000-edge-graph-into-a-mobile-friendly-json-2c6a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi1nds1zzjgocpjdferan.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi1nds1zzjgocpjdferan.png" alt="Alice Shrinking (1865) by John Tenniel" width="489" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Part 3: How I optimized, packed, and compressed a massive chess opening graph to load instantly on a cheap phone.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;In Part 1, we tackled the transpositions and bridged the gap between orphaned variations. In Part 2, we fought the "Source Wars" to clean, sanitize, and merge ten uncooperative data sources (including a relentlessly manual Wikibooks parser).&lt;/p&gt;

&lt;p&gt;We finally had a masterpiece: a mathematically sound, beautifully normalized, 17,000-edge directed graph representing the entire tree of chess openings. &lt;/p&gt;

&lt;p&gt;But then, I ran a build.&lt;/p&gt;

&lt;p&gt;And my output directory spit out a single, monstrous, &lt;strong&gt;4.2 megabyte JSON file&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;For a backend pipeline, 4.2 MB is a rounding error. But for a mobile-friendly frontend application where users expect instant interactive feedback on every chess move, shipping a multi-megabyte JSON file over a spotty mobile network is a cardinal sin. It means layout thrashing, high latency, and a miserable user experience.&lt;/p&gt;

&lt;p&gt;I had to put the graph on a strict diet. Here is how we got &lt;code&gt;@chess-openings/eco.json&lt;/code&gt; down to a featherweight fraction of its original size—without writing a single line of complex compression code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Chess JSONs are Inherently Bloated
&lt;/h3&gt;

&lt;p&gt;Standard, raw JSON is an incredibly expressive format, but when you represent a massive database of moves and names, it is brutally redundant. &lt;/p&gt;

&lt;p&gt;Take a look at two actual entries from our master database. Even though these represent two distinct, specific lines of the French Defense, notice how much text is duplicated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rnbqkbnr/2pp1ppp/4p3/1p6/3PP3/8/PP3PPP/RNBQKBNR w KQkq - 0 5"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eco_tsv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eco"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moves"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1. e4 e6 2. d4 a6 3. c4 b5 4. cxb5 axb5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"French Defense: St. George Defense, St. George Gambit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aliases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eco_js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"St. George Defense: St. George Gambit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"St. George Defense, St. Georgs Gambit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"chessGraph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"St George's Gambit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"icsbot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"St George's Gambit "&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rnbqkbnr/1ppp1ppp/p3p3/8/2PPP3/8/PP3PPP/RNBQKBNR b KQkq - 0 3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eco_tsv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eco"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"moves"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1. e4 e6 2. d4 a6 3. c4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"French Defense: St. George Defense, Three Pawn Attack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"aliases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"eco_js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"St. George Defense: New St. George, Three Pawn Attack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"St. George Defense, New St. George"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"chessGraph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Three Pawn Attack, St George"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"icsbot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Three Pawn Attack, St George "&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multiply this by &lt;strong&gt;17,000 positions&lt;/strong&gt;, and you are wasting megabytes on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Repeating Key Names:&lt;/strong&gt; Repeating &lt;code&gt;"src"&lt;/code&gt;, &lt;code&gt;"eco"&lt;/code&gt;, &lt;code&gt;"moves"&lt;/code&gt;, &lt;code&gt;"name"&lt;/code&gt;, &lt;code&gt;"aliases"&lt;/code&gt;, &lt;code&gt;"eco_js"&lt;/code&gt;, &lt;code&gt;"ct"&lt;/code&gt;, &lt;code&gt;"chessGraph"&lt;/code&gt;, and &lt;code&gt;"icsbot"&lt;/code&gt; 17,000 times eats up hundreds of kilobytes of pure ASCII noise.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Duplicate Substrings:&lt;/strong&gt; Because every single chess opening starts from the exact same board setup, the first few ranks of the FEN strings (like &lt;code&gt;rnbqkbnr/&lt;/code&gt;) are virtually identical across thousands of keys.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Semantic Overlap:&lt;/strong&gt; Common opening names like &lt;code&gt;"French Defense"&lt;/code&gt;, &lt;code&gt;"St. George Defense"&lt;/code&gt;, and &lt;code&gt;"Gambit"&lt;/code&gt; appear on dozens of sub-variations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At first glance, it looks like we need to write a highly complex, custom serialization script to tokenize these strings, map them to integer dictionaries, and "rehydrate" the graph on the client side.&lt;/p&gt;

&lt;p&gt;But the best engineering solution is the one you don't have to write.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Let Gzip Do the Heavy Lifting (via Streams, Not JS Memory)
&lt;/h3&gt;

&lt;p&gt;Instead of over-engineering a complex custom serialization script in Node.js that would eat up system memory loading and processing 17,000 deep objects, we can let standard UNIX utilities and the web's native transport compression do 100% of the work.&lt;/p&gt;

&lt;p&gt;I wrote a dead-simple bash script that concatenates the individual JSON segment files (&lt;code&gt;ecoA.json&lt;/code&gt; through &lt;code&gt;ecoE.json&lt;/code&gt; plus our custom &lt;code&gt;eco_interpolated.json&lt;/code&gt;) on the fly. &lt;/p&gt;

&lt;p&gt;It treats the files as raw text streams, strips their outer JSON braces using standard &lt;code&gt;sed&lt;/code&gt; pipelines, merges them, and pipes the output stream directly into high-level Gzip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Strip blank lines, then first and last lines (the outer { and })&lt;/span&gt;
strip&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'/^[[:space:]]*$/d'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'1d;$d'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'{'&lt;/span&gt;
  &lt;span class="nv"&gt;last_idx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((${#&lt;/span&gt;&lt;span class="nv"&gt;FILES&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!FILES[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;strip &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FILES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt;&lt;span class="nv"&gt;$last_idx&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;
    &lt;span class="k"&gt;fi
  done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'}'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; | &lt;span class="nb"&gt;gzip&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No custom JavaScript transformation, no intermediate combined file, and only modest streaming overhead.&lt;/p&gt;

&lt;p&gt;Because our final output has a perfectly flat, highly consistent structure, it is the absolute dream scenario for Gzip's DEFLATE algorithm (which replaces repeating strings with tiny backreference pointers). The repetitive metadata keys ("src", "eco", "moves") and the matching FEN ranks collapse with staggering efficiency.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Keeping the Client Code Dead Simple
&lt;/h3&gt;

&lt;p&gt;By resisting the urge to build a custom compression format, we kept our frontend and API code unbelievably clean. &lt;/p&gt;

&lt;p&gt;Because the database remains a standard, flat JSON key-value map, the frontend doesn't need a "rehydration" step, a parsing library, or any complex state management. It simply loads the JSON, and our lookup logic is a blazing-fast, O(1) direct memory access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChessOpeningDatabase&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jsonData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Blazing-fast direct lookup&lt;/span&gt;
  &lt;span class="nf"&gt;getMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fenString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fenString&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No CPU-heavy iteration, no dictionary mapping, and zero initialization delay when the application boots up on a cheap mobile device.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Payoff: Zero Effort, Massive Savings
&lt;/h3&gt;

&lt;p&gt;By keeping the database flat and letting native server-to-browser compression do its job, we achieved a production-ready payload with absolute minimal friction:&lt;/p&gt;

&lt;p&gt;The final payload delivered over the wire to the client's browser is &lt;strong&gt;469 KB&lt;/strong&gt;—smaller than a single average-sized JPEG image. Yet, it contains a fully functional, mathematically sound, 17,000-edge navigational map of the entire history of chess openings, complete with multiple historical source aliases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping Up the Chronicles
&lt;/h3&gt;

&lt;p&gt;This project started with a simple problem: my chess video generator had holes in its openings because our data sources didn't understand transpositions. &lt;/p&gt;

&lt;p&gt;By the end of this journey, we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Built a &lt;strong&gt;Directed Acyclic Graph (DAG)&lt;/strong&gt; to map transpositions and engineered synthetic bridges to repair 3,600 "orphan" positions.&lt;/li&gt;
&lt;li&gt; Created a &lt;strong&gt;normalization sanitizer&lt;/strong&gt; to force ten chaotic, historical databases to speak with one voice, culminating in a custom scraper to tame a relentlessly uncooperative Wikibooks.&lt;/li&gt;
&lt;li&gt; Delivered the entire system as a &lt;strong&gt;469 KB production payload&lt;/strong&gt; by leaning into structural repetition and native browser compression.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Software engineering isn't just about making things work. Sometimes, it is about knowing when &lt;em&gt;not&lt;/em&gt; to write code. By letting standard web protocols do what they do best, we kept our codebase simple, our payload featherweight, and our user experience flawless.&lt;/p&gt;

&lt;h3&gt;
  
  
  See It in Action
&lt;/h3&gt;

&lt;p&gt;The compressed graph is not merely a benchmark artifact. It powers &lt;a href="https://fensterchess.com" rel="noopener noreferrer"&gt;Fenster&lt;/a&gt;, an opening-research web app that lets you play through moves, follow transpositions, and explore the opening names attached to each resulting position.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/h3t6e4ojgmaglappgh1k.png" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcem0d26b9f4rep2ji5c2.png" alt="Fenster showing a Queen's Gambit Declined position, named continuations, and a transposition" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

The opening graph at work in Fenster: one position, several continuations, and a transposition into another opening family. Click the image for a larger view. 


 

&lt;p&gt;[&lt;a href="https://fensterchess.com/?moves=1.%20d4%20d5%202.%20c4%20e6" rel="noopener noreferrer"&gt;Explore this position in Fenster&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Fenster is the practical test of the architecture described in this series: the graph must be complete enough to navigate, consistent enough to explain what the user is seeing, and small enough to load without getting in the way.&lt;/p&gt;

&lt;p&gt;What began as a collection of conflicting opening records is now a usable research tool—one that can trace how openings branch, converge, transpose, and acquire their names.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>database</category>
      <category>architecture</category>
      <category>chess</category>
    </item>
    <item>
      <title>Source Wars: The Chaos of Ten Conflicting Databases</title>
      <dc:creator>Jeff Lowery</dc:creator>
      <pubDate>Wed, 22 Jul 2026 16:51:04 +0000</pubDate>
      <link>https://dev.to/jeffml/source-wars-the-chaos-of-ten-conflicting-databases-6n0</link>
      <guid>https://dev.to/jeffml/source-wars-the-chaos-of-ten-conflicting-databases-6n0</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fupload.wikimedia.org%2Fwikipedia%2Fcommons%2Fthumb%2Ff%2Ffc%2FPieter_Bruegel_the_Elder_-_The_Tower_of_Babel_%28Vienna%29_-_Google_Art_Project_-_edited.jpg%2F1280px-Pieter_Bruegel_the_Elder_-_The_Tower_of_Babel_%28Vienna%29_-_Google_Art_Project_-_edited.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fupload.wikimedia.org%2Fwikipedia%2Fcommons%2Fthumb%2Ff%2Ffc%2FPieter_Bruegel_the_Elder_-_The_Tower_of_Babel_%28Vienna%29_-_Google_Art_Project_-_edited.jpg%2F1280px-Pieter_Bruegel_the_Elder_-_The_Tower_of_Babel_%28Vienna%29_-_Google_Art_Project_-_edited.jpg" alt="The Tower of Babel by Bruegel the Elder" width="800" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Part 2: How I forced ten chaotic, crowdsourced, and uncooperative data sources to speak the same language.&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;In Part 1, &lt;a href="https://dev.to/jeffml/the-transposition-trap-and-3600-fake-chess-positions-1igd"&gt;The Transposition Trap&lt;/a&gt;, we successfully mapped our chess openings into a Directed Acyclic Graph (DAG) and built 3,600 synthetic bridges to link our orphaned variations. Mathematically, our graph was beautiful. &lt;/p&gt;

&lt;p&gt;But content-wise? It was a screaming, chaotic tower of Babel. &lt;/p&gt;

&lt;p&gt;To build a truly comprehensive, open-source chess opening database, you can't rely on just one source. You have to aggregate. I pulled data from ten different legendary chess repositories: Lichess, Arasan, ICSBot, ChessTempo, and more. &lt;/p&gt;

&lt;p&gt;But when I tried to merge them, I realized that chess players—and the developers who write chess software—have been disagreeing on how to write down names, moves, and codes for decades. &lt;/p&gt;

&lt;p&gt;This is the story of how I survived the Source Wars.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Battleground: Name Fragmentation
&lt;/h3&gt;

&lt;p&gt;You would think that a standardized code system like ECO (Encyclopaedia of Chess Openings) would keep things uniform. It doesn't. &lt;/p&gt;

&lt;p&gt;ECO codes (like &lt;code&gt;C89&lt;/code&gt;) only cover broad families. The actual &lt;em&gt;names&lt;/em&gt; of the variations are left to the whims of whoever is typing them into a database. When merging my ten sources, the same exact position would return completely different strings depending on who wrote the file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Source A:&lt;/strong&gt; &lt;code&gt;Ruy Lopez: Marshall Attack, Main Line&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Source B:&lt;/strong&gt; &lt;code&gt;Spanish Game: Marshall Gambit&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Source C:&lt;/strong&gt; &lt;code&gt;Ruy Lopez: Marshall, Modern Variation&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Source D:&lt;/strong&gt; &lt;code&gt;Ruy Lopez, Marshall Blackburne Variation&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I merged these blindly, my UI would cycle through four different names for the exact same board state, giving the user digital whiplash. I needed a single, canonical truth.&lt;/p&gt;

&lt;p&gt;To solve this, I wrote a custom sanitizer pipeline that strips out fluff words (like "Game", "System", or "Opening" when they don't add value), standardizes punctuation, and forces a strict hierarchy of &lt;code&gt;[Primary Opening]: [Variation], [Sub-variation]&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;But that was the easy part. Then, I met the ultimate boss of uncooperative data.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Ultimate Boss: Wikibooks is Not a Database
&lt;/h3&gt;

&lt;p&gt;Of all ten data sources, Wikibooks was the only one relentlessly hostile to automation. Every other source gave me beautifully structured data: TSV columns, CSV rows, nested JSON trees, or &lt;a href="https://en.wikipedia.org/wiki/Portable_Game_Notation" rel="noopener noreferrer"&gt;good old PGN&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Wikibooks instead gave me 500 human-edited wiki pages where the &lt;strong&gt;URL itself is the data source&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how Wikibooks encodes a chess opening's move history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[https://en.wikibooks.org/wiki/Chess_Opening_Theory/1._e4/1...e5/2._Nf3/2...Nc6/3._Bb5](https://en.wikibooks.org/wiki/Chess_Opening_Theory/1._e4/1...e5/2._Nf3/2...Nc6/3._Bb5)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;White moves use &lt;code&gt;{N}._ {piece}{square}&lt;/code&gt;, while Black responses use &lt;code&gt;{N}... {piece}{square}&lt;/code&gt;. The URL &lt;em&gt;is&lt;/em&gt; the move sequence. The HTML page itself only tells you the opening's name—if you are lucky.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Regex Pipeline of Doom
&lt;/h4&gt;

&lt;p&gt;Extracting Standard Algebraic Notation (SAN) from these URLs required building a multi-pass regex pipeline just to clean up the formatting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Strip underscores:&lt;/strong&gt; Translate &lt;code&gt;1._e4&lt;/code&gt; into &lt;code&gt;1. e4&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Drop Black move numbers:&lt;/strong&gt; Parse &lt;code&gt;1...e5&lt;/code&gt; and strip the leading numbers to get just &lt;code&gt;e5&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Castling Trap:&lt;/strong&gt; Wikibooks editors frequently used zeroes instead of the capital letter 'O' for castling. I had to map &lt;code&gt;0-0&lt;/code&gt; to &lt;code&gt;O-O&lt;/code&gt; on the fly.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;URL Artifacts:&lt;/strong&gt; Scrub out percentage-encoded annotations like &lt;code&gt;%2B&lt;/code&gt; (check), &lt;code&gt;%3F&lt;/code&gt; (theoretical novelty), and &lt;code&gt;!&lt;/code&gt; (good move).&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Wikibooks Editors Are Not Database Engineers
&lt;/h4&gt;

&lt;p&gt;The name you scrape from a wiki heading is whatever a volunteer typed in back in 2008. I found "King's Pawn opening" competing with "King's Pawn Game." I found "Latvian Gambit" fighting "Greco Countergambit." &lt;/p&gt;

&lt;p&gt;Worse, many sub-variations completely lacked their parent context. A wiki page heading might say simply &lt;code&gt;"Mengarini Variation"&lt;/code&gt;, but my database needed the full lineage: &lt;code&gt;"Sicilian Defense: Mengarini Variation"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9m41emwlptiitx9pdyz1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9m41emwlptiitx9pdyz1.jpeg" alt="exasperated" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To keep my sanity, I had to maintain a &lt;strong&gt;4,244-entry alias map (&lt;code&gt;aliases.txt&lt;/code&gt;)&lt;/strong&gt; to manually normalize Wiki names to canonical forms. I also designed a word-overlap algorithm that looks at the parent page, determines if the child heading is missing context, and prepends the parent's opening name—but only if the overlap is insufficient. (For instance, &lt;code&gt;"Latvian Gambit: Mayet Attack"&lt;/code&gt; only needs &lt;code&gt;"Mayet Attack"&lt;/code&gt; appended, not the full parent string).&lt;/p&gt;

&lt;h4&gt;
  
  
  Malformed URLs and Dead Ends
&lt;/h4&gt;

&lt;p&gt;When humans write URLs, things break. Eight wiki pages had URLs that completely violated the naming convention. I ran into missing underscores (&lt;code&gt;4.e5&lt;/code&gt; instead of &lt;code&gt;4._e5&lt;/code&gt;), extra periods (&lt;code&gt;4....Nfd7&lt;/code&gt;), and a Caro-Kann variation whose URL inexplicably pointed to a London System page. I had to hardcode a &lt;code&gt;corrections.json&lt;/code&gt; file to intercept and patch these malformed URLs before the regex engine even looked at them.&lt;/p&gt;

&lt;p&gt;I also hit the &lt;strong&gt;"anonymous continuation"&lt;/strong&gt; dead end. Some wiki pages exist solely to sub-divide a long variation, so their heading is just the parent opening's name repeated. The page for:&lt;br&gt;
&lt;code&gt;1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O&lt;/code&gt; &lt;br&gt;
...had the heading &lt;code&gt;"Ruy Lopez"&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This is identical to the root opening eight moves prior! These are paths the wiki community wanted to document, but never bothered to name. My parser had to detect these by measuring the move-back distance, skipping them entirely rather than polluting the database with duplicate names.&lt;/p&gt;
&lt;h4&gt;
  
  
  The MediaWiki API as a Change Detector
&lt;/h4&gt;

&lt;p&gt;To avoid hammering Wikipedia's servers, I wrote a change-detection script. The parser queries the MediaWiki API for the last revision timestamp of the &lt;code&gt;Chess_Opening_Theory&lt;/code&gt; root page. If nothing has changed, the parser refuses to run. &lt;/p&gt;

&lt;p&gt;I took this a step further with a custom tool, &lt;code&gt;diff-wiki.js&lt;/code&gt;. It fetches the 500 most recent edits to the category, filters them down to our specific opening pages, deduplicates them, and displays &lt;em&gt;exactly&lt;/em&gt; which lines changed. This lets me audit human edits before committing to a full database rebuild.&lt;/p&gt;


&lt;h3&gt;
  
  
  The Name-Resolution Algorithm
&lt;/h3&gt;

&lt;p&gt;Once the Wikibooks data was scraped, sanitized, and corrected, I merged it back into the master pool with the other nine sources. To resolve conflicts when multiple sources claimed different names for the exact same FEN, I established a strict &lt;strong&gt;Source Hierarchy&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SOURCE_PRIORITY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lichess&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Most modern, clean, and player-standardized&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;caissa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Solid, historically accurate database&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wikibooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Rich in variation names, but needs heavy sanitization&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;arasan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Great engine book, but terse names&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openings_txt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// Legacy fallback&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When two databases disagreed, the algorithm checked the priority array. If &lt;code&gt;lichess&lt;/code&gt; named a position &lt;code&gt;"Sicilian Defense: Old Sicilian"&lt;/code&gt;, it overrode &lt;code&gt;arasan&lt;/code&gt; calling it &lt;code&gt;"KP: Sicilian"&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If the conflict was between names within the same priority level, a &lt;strong&gt;string similarity threshold&lt;/strong&gt; (using Jaro-Winkler distance) evaluated the options. If they were 95% similar, it picked the shorter, cleaner name. If they diverged completely, it flagged the FEN for manual review in my &lt;code&gt;corrections.json&lt;/code&gt; file.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Result: One Voice for 17,000 Positions
&lt;/h3&gt;

&lt;p&gt;By building a robust sanitization pipeline, surviving the wild west of Wikibooks URL-scraping, and enforcing a strict source hierarchy, we achieved something incredible. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fja406l1gk8d4axviuxc2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fja406l1gk8d4axviuxc2.png" alt="back to sanity" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We took ten fragmented, historical, and crowdsourced files and merged them into a singular, cohesive database. Every single one of our 17,000 graph edges (named openings + &lt;a href="https://dev.to/jeffml/the-transposition-trap-and-3600-fake-chess-positions-1igd#the-interpolation-rabbit-hole"&gt;interpolated positions&lt;/a&gt; now speaks with the exact same voice. &lt;/p&gt;

&lt;p&gt;The lesson here is simple: &lt;strong&gt;crowdsourced data isn't dirty—it's just data that trusted its authors instead of its consumers.&lt;/strong&gt; Parsing it means building a solid bridge between human editing habits and machine-readable structures.&lt;/p&gt;

&lt;p&gt;Now that our data is clean, unified, and fully connected, we face our final hurdle: &lt;strong&gt;How do we fit a 17,000-edge graph into a file small enough to load instantly on a cheap phone?&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Watch for Part 3: Size Matters!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>database</category>
      <category>architecture</category>
      <category>chess</category>
    </item>
    <item>
      <title>The Transposition Trap and 3,600 "Fake" Chess Positions</title>
      <dc:creator>Jeff Lowery</dc:creator>
      <pubDate>Tue, 14 Jul 2026 22:08:18 +0000</pubDate>
      <link>https://dev.to/jeffml/the-transposition-trap-and-3600-fake-chess-positions-1igd</link>
      <guid>https://dev.to/jeffml/the-transposition-trap-and-3600-fake-chess-positions-1igd</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkkqgbcs4yw3y6vqga8yo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkkqgbcs4yw3y6vqga8yo.jpg" alt="Galloping Gertie--Tacoma Narrows Bridge Disaster" width="365" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Part 1: The Graph, The Orphans, and the Synthetic Bridge&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;If you ask a casual chess player what an opening is, they’ll give you a simple answer: it’s a name, an ECO (Encyclopedia of Chess Openings) code, and a sequence of moves. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;1. e4 e5 2. Nf3 Nc6 3. Bb5&lt;/code&gt; is the Ruy Lopez. Clean. Elegant. Linear.&lt;/p&gt;

&lt;p&gt;If you ask a software engineer who maintains a chess opening database how hard it could be to map these openings, they’ll likely utter the famous last words of every data engineer: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It’s just a flat list of moves. How hard could it be?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I know this because I am that engineer. I maintain &lt;code&gt;@chess-openings/eco.json&lt;/code&gt;, an open-source data package on npm. And during a recent extensive rewrite of my data pipeline, I was violently reminded that in the world of chess data, linearity is an illusion, FEN strings are slippery shapes, and the data structure isn’t a list at all.&lt;/p&gt;

&lt;p&gt;It’s a directed nightmare of a graph.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Transposition Trap
&lt;/h3&gt;

&lt;p&gt;Let’s start with the fundamental lie of chess data: that a position’s identity is tied to how you got there.&lt;/p&gt;

&lt;p&gt;In chess, different move orders can lead to the exact same layout of pieces on the board. This is known as a &lt;strong&gt;transposition&lt;/strong&gt;. Consider the Benoni Defense. You can reach a classic Benoni position via the canonical move order:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;1. d4 Nf6 2. c4 c5 3. d5&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1us1mwfinmuj4ei7eb7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1us1mwfinmuj4ei7eb7.png" alt="The Benoni Opening via move order 1" width="382" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But what happens if Black plays a slightly different order to achieve the exact same strategic setup? &lt;/p&gt;

&lt;p&gt;&lt;code&gt;1. c4 c5 2. d4 Nf6 3. d5&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1us1mwfinmuj4ei7eb7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb1us1mwfinmuj4ei7eb7.png" alt="The same opening via a different move order" width="382" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To the human eye, the board is identical. To a database relying strictly on Forsythe-Edwards Notation (FEN) as a strict unique identifier, they can look like entirely different entities. A standard FEN string doesn't just track where the pieces are; it tracks whose turn it is, castling rights, and potential &lt;em&gt;en passant&lt;/em&gt; squares.&lt;/p&gt;

&lt;p&gt;If White changes the order of their pawn pushes, castling rights or &lt;em&gt;en passant&lt;/em&gt; availability might technically differ for a fleeting move, even if the piece layout converges. &lt;em&gt;(Yes, I hear you, chess purists: technically, an active en passant option makes identical piece layouts mathematically distinct game states. But for the sake of navigation mapping, we must occasionally fly close to the sun).&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;If your data pipeline treats a full, standard FEN string as an absolute, immutable identity, you will instantly split your data universe in two.&lt;/p&gt;

&lt;p&gt;The fix? I had to implement a strict position-only FEN matching fallback across the entire ingestion engine. In chess data engineering, you quickly learn that a full FEN is merely a suggestion; the raw piece placement layout is the true identity.&lt;/p&gt;




&lt;h3&gt;
  
  
  A Directed Nightmare: The &lt;code&gt;fromTo&lt;/code&gt; Graph
&lt;/h3&gt;

&lt;p&gt;Once you accept that positions transpose, your flat file of chess openings explodes into a massive directed graph. In my latest build of the pipeline, this graph ballooned to 17,500 edges connecting over 16,000 unique nodes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Position A] --(Move: c5)--&amp;gt; [Position B] --(Move: d5)--&amp;gt; [Position C]
    \                                                         /
     \-----------------(Alternative Path)--------------------/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every single opening and variation in the database must know where it came from (&lt;code&gt;from&lt;/code&gt;) and where it can go (&lt;code&gt;to&lt;/code&gt;). If a single link breaks, that entire branch of chess history becomes unreachable noise in the application layer.&lt;/p&gt;

&lt;p&gt;When I ran my newly rewritten pipeline for the very first time, it didn't gracefully compile. It screamed. The integrity report spat out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;45 disconnected interpolated openings&lt;/strong&gt; (floating in the void)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;87 duplicate edges&lt;/strong&gt; (circular logic loops)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;16 FEN collisions&lt;/strong&gt; (two different opening names claiming the exact same board state)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fixing this wasn’t a matter of tweaking a JSON file. It required rewriting four fundamental pipeline changes across three separate architecture files to enforce strict deduplication at the moment of generation, rather than trying to retrofit it after the fact.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Interpolation Rabbit Hole
&lt;/h3&gt;

&lt;p&gt;But the deepest psychological damage came from what I call "Orphan Openings."&lt;/p&gt;

&lt;p&gt;The immediate problem appeared in &lt;a href="https://fensterchess.com" rel="noopener noreferrer"&gt;Fenster&lt;/a&gt;, my browser-based opening research app: legitimate opening lines sometimes became unreachable because their source records did not form a continuous path.&lt;/p&gt;

&lt;p&gt;When you scrape, merge, and normalize chess-opening data from ten different online sources, you inevitably encounter records for deeply specific subvariations. A source might, for example, name a line twenty moves deep in the King’s Indian Defense.&lt;/p&gt;

&lt;p&gt;The problem is that the source may contain a record for an early position, &lt;strong&gt;Opening A&lt;/strong&gt;, and another for a much deeper position, &lt;strong&gt;Opening Z&lt;/strong&gt;, while omitting every intervening position. Opening Z’s move list includes the moves that pass through those positions, but the source provides no separate entries for them.&lt;/p&gt;

&lt;p&gt;This leaves a broken path in the opening graph. Opening A has no chain of child positions leading forward to Opening Z, even though Opening Z’s move sequence reveals exactly what those missing positions must be. To connect the graph, the intervening positions must be generated from Opening Z’s move list and inserted between A and Z.&lt;/p&gt;

&lt;p&gt;To make the graph navigable, I had to build an interpolation engine. When the pipeline encounters an orphan, it is forced to go down a recursive rabbit hole. It triggers a function called &lt;code&gt;lineOfDescent&lt;/code&gt;, which programmatically walks backward through the move sequence, move by move, generating synthetic bridge nodes until it finally collides with a known, recognized opening.&lt;/p&gt;

&lt;p&gt;Think about the absurdity of this: &lt;strong&gt;I had to programmatically generate 3,625 synthetic chess positions solely so that real chess openings could find each other.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These interpolated entries don't have historical flavor text or specific human names; they exist purely as the digital mortar holding the bricks of the graph together. If the recursion logic is off by even a single character, it triggers a recursion explosion, spinning out infinitely as it tries to resolve transpositions that loop back on themselves.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Joy of the Green Check
&lt;/h3&gt;

&lt;p&gt;After days of wrestling with the graph topology, adjusting fallback FEN algorithms, and ensuring the &lt;code&gt;lineOfDescent&lt;/code&gt; didn't eat my CPU, I finally ran the verification suite.&lt;/p&gt;

&lt;p&gt;There is a unique, quiet satisfaction that only a developer knows when a wall of red terminal text finally turns into a clean, green monolithic block of passing checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ fen-unique: 12509 unique in ecoA-E, 3625 in interpolated, 0 overlaps
✓ eco-prefix: all entries match their category file
✓ interp-isolation: interpolated entries correctly separated
✓ fromto-ref: all 17565 transitions reference existing FENs
✓ fromto-dup: no duplicate transitions
✓ valid-src: all source identifiers are valid
✓ rootsrc: all 3625 interpolated entries have rootSrc
✓ interp-connect: all 3625 interpolated entries connected in fromTo graph

✗ 0 failure(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The graph was whole. The orphans were adopted. The synthetic bridges held.&lt;/p&gt;

&lt;p&gt;But normalizing the graph topology was only half the battle. Once the architecture was solid, I had to plunge into the messy, politically charged, and deeply inconsistent world of human naming conventions—a data normalization war where there are no winners.&lt;/p&gt;

&lt;p&gt;In the next article, we’ll dive into "Source Wars," dealing with the chaos of ten conflicting databases, and why "King's Indian Attack" and "Reti: KIA" are a data engineer's worst nightmare.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>database</category>
      <category>architecture</category>
      <category>chess</category>
    </item>
    <item>
      <title>How to build an associative graph using React + p5</title>
      <dc:creator>Jeff Lowery</dc:creator>
      <pubDate>Sun, 12 May 2024 19:21:23 +0000</pubDate>
      <link>https://dev.to/jeffml/how-to-build-an-associative-graph-using-react-p5-25ic</link>
      <guid>https://dev.to/jeffml/how-to-build-an-associative-graph-using-react-p5-25ic</guid>
      <description>&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;I've been working on a &lt;a href="https://fensterchess.com" rel="noopener noreferrer"&gt;chess opening database&lt;/a&gt; in my copious spare time, and have begun experimenting with visualizations of said openings. One of those is a relationship graph (see above) that displays origin square to destination square of each opening move. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Farmer in the Dell
&lt;/h2&gt;

&lt;p&gt;Relationships (a type of association) can be found everywhere. In this post, I'll graph the relationships as described in a nursery rhyme called "The Farmer in the Dell".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The farmer in the dell&lt;br&gt;
The farmer in the dell&lt;br&gt;
Hi-ho, the derry-o&lt;br&gt;
The farmer in the dell&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After establishing that there's a farmer that lives in a dell, a series of new characters are introduced via the verb "takes".&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The farmer takes a wife&lt;/li&gt;
&lt;li&gt;The wife takes the child&lt;/li&gt;
&lt;li&gt;The child takes the nurse&lt;/li&gt;
&lt;li&gt;The nurse takes the cow&lt;/li&gt;
&lt;li&gt;The cow takes the dog&lt;/li&gt;
&lt;li&gt;The dog takes the cat&lt;/li&gt;
&lt;li&gt;The cat takes the mouse&lt;/li&gt;
&lt;li&gt;The mouse takes the cheese&lt;/li&gt;
&lt;li&gt;The cheese stands alone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The word "takes" is way overloaded here, but that can be addressed later. From this list of relationships we can see that the farmer and the cheese have a single relation (terminal nodes in the graph to come), but all other characters have two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graphing the characters
&lt;/h2&gt;

&lt;p&gt;To display the relationships among the characters in the rhyme, we can start by arranging each in a circle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97uenplpmwheuspviw8w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97uenplpmwheuspviw8w.png" alt="Our cast of characters" width="800" height="804"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm using the JavaScript graphics library &lt;a href="https://p5js.org/" rel="noopener noreferrer"&gt;p5&lt;/a&gt; inside a react component, like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* eslint-disable react/prop-types */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;p5&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;farmer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;wife&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;child&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;nurse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;cow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;dog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;mouse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="na"&gt;cheese&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Version1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;p5&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;character&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                    &lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TWO_PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;//move 0,0 to the center of the canvas&lt;/span&gt;
                &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ellipseMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textAlign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CENTER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;textFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Georgia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ellipse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Version1"&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;renderRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without delving too deep into p5, it's worth pointing out some features of this code.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to render graphics inside a React component
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;const renderRef = useRef();&lt;/code&gt;&lt;br&gt;
&lt;code&gt;p.createCanvas(600, 600).parent(renderRef.current);&lt;/code&gt;&lt;br&gt;
&lt;code&gt;return &amp;lt;div id="Version1" ref={renderRef}&amp;gt;&amp;lt;/div&amp;gt;;&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  useEffect() cleanup function
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;    &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;p5&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;remove&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="cm"&gt;/* ... */&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;the p5 remove function 'cleans up' the previous render before useEffect is called again. &lt;/p&gt;
&lt;h3&gt;
  
  
  Setting up the character locations
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;         &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Note the two Object.entries() calls. This is done to get an index to calculate each character's angle from the center:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;     &lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TWO_PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Drawing the relationships
&lt;/h2&gt;

&lt;p&gt;Now to draw a line from each taker to each taken character. First, I add a "takes" relation from farmer all the way down to cheese, then draw a line from the taker location to the taken location.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doRelations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// for this version, all relations are the same.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;takes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/* ... */&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;character&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;takes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;character&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;takes&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;taken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ellipse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2uuseg78nr8mm4un3j02.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2uuseg78nr8mm4un3j02.png" alt="lines of relationships" width="800" height="801"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not terribly interesting, but it's a start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements analysis
&lt;/h2&gt;

&lt;p&gt;Let's reexamine the word "takes". It's usage is very ambiguous and often nonsensical. Here's one attempt to disambiguate the rhyme:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The farmer &lt;strong&gt;&lt;em&gt;married&lt;/em&gt;&lt;/strong&gt; a wife&lt;/li&gt;
&lt;li&gt;The wife &lt;strong&gt;&lt;em&gt;married&lt;/em&gt;&lt;/strong&gt; a farmer&lt;/li&gt;
&lt;li&gt;The wife &lt;strong&gt;&lt;em&gt;adopts&lt;/em&gt;&lt;/strong&gt; the child&lt;/li&gt;
&lt;li&gt;The wife &lt;strong&gt;&lt;em&gt;employs&lt;/em&gt;&lt;/strong&gt; the nurse&lt;/li&gt;
&lt;li&gt;The child &lt;strong&gt;&lt;em&gt;needs&lt;/em&gt;&lt;/strong&gt; the nurse&lt;/li&gt;
&lt;li&gt;The nurse &lt;strong&gt;&lt;em&gt;cares for&lt;/em&gt;&lt;/strong&gt; the child&lt;/li&gt;
&lt;li&gt;The farmer &lt;strong&gt;&lt;em&gt;owns&lt;/em&gt;&lt;/strong&gt; the cow&lt;/li&gt;
&lt;li&gt;The nurse &lt;strong&gt;&lt;em&gt;milks&lt;/em&gt;&lt;/strong&gt; the cow&lt;/li&gt;
&lt;li&gt;The farmer &lt;strong&gt;&lt;em&gt;owns&lt;/em&gt;&lt;/strong&gt; the dog&lt;/li&gt;
&lt;li&gt;The dog &lt;strong&gt;&lt;em&gt;guards&lt;/em&gt;&lt;/strong&gt; the cow&lt;/li&gt;
&lt;li&gt;The dog &lt;strong&gt;&lt;em&gt;befriends&lt;/em&gt;&lt;/strong&gt; the cat&lt;/li&gt;
&lt;li&gt;The cat &lt;strong&gt;&lt;em&gt;befriends&lt;/em&gt;&lt;/strong&gt; the dog&lt;/li&gt;
&lt;li&gt;The cat &lt;strong&gt;&lt;em&gt;adopts&lt;/em&gt;&lt;/strong&gt; the farmer&lt;/li&gt;
&lt;li&gt;The cat &lt;strong&gt;&lt;em&gt;hunts&lt;/em&gt;&lt;/strong&gt; the mouse&lt;/li&gt;
&lt;li&gt;The mouse &lt;strong&gt;&lt;em&gt;eats&lt;/em&gt;&lt;/strong&gt; the cheese&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's see what the relationship graph looks like now:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmk8xtymd2m3ztx2tc5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpmk8xtymd2m3ztx2tc5t.png" alt="further relationships" width="774" height="772"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not bad, but now let's color-code the relationships and add a legend:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa9uan2jefdon3yobdegs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa9uan2jefdon3yobdegs.png" alt="relationships color-coded" width="799" height="651"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawing curves instead of lines
&lt;/h2&gt;

&lt;p&gt;One problem with using lines is that in reciprocal relationships, such as "married", one line overwrites another, obscuring the former relationship. Bezier curves can be used instead of lines, and look better. Each bezier curve takes eight arguments, which are the (x,y) coordinates of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the start point&lt;/li&gt;
&lt;li&gt;the first control point&lt;/li&gt;
&lt;li&gt;the second control point&lt;/li&gt;
&lt;li&gt;the end point&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What are these control points? Here's an illustration:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flejrrzql4cteparl1qdb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flejrrzql4cteparl1qdb.png" alt="bezier control points" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The shape of the curve can be adjusted by moving the control points. In this case, I'll set the first control point to be halfway between the start location and the center of the graph, and similarly for the end point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    const cp1 = [x / 2, y / 2];
    const cp2 = [x2 / 2, y2 / 2];

    p.noFill();
    p.bezier(x, y, ...cp1, ...cp2, x2, y2);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4eqoj2i5pwntjdfnxsby.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4eqoj2i5pwntjdfnxsby.png" alt="curves instead of lines" width="800" height="810"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Though aesthetically better, the change didn't fix the overwrite problem. The control points can be adjusted for each character by adding a "fudge" factor to the control point equation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fudge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fudge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fudge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nx2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;x2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fudge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ny2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;y2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fudge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cp1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cp2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nx2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ny2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the variable &lt;strong&gt;i&lt;/strong&gt; is a counter that is increased as each character in the rhyme is rendered. The final output is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmut2kdg0kt0i2k60ioko.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmut2kdg0kt0i2k60ioko.png" alt="further adjustment to curves" width="800" height="810"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Arrows? What about arrows?
&lt;/h2&gt;

&lt;p&gt;One thing missing from the diagram is an indication of the direction of the relationship, e.g. "farmer owns dog" and not "dog owns farmer". However, there is no native support for arrowheads in p5, so it takes quite a bit of trigonometry to draw them...another post in itself. See links below for more info on the subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;A complete program can be found &lt;a href="https://github.com/JeffML/farmer-in-the-dell" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Useful links:
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://editor.p5js.org/kevinhb92/sketches/zLDDLrg_" rel="noopener noreferrer"&gt;This example&lt;/a&gt; gets halfway to an arrow feature.&lt;/p&gt;

&lt;p&gt;One thing is to have the tip of the arrow touch the edge of each character's circle, using the formula in the answer found &lt;a href="https://stackoverflow.com/questions/14096138/find-the-point-on-a-circle-with-given-center-point-radius-and-degree" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>graphical</category>
      <category>javascript</category>
      <category>p5</category>
    </item>
  </channel>
</rss>
