<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Creating with Data]]></title><description><![CDATA[Create — Play — Learn]]></description><link>https://creatingwithdata.com/</link><image><url>https://creatingwithdata.com/favicon.png</url><title>Creating with Data</title><link>https://creatingwithdata.com/</link></image><generator>Ghost 5.2</generator><lastBuildDate>Tue, 31 Mar 2026 03:28:30 GMT</lastBuildDate><atom:link href="https://creatingwithdata.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Why Some Code Examples Are No Longer Working]]></title><description><![CDATA[<h1></h1><p>Some of the interactive code examples and demos in older posts on <em>Creating With Data</em> were originally hosted on <a href="https://glitch.com/">Glitch</a>, a platform for building and sharing live code projects.</p><p>Recently, Glitch <a href="https://blog.glitch.com/post/changes-are-coming-to-glitch/">announced</a> that it is shutting down its free hosting services. As a result, many of the embedded demos and</p>]]></description><link>https://creatingwithdata.com/transfer/</link><guid isPermaLink="false">688e595f6e945a01d6be7a64</guid><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Mon, 04 Aug 2025 10:08:25 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2025/08/16e97fbf-41c1-4b7c-8044-4bf28ace669a.png" medium="image"/><content:encoded><![CDATA[<h1></h1><img src="https://creatingwithdata.com/content/images/2025/08/16e97fbf-41c1-4b7c-8044-4bf28ace669a.png" alt="Why Some Code Examples Are No Longer Working"><p>Some of the interactive code examples and demos in older posts on <em>Creating With Data</em> were originally hosted on <a href="https://glitch.com/">Glitch</a>, a platform for building and sharing live code projects.</p><p>Recently, Glitch <a href="https://blog.glitch.com/post/changes-are-coming-to-glitch/">announced</a> that it is shutting down its free hosting services. As a result, many of the embedded demos and linked projects that relied on Glitch may no longer load or function correctly.</p><p>I&#x2019;m currently working on migrating this blog and all the projects I had on Glitch, including my personal website. </p><p>I&apos;m going to consolidate many of my personal sites and projects, but it&apos;ll probably take me a while. It&apos;s quite an undertaking as I&apos;ve been using Glitch for about 8-9 years now! </p>]]></content:encoded></item><item><title><![CDATA[Saving D3.js animations as video files with WebAssembly]]></title><description><![CDATA[Browsers are more powerful than ever! Now we can use FFmpeg + Resvg in the browser to save D3 animations as video files. ]]></description><link>https://creatingwithdata.com/capturing-d3-js-animations-with-resvg-and-ffmpeg/</link><guid isPermaLink="false">6720f2dfcb866f02924180b5</guid><category><![CDATA[d3.js]]></category><category><![CDATA[webassembly]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Tue, 29 Oct 2024 17:28:57 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2024/10/Zeichenfla-che-1-1500x880-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2024/10/Zeichenfla-che-1-1500x880-1.png" alt="Saving D3.js animations as video files with WebAssembly"><p>Browsers are more powerful than ever! Non-browser software can be compiled to WebAssembly and used client-side. WebAssembly or &apos;WASM&apos; acts as a bridge, allowing code written in languages like Rust, C, and C++ to execute smoothly in the browser.</p><p>In my <a href="https://supermap.world/">SuperMap.world</a> side-project I&apos;m using D3 to generatively create lots of map designs in PNG and SVG formats. To up the challenge, I&apos;ve been considering adding map animations using <a href="https://d3js.org/d3-transition">d3-transition</a>. </p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/10/map.gif" class="kg-image" alt="Saving D3.js animations as video files with WebAssembly" loading="lazy" width="320" height="366"></figure><p>The only problem is compiling video can be a pretty heavy operation (certainly for servers on a side-project budget). Thanks to WebAssembly, we can now get those lazy freeloading clients to do some of the work. <br><br>To tip my toe in the water, I&apos;ve been experimenting with <a href="https://github.com/yisibl/resvg-js">Resvg</a> and <a href="https://ffmpegwasm.netlify.app/">FFmpeg</a> (compiled to WASM) to see if I can capture D3.js transitions and render them out to Gif, MP4, or whatever format &#x2013; no servers req<em>uired!</em></p><p><em>Before you ask &#x2013; yes, I know that rendering to Webcanvas in a raster format then capturing with FFmpeg makes more sense but I have my reasons which I cannot be bothered to explain right now.</em></p><h3 id="capturing-png-with-resvg">Capturing PNG with Resvg</h3><p>Resvg is a powerful tool for rendering and working with SVGs. I&apos;ve been using Resvg to convert SVG to PNG from the command line and it does a great job. The plan here is to use it to capture frames that will eventually be compiled into a video. </p><p>If you want to try it out on its own all you need to do is import and write a capture function e.g.</p><pre><code class="language-HTML">&lt;script src=&quot;https://unpkg.com/@resvg/resvg-wasm&quot;&gt;&lt;/script&gt;</code></pre><pre><code class="language-Javascript">async function captureSvg() {
    // Initialize Resvg WASM
    await resvg.initWasm(
        fetch(&apos;https://unpkg.com/@resvg/resvg-wasm/index_bg.wasm&apos;)
    );

    // Get the SVG element and convert to string
    const svgElement = document.getElementById(&apos;mySvg&apos;);
    const svgString = new XMLSerializer().serializeToString(svgElement);

    // Create Resvg instance
    const resvgJS = new resvg.Resvg(svgString);

    // Render to PNG
    const pngData = resvgJS.render();
    const pngBuffer = pngData.asPng();

    // Create blob and download link
    const blob = new Blob([pngBuffer], { type: &apos;image/png&apos; });
    const url = URL.createObjectURL(blob);
    
    // Create download link
    const a = document.createElement(&apos;a&apos;);
    a.href = url;
    a.download = &apos;capture.png&apos;;
    a.textContent = &apos;Download PNG&apos;;
    document.body.appendChild(a);
}</code></pre><p>As you&apos;ll see above it goes and fetches a .wasm file. Sometimes these files can be quite large &#x2013; FFmpeg is about 30MB! &#xA0; &#xA0;</p><p>In the above example we get a PNG version of the SVG and a download link. We don&apos;t want to download a single PNG, but the above is a concise SVG to PNG convert function (NB if you want text rendered correctly you&apos;ll need to hand that to Resvg).</p><h3 id="combining-d3-resvg-ffmpeg">Combining D3 + Resvg + FFmpeg </h3><p>What we want to do is a little different: capture a PNG every time something changes, (e.g. inside a D3 tween function), hand that PNG over to FFmpeg to use as a frame in the video. Once we&apos;re done animating we compile and offer the MP4 up for download.</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/10/image.png" class="kg-image" alt="Saving D3.js animations as video files with WebAssembly" loading="lazy" width="1026" height="836" srcset="https://creatingwithdata.com/content/images/size/w600/2024/10/image.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/10/image.png 1000w, https://creatingwithdata.com/content/images/2024/10/image.png 1026w" sizes="(min-width: 720px) 720px"></figure><p>This example animates a simple SVG circle and captures each frame. Here&apos;s the code for <a href="https://creating-with-data.glitch.me/connecting-atlantic-ani/simple.html">this example (available on Glitch)</a>:</p><pre><code class="language-HTML">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;script src=&quot;https://d3js.org/d3.v7.min.js&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;https://unpkg.com/@resvg/resvg-wasm&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;./assets/ffmpeg/package/dist/umd/ffmpeg.js&quot;&gt;&lt;/script&gt;
  &lt;script src=&quot;./assets/util/package/dist/umd/index.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;svg id=&quot;animation&quot; width=&quot;400&quot; height=&quot;200&quot;&gt;&lt;/svg&gt;
&lt;script&gt;
const { FFmpeg } = FFmpegWASM;

// Setup constants and initialization
const FRAME_RATE = 12;
let ffmpeg = null;
  
// Initialize capture system
async function initCapture() {
  // Initialize Resvg
  await resvg.initWasm(fetch(&apos;https://unpkg.com/@resvg/resvg-wasm/index_bg.wasm&apos;));

  // Initialize FFmpeg
  const ffmpeg = new FFmpeg();
  ffmpeg.on(&quot;log&quot;, ({ message }) =&gt; {
    console.log(message);
  })
  ffmpeg.on(&quot;progress&quot;, ({ progress }) =&gt; {
    console.log(`${progress * 100} %`);
  });
  
  console.log(&quot;Loading ffmpeg wasm&quot;)
  await ffmpeg.load({
    wasmURL: &quot;https://cdn.glitch.me/4877c24f-4232-45d4-9767-d4c67608b8ed/ffmpeg-core.wasm?v=1730219929011&quot;,
    coreURL: &quot;https://creating-with-data.glitch.me/connecting-atlantic-ani/assets/core/package/dist/umd/ffmpeg-core.js&quot;
  });
  console.log(&quot;Done.&quot;)
  return ffmpeg;
}

// Frame capture function
let frameCount = 0;
let captureQueue = Promise.resolve();
async function captureFrame(svgString) {
  captureQueue = captureQueue.then(async () =&gt; {
    const resvgJS = new resvg.Resvg(svgString);
    const pngData = resvgJS.render();
    const pngBuffer = pngData.asPng();

    const filename = `frame_${frameCount.toString().padStart(5, &apos;0&apos;)}.png`;
    console.log(`Capturing ${filename}`)
    await ffmpeg.writeFile(filename, pngBuffer);
    frameCount++;
  });
  return captureQueue;
}

// Animation with capture
async function animateAndCapture() {
  ffmpeg = await initCapture();

  // Setup SVG
  const svg = d3.select(&quot;#animation&quot;);
  const circle = svg.append(&quot;circle&quot;)
    .attr(&quot;cx&quot;, 50)
    .attr(&quot;cy&quot;, 100)
    .attr(&quot;r&quot;, 20)
    .style(&quot;fill&quot;, &quot;blue&quot;);

  // Create transition
  const duration = 2000; // 2 seconds

  await circle.transition()
    .duration(duration)
    .attr(&quot;cx&quot;, 350)
    .style(&quot;fill&quot;, &quot;red&quot;)
    .tween(&quot;capture&quot;, () =&gt; {
      return async (t) =&gt; {
        // Get current state of SVG
        const svgString = new XMLSerializer()
          .serializeToString(svg.node());
        await captureFrame(svgString);
      };
    })
    .end(); // Wait for transition to complete
  
  // Create video from frames
  await createVideo(ffmpeg);
}

// Create video from captured frames
async function createVideo(ffmpeg) {
  await ffmpeg.exec([
    &apos;-framerate&apos;, FRAME_RATE.toString(),
    &apos;-i&apos;, &apos;frame_%05d.png&apos;,
    &apos;-c:v&apos;, &apos;libx264&apos;,
    &apos;-pix_fmt&apos;, &apos;yuv420p&apos;,
    &apos;output.mp4&apos;
  ]);

  // Create download link
  const data = await ffmpeg.readFile(&apos;output.mp4&apos;);
  const videoBlob = new Blob([data.buffer], { type: &apos;video/mp4&apos; });
  const videoUrl = URL.createObjectURL(videoBlob);

  const downloadLink = document.createElement(&apos;a&apos;);
  downloadLink.href = videoUrl;
  downloadLink.download = &apos;animation.mp4&apos;;
  downloadLink.textContent = &apos;Download Animation&apos;;
  document.body.appendChild(downloadLink);
}


// Start animation when page loads
document.addEventListener(&apos;DOMContentLoaded&apos;, animateAndCapture);  
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><h3 id="result">Result</h3><figure class="kg-card kg-video-card"><div class="kg-video-container"><video src="https://creatingwithdata.com/content/media/2024/10/ball.mp4" poster="https://img.spacergif.org/v1/400x200/0a/spacer.png" width="400" height="200" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://creatingwithdata.com/content/images/2024/10/media-thumbnail-ember628.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container kg-video-hide"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div></figure><h3 id="the-capture-queue">The capture queue</h3><p>The real magic here happens in the <code>captureFrame</code> function. We want to write multiple frames to FFmpeg&apos;s internal file handler, as a sequentially numbered file (e.g., <code>frame_00000.png</code>, <code>frame_00001.png</code>).</p><p>We use <code>captureQueue</code> to manage asynchronous frame capturing. Chaining frames in a promise queue ensures each frame finishes processing before moving to the next. </p><p>This approach resolved an issue where D3&#x2019;s tween function wasn&#x2019;t running synchronously, leading to duplicated or misordered frames.</p><pre><code class="language-Javascript">// Frame capture function
let frameCount = 0;
let captureQueue = Promise.resolve();
async function captureFrame(svgString) {
  captureQueue = captureQueue.then(async () =&gt; {
    const resvgJS = new resvg.Resvg(svgString);
    const pngData = resvgJS.render();
    const pngBuffer = pngData.asPng();

    const filename = `frame_${frameCount.toString().padStart(5, &apos;0&apos;)}.png`;
    console.log(`Capturing ${filename}`)
    await ffmpeg.writeFile(filename, pngBuffer);
    frameCount++;
  });
  return captureQueue;
}
</code></pre><p>Using <code>captureFrame</code> is simple. We just call it whenever we want to add a frame, such as in a d3 tween function.</p><h3 id="frame-rates">Frame rates</h3><p>When it comes to D3, it&apos;s not possible to dictate frame rate. From what I understand, D3 will aim to go as fast as supported by your browser. That&apos;s not always possible if you have complex transitions going on. As a result, the frame rate you tell FFmpeg to use may not match what&apos;s happening in the browser. I imagine you could track your animation&apos;s FPS yourself and pass that to over to FFmpeg at compile time. </p><p>The GIF I rendered of the transatlantic cable visualisation has a lot more going on than the circle animation. It came out at about 6 or 7 FPS. That&apos;s fine for a GIF but not what you&apos;d want from a &quot;real&quot; video. &#xA0;</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2024/10/map.gif" class="kg-image" alt="Saving D3.js animations as video files with WebAssembly" loading="lazy" width="320" height="366"><figcaption>A Gif version of the <a href="https://creatingwithdata.com/dataviz-connecting-the-atlantic/">telegraph cable visualisation</a>&#xA0;</figcaption></figure><p>Since these transitions use an interpolator I was able to render higher FPS videos by dropping D3 tweens in favour of just capturing renders inside a for loop &#x2013; &#xA0;</p><pre><code>// Use this  ...
const framesPerMillisecond = FRAME_RATE / 1000;
const frames = positionDuration * framesPerMillisecond;
for (let i = 0; i &lt; frames; i++) {
  positionTween(i / frames, ip, sphere, land);
}

//Instead of ...
d3.transition().duration(positionDuration).tween(&quot;render&quot;, () =&gt; {
  return t =&gt; {
    positionTween(t, ip, sphere, land);
  }
}).end();</code></pre><p>While my browser wasn&apos;t able to keep up (i.e. we can&apos;t watch the nice animation) it does succesfully produce a smoother video!</p><figure class="kg-card kg-video-card"><div class="kg-video-container"><video src="https://creatingwithdata.com/content/media/2024/10/map.mp4" poster="https://img.spacergif.org/v1/744x874/0a/spacer.png" width="744" height="874" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://creatingwithdata.com/content/images/2024/10/media-thumbnail-ember666.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container kg-video-hide"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div></figure><p> </p>]]></content:encoded></item><item><title><![CDATA[Choropleth mapping house sale prices - Part 2. Visualisation]]></title><description><![CDATA[How to map house prices in an interactive choroplath visualisation using d3.  ]]></description><link>https://creatingwithdata.com/choropleth-mapping-house-sale-prices-part-2/</link><guid isPermaLink="false">663cd0e8cb866f0292417b10</guid><category><![CDATA[code-walkthrough]]></category><category><![CDATA[d3.js]]></category><category><![CDATA[dataviz]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Fri, 09 Aug 2024 14:04:20 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2024/08/cover-scothousing-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2024/08/cover-scothousing-1.png" alt="Choropleth mapping house sale prices - Part 2. Visualisation"><p>In <a href="https://creatingwithdata.com/choropleth-mapping-house-sale-prices-part-1/">Part 1</a>, I focused on readying the data for the visualisation. As far as the actual visualisation starter was concerned we got to the point of setting up the SVG with the boundaries projected. Now, we need to pick up our crayons and do some colouring in! </p><p>Before proceeding, take a quick look at the code for the full visualisation. You can <a href="https://glitch.com/edit/#!/creating-with-data?path=scothousing%2Findex.html">view/remix the final code over on glitch</a>. Also, if you haven&apos;t seen it already, go checkout <a href="https://creating-with-data.glitch.me/scothousing/">the visualisation here</a>. <br><br>In this post I won&apos;t go through each line but rather refer to the key steps that bring the visualisation together.</p><p></p><h3 id="1-import-and-prep-the-price-data">1. Import and prep the price data</h3><p>The starter didn&apos;t use or refer to any of the pricing data we prepared, so we need to import it and ready it for binding to the geographic boundaries. </p><pre><code class="language-javascript">const prices = await d3.csv(&quot;https://creating-with-data.glitch.me/scothousing/intermediate-zones.csv&quot;);
const pricesMap = {};
const combinedPrices = prices.map((d) =&gt; { 
    d[&quot;combinedMean&quot;] = combinedWeighted(+d[&quot;2021: Mean&quot;], +d[&quot;2022: Mean&quot;], 1, 2) * 1.033;
    d[&quot;combinedMedian&quot;] = combinedWeighted(+d[&quot;2021: Median&quot;], +d[&quot;2022: Median&quot;], 1, 2) * 1.033;
    d[&quot;combinedLower&quot;] = combinedWeighted(+d[&quot;2021: Lower Quartile&quot;], +d[&quot;2022: Lower Quartile&quot;], 1, 2) * 1.033;
    d[&quot;combinedSales&quot;]  = (+d[&quot;2021: Number of sales&quot;]) + (+d[&quot;2022: Number of sales&quot;]);
    pricesMap[d[&quot;Feature Identifier&quot;]] = d;        
    return d;
});
</code></pre><p>This code loads the sale data from a CSV file and calculates a weighted average for house prices across 2021 and 2022. The <code>combinedWeighted</code> applies a slight adjustment (<code>* 1.033</code>) to represent inflation to date (based on inflation stats). This data is then stored in a <code>pricesMap</code> object for quick lookup by geographic area.</p><p><strong>1.1 Adding controls</strong></p><p>We have a few different columns of data for the user to choose from, as well as setting their &apos;amount available&apos;. The following controls in the markup are referred to by d3 later on to help choose values from the <code>pricesMap</code> object.</p><pre><code class="language-html">&lt;div class=&quot;controls&quot;&gt;
    &lt;div class=&quot;mb-2&quot;&gt;
        &lt;label&gt;Lower quartile &lt;input type=&quot;radio&quot; name=&quot;column&quot; value=&quot;combinedLower&quot; checked /&gt;&lt;/label&gt;            
        &lt;label&gt;Median &lt;input type=&quot;radio&quot; name=&quot;column&quot; value=&quot;combinedMedian&quot; /&gt;&lt;/label&gt;
        &lt;label&gt;Mean &lt;input type=&quot;radio&quot; name=&quot;column&quot; value=&quot;combinedMean&quot; /&gt;&lt;/label&gt;        
    &lt;/div&gt;

    &lt;label for=&quot;amount-input&quot;&gt;&lt;span&gt;Amount available&lt;/span&gt; &lt;span id=&quot;amount-display&quot;&gt;&lt;/span&gt;&lt;/label&gt;
    &lt;div class=&quot;amount-control&quot;&gt;
        &lt;input value=&quot;100&quot; id=&quot;amount-input&quot; type=&quot;range&quot; step=&quot;1&quot;&gt;
        &lt;datalist id=&quot;amount-list&quot;&gt;&lt;/datalist&gt;
    &lt;/div&gt;
&lt;/div&gt;</code></pre><p>There&apos;s not a lot to this, just plain HTML radio inputs and a slider. We&apos;ll refer to this selection later in the render function.</p><p></p><h3 id="2-colouring-the-boundaries">2. Colouring the boundaries</h3><p>The main magic happens in the <code>render</code> function. It&apos;s a long one, so let me break it down. </p><p><strong>2.0 Reading user selection</strong></p><p>The function starts by checking which radio button the user has selected, and keeping a track of that in <code>selectedColumn</code>.</p><pre><code class="language-javascript">const selectedColumn = d3.select(&quot;input[name=&apos;column&apos;]:checked&quot;).node().value || &quot;combinedLower&quot;;
</code></pre><p><strong>2.1 Calculating the colour scale</strong></p><p>Once the relevant data column is identified, the function extracts the house price values and calculates min and max values. These define the range for the colour scale using D3&apos;s <code>scaleSequential</code> and <code>interpolateCool</code> functions.</p><pre><code class="language-javascript">const values = combinedPrices.map(d =&gt; d[selectedColumn]);
const minMax = d3.extent(values);
const color = d3.scaleSequential(minMax, d3.interpolateCool);</code></pre><p>The resulting &apos;<code>color</code>&apos; here, is a function that when passed a number, will turn that number into a colour value.</p><p><strong>2.2 Rendering the legend and boundaries</strong> </p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/08/image-1.png" class="kg-image" alt="Choropleth mapping house sale prices - Part 2. Visualisation" loading="lazy" width="352" height="62"></figure><p>We first use the <code>color</code> function when generating the legend. This appears as a little gradient at the bottom right of the visualisation. The number of steps are controller by the <code>numIntervals</code> loop: &#xA0; </p><pre><code class="language-javascript">const intervalValues = [];      
for (var i = 0; i &lt; numIntervals; i++) {
    intervalValues.push(i * (minMax[1] / (numIntervals - 1)));
}

legendColours.selectAll(&quot;div&quot;)
    .data(intervalValues)
    .enter().append(&quot;div&quot;)
    .attr(&quot;style&quot;, function(d, i) { 
    return &quot;background: &quot;+ color(d) +&quot;; width: &quot; + lWidth + &quot;px&quot;; 
})

legendNumbers
    .selectAll(&quot;div&quot;)
    .data(intervalValues)
    .enter().append(&quot;span&quot;)
    .text(function(d) { return &quot;&#xA3;&quot; + Math.round(d); });

const areaPaths = areasGroup.selectAll(&quot;path&quot;)
    .data(geoFeatures.features)
    .enter()
    .append(&quot;path&quot;)
    .attr(&quot;data-centroid&quot;, (d)=&gt;{ return JSON.stringify(pathDefiner.centroid(d)) })
    .attr(&quot;d&quot;, pathDefiner)
    .attr(&quot;title&quot;, (d) =&gt; { return d.properties.Name })
    .attr(&quot;id&quot;, (d) =&gt; { return &quot;g-&quot; + d.properties.InterZone; })
</code></pre><p>After rendering the legend, we render each region as a path element. Attaching some <code>data</code>, <code>title</code>, and <code>id</code> attributes acts as a handy reference for further interaction and debugging. &#xA0;</p><p><strong>2.3 Applying colours to the boundaries</strong></p><p>We know what column the user wants to visualise, and we know how to turn the price values into a colour. The <code>updateFill</code> is responsible for using the <code>color</code> function and applying it to each geographic area (<code>path</code> element) on the map.</p><p>Our visualisation involves more than just visualising the chosen price value for each region, there&apos;s also the &quot;amount to spend&quot; in the mix too.</p><pre><code class="language-javascript">const updateFill = () =&gt; {
  const spend = (amountInput.node().value / 100) * maxAmount;
  amountDisplay.text(&quot;&#xA3;&quot; + Math.round(spend));

  areasGroup.selectAll(&quot;path&quot;)
    .attr(&quot;data-value&quot;, (d) =&gt; { return pricesMap[d.properties.InterZone][selectedColumn] })
    .attr(&quot;fill&quot;, function(d){
    const c = color(pricesMap[d.properties.InterZone][selectedColumn]);
    if (spend &gt;= pricesMap[d.properties.InterZone][selectedColumn]) {
      return c;
    } else {
      return d3.color(c).copy({ opacity: 0.1 });
    }
  });
};</code></pre><p>The first thing <code>updateFill</code> function does is read the user&apos;s budget input from the range slider (<code>amountInput</code>). It then calculates the corresponding monetary value (<code>spend</code>) based on this input, and updates &#xA0;<code>amountDisplay</code> label beside the slider.</p><p>The function then iterates over all the map areas (<code>path</code> elements) and updates their fill color based on the calculated spend. The &apos;budget&apos; condition <code>spend &gt;= pricesMap[d.properties.InterZone][selectedColumn]</code> basically determines whether or not opacity is applied to the region&apos;s fill colour.</p><p>This is what creates the &apos;blackening out&apos; effect when you decrease the amount available using the slider.</p><p>Note, D3 has really neat functions for parsing and manipulating colour spaces. In this case, using <code>d3.color</code> to parse the colour value and apply opacity of <code>0.1</code>. </p><h3 id="3-event-handling">3. Event handling</h3><p>Since the user can change a number of things about the visualisation, there&apos;s a bit of event handling going. </p><figure class="kg-card kg-video-card"><div class="kg-video-container"><video src="https://creatingwithdata.com/content/media/2024/08/Screencast-from-09-08-24-14-50-25.webm" poster="https://img.spacergif.org/v1/757x489/0a/spacer.png" width="757" height="489" playsinline preload="metadata" style="background: transparent url(&apos;https://creatingwithdata.com/content/images/2024/08/media-thumbnail-ember2310.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div></figure><pre><code class="language-javascript">amountInput.on(&quot;input&quot;, updateFill);</code></pre><p><code>updateFill</code> is called in two places, i.e. whenever the user interacts with the <code>amountInput</code> slider, and at the end of the <code>render</code> function itself. </p><p><code>render</code> itself gets called once at the end of the script block, so the visualisation shows when the page is first loaded. It is also called when the user selects a different radio button.</p><pre><code class="language-javascript">const radioButtons = d3.selectAll(&quot;input[name=&apos;column&apos;]&quot;);
radioButtons.on(&quot;change&quot;, render);</code></pre><p>As you&apos;ll have noticed, there&apos;s quite a lot going on in <code>render</code>, but it needs to be that way since we have different min and max values, so therefore the colour scale needs to change in line with the selected columns.</p><h3 id="advanced-zoom-interaction">Advanced zoom interaction</h3><p>You may be wondering what&apos;s going on in the zoom handler code as its a bit more extensive than the starter example in <a href="https://creatingwithdata.com/choropleth-mapping-house-sale-prices-part-1/">Part 1</a>. </p><p>When you zoom and pan the map, the visualisation highlights the highest and lowest sale price data on the map with a little circle and label.</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/08/image.png" class="kg-image" alt="Choropleth mapping house sale prices - Part 2. Visualisation" loading="lazy" width="1005" height="494" srcset="https://creatingwithdata.com/content/images/size/w600/2024/08/image.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/08/image.png 1000w, https://creatingwithdata.com/content/images/2024/08/image.png 1005w" sizes="(min-width: 720px) 720px"></figure><p>There&apos;s a lot to it so I&apos;m going to write a dedicated post on how this bit works &#x2013; especially as its one of the more challenging features of a visualisation I&apos;ve worked on!</p>]]></content:encoded></item><item><title><![CDATA[Choropleth mapping house sale prices - Part 1. Data Prep]]></title><description><![CDATA[Part 1 of 2 posts on making a choropleth map of house sale data using d3.js and mapshaper.]]></description><link>https://creatingwithdata.com/choropleth-mapping-house-sale-prices-part-1/</link><guid isPermaLink="false">66326600cb866f02924176c7</guid><category><![CDATA[d3.js]]></category><category><![CDATA[code-walkthrough]]></category><category><![CDATA[javascript]]></category><category><![CDATA[OpenData]]></category><category><![CDATA[svg]]></category><category><![CDATA[tutorial]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Fri, 03 May 2024 14:12:48 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2024/08/sss-57-51.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-purple"><div class="kg-callout-emoji">&#x1F5FA;&#xFE0F;</div><div class="kg-callout-text"><em>If you&apos;re looking <a href="https://creating-with-data.glitch.me/scothousing/">for the dataviz, here it is</a>.</em></div></div><img src="https://creatingwithdata.com/content/images/2024/08/sss-57-51.png" alt="Choropleth mapping house sale prices - Part 1. Data Prep"><p>Looking at the stats, the &apos;<a href="https://creatingwithdata.com/choropleth-map/">Choropleth code walkthrough</a>&apos; has been the most popular post/video by far. On that note, I thought I&apos;d do another Choropleth map. This time I&apos;ve sprinkled in interactivity to demonstrate d3&apos;s &apos;update&apos; behaviour. In this post, I&apos;ll outline the rationale for the visualisation and how to prepare the data for it. &#xA0; &#xA0;</p><h3 id="the-visualisation">The Visualisation</h3><p>Sometime soon I&apos;d like to move house. If you&apos;ve ever been in this position, the market somewhat comes to dominate your thoughts: where can I buy? how much can I sell for? etc. &#xA0;</p><p>All sales (where I live) in Scotland are recorded by the Registers of Scotland (RoS) and that data is published openly on the <a href="https://statistics.gov.scot/">government&apos;s stats portal</a>. </p><p>Those figures are made available for various boundaries including what&apos;s called &apos;2011 Intermediate Data Zones&apos; &#x2013; for a fairly high resolution choropleth map. Here below is the end result: </p><figure class="kg-card kg-video-card kg-card-hascaption"><div class="kg-video-container"><video src="https://creatingwithdata.com/content/media/2024/05/output-na.mp4" poster="https://img.spacergif.org/v1/836x704/0a/spacer.png" width="836" height="704" playsinline preload="metadata" style="background: transparent url(&apos;https://creatingwithdata.com/content/images/2024/05/media-thumbnail-ember333.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div><figcaption>A video of the visualisation. Alternatively, you can <a href="https://creating-with-data.glitch.me/scothousing/">try the real thing here</a>.</figcaption></figure><p>The slider controls allows you to choose an amount available towards a house. Areas above or below the value selected are dimmed or activated depending whether you go over a lower quintile, median, or mean value (also controllable).</p><h3 id="getting-price-data">Getting Price Data</h3><p>The map reflects data collected for 2021 and 2022. This is the most up-to-date <a href="https://statistics.gov.scot/resource?uri=http%3A%2F%2Fstatistics.gov.scot%2Fdata%2Fresidential-properties-sales-and-price">data that was published by the RoS.</a> Other years are available all the way back to 2004 (in this dastaset anyway). </p><p>Although the dataset goes to 2022 there are inflation statistics that can help the prices reflect a more recent value. For this I used the UK House Price Index&apos;s <a href="https://landregistry.data.gov.uk/app/ukhpi/browse?from=2023-01-01&amp;location=http%3A%2F%2Flandregistry.data.gov.uk%2Fid%2Fregion%2Fscotland&amp;to=2024-03-01&amp;lang=en">monthly percentage change for Scotland from Jan 2023 to Feb 2024</a>. </p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/05/image-2.png" width="1000" height="714" loading="lazy" alt="Choropleth mapping house sale prices - Part 1. Data Prep" srcset="https://creatingwithdata.com/content/images/size/w600/2024/05/image-2.png 600w, https://creatingwithdata.com/content/images/2024/05/image-2.png 1000w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/05/image-1.png" width="1236" height="626" loading="lazy" alt="Choropleth mapping house sale prices - Part 1. Data Prep" srcset="https://creatingwithdata.com/content/images/size/w600/2024/05/image-1.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/05/image-1.png 1000w, https://creatingwithdata.com/content/images/2024/05/image-1.png 1236w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/05/image-3.png" width="869" height="643" loading="lazy" alt="Choropleth mapping house sale prices - Part 1. Data Prep" srcset="https://creatingwithdata.com/content/images/size/w600/2024/05/image-3.png 600w, https://creatingwithdata.com/content/images/2024/05/image-3.png 869w" sizes="(min-width: 720px) 720px"></div></div></div></figure><p>The pricing dataset exported from the stats hub was in great condition so I didn&apos;t have any cleaning to do. The only thing was figuring out how to actually export the data I wanted using the &apos;data cart&apos; feature. The thing that threw me off was needing to navigate away from the dataset to the &apos;Atlas&apos; in order to add desired rows to the export. It&apos;s not immediately intuitive so you need to <a href="https://guides.statistics.gov.scot/article/20-find-and-download-collections-of-multiple-areas-at-once-using-the-data-cart">read the guide</a> rather than jumping in as I usually like to do. </p><p>Here&apos;s a video of the process that might be help: </p><figure class="kg-card kg-video-card kg-card-hascaption"><div class="kg-video-container"><video src="https://creatingwithdata.com/content/media/2024/05/data-cart.mp4" poster="https://img.spacergif.org/v1/722x554/0a/spacer.png" width="722" height="554" playsinline preload="metadata" style="background: transparent url(&apos;https://creatingwithdata.com/content/images/2024/05/media-thumbnail-ember395.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div><figcaption>Building a &apos;Data Cart&apos;</figcaption></figure><h3 id="preparing-the-boundary-data">Preparing the Boundary Data</h3><p>The Scottish stats hub has a companion spatial data hub - spatialdata.gov.scot where you can download various open boundary data. In this case, the &apos;<a href="https://spatialdata.gov.scot/geonetwork/srv/eng/catalog.search#/metadata/389787c0-697d-4824-9ca9-9ce8cb79d6f5">Intermediate Zone Boundaries 2011</a>&apos;.</p><p>The boundaries come as ESRI Shapefiles, so they need some conversion before we can use them with the d3-geo functions. For this purpose, <a href="https://github.com/mbloch/mapshaper/">MapShaper is a tremendous tool</a> that does what we need.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">I owe a lot to <a href="https://moriartynaps.org/command-carto-part-one/">this tutorial by Dylan Moriarty</a> that helped me get a start with MapShaper.&#xA0;</div></div><p>At first I used the MapShaper GUI to open the Shapefiles, and before converting to TopoJSON simplify the map boundaries somewhat. </p><figure class="kg-card kg-video-card kg-card-hascaption"><div class="kg-video-container"><video src="https://creatingwithdata.com/content/media/2024/05/output-na-1.mp4" poster="https://img.spacergif.org/v1/1280x1120/0a/spacer.png" width="1280" height="1120" playsinline preload="metadata" style="background: transparent url(&apos;https://creatingwithdata.com/content/images/2024/05/media-thumbnail-ember509.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div><figcaption>Using the &apos;Simplify&apos; feature in MapShaper</figcaption></figure><h3 id="troubleshooting-a-broken-map-export">Troubleshooting a broken map export</h3><p>When I first exported to TopoJSON and attempted to render the map with D3 I saw nothing. Fiddling with the projection parameters I was able to produce a big mess. Inspecting the boundary paths I could see the &apos;d&apos; attribute had &apos;NaN&apos; scattered throughout what should be a string of coordinates. If you know your JS, NaN = &quot;Not a number&quot;. </p><p>The step I left out was to specify the coordinate system. On this topic, I can <a href="https://helpcenter.flourish.studio/hc/en-us/articles/8827970607887-How-to-make-your-coordinates-WGS84-with-mapshaper-org#transform-coordinates">recommend this tutorial by Flourish</a>. The fix was fairly simple. In <a href="https://github.com/mbloch/mapshaper/wiki/Command-Reference#-proj">Mapshaper you can convert CRS (Coordinate Reference System)</a> using <code>-proj</code>.</p><p>Referring back to the boundary metadata show CRS (EPSG:4258), so the command I needed to run was:</p><p><code>-proj from=EPSG:4258 crs=EPSG:4326</code></p><p>NB- Once you do this you&apos;ll see the map looks a little squashed, don&apos;t worry that&apos;s meant to happen!</p><h3 id="rendering-the-topojson-in-d3">Rendering the TopoJSON in D3</h3><p>Naturally our start point is <a href="https://creating-with-data.glitch.me/scothousing/starter.html">just visualisng the map data</a>. I&apos;ve put the code for <a href="https://glitch.com/edit/#!/creating-with-data?path=scothousing%2Fstarter.html%3A2%3A0">this on Glitch</a>. &#xA0; </p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/05/image-5.png" class="kg-image" alt="Choropleth mapping house sale prices - Part 1. Data Prep" loading="lazy" width="690" height="708" srcset="https://creatingwithdata.com/content/images/size/w600/2024/05/image-5.png 600w, https://creatingwithdata.com/content/images/2024/05/image-5.png 690w"></figure><p>The above map is rendered in 22 lines of code, though most of the magic happens in just a few of these. </p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/05/image-9.png" class="kg-image" alt="Choropleth mapping house sale prices - Part 1. Data Prep" loading="lazy" width="1174" height="585" srcset="https://creatingwithdata.com/content/images/size/w600/2024/05/image-9.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/05/image-9.png 1000w, https://creatingwithdata.com/content/images/2024/05/image-9.png 1174w" sizes="(min-width: 720px) 720px"></figure><p><strong>Lines 7-11</strong>: are just about sizing the SVG to fill most of the window minus the size of the header. </p><p><strong>Lines 14-17</strong>: After that, we grab the TopoJSON file we generated using mapshaper. <a href="https://d3js.org/d3-geo">d3-geo</a> uses GeoJSON, but since TopoJSON is a much more compressed format you&apos;ll frequently see people use TopoJSON with <a href="https://www.npmjs.com/package/@types/topojson-client">the <code>topojson-client</code> library</a> to make the final conversion on the client side. </p><p>After we have GeoJSON, called <code>geoFeatures</code> here. We can configure our d3 projection with the GeoJSON data plus details about how we want to render the map - i.e. how we want to convert coordinates into x, y pixels that will make sense for SVG elements. </p><p>For this purpose <a href="https://d3js.org/d3-geo/projection#projection_fitExtent">fitExtent helps configure</a> the projection by setting dimensions and centring the projection on the centre-point of our boundary data rather than the default coordinates (0&#xB0;N 0&#xB0;E ).</p><p><strong>Lines 20-31</strong>: Once we have a projection function defined, we use this create our <code>pathDefiner</code> function. This is the geographic path generator, the function that will take coordinates and turn them into SVG Path parameters. </p><p>The data zone boundaries are collected in <code>feautres</code> as an array. This is what we use for our data binding, so for each GeoJSON feature we append a path element. The <code>pathDefiner</code> is then used to set the &quot;d&quot; attribute which describes the shape of the path.</p><h3 id="zoom-and-pan-behaviour">Zoom and Pan behaviour</h3><p>Beyond just displaying the map, we have some zoom and pan behaviour. &#xA0; &#xA0;</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/05/image-10.png" class="kg-image" alt="Choropleth mapping house sale prices - Part 1. Data Prep" loading="lazy" width="759" height="278" srcset="https://creatingwithdata.com/content/images/size/w600/2024/05/image-10.png 600w, https://creatingwithdata.com/content/images/2024/05/image-10.png 759w" sizes="(min-width: 720px) 720px"></figure><p>D3 helps you define this behaviour with <a href="https://d3js.org/d3-zoom#d3-zoom">d3-zoom</a> module. Basically it helps you work out how to transform SVG elements when you drag or scroll-in / out. That&apos;s what happens here, where I select all path elements in the areasGroup, and updating the &quot;transform&quot; attribute.</p><p>You&apos;ll see how this works if you open the element inspector and move the map around.</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/05/image-11.png" class="kg-image" alt="Choropleth mapping house sale prices - Part 1. Data Prep" loading="lazy" width="1544" height="460" srcset="https://creatingwithdata.com/content/images/size/w600/2024/05/image-11.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/05/image-11.png 1000w, https://creatingwithdata.com/content/images/2024/05/image-11.png 1544w" sizes="(min-width: 720px) 720px"></figure><p>You might be wondering what the <code>skip % 3</code> condition is doing here. This isn&apos;t strictly necessary. It&apos;s a way of debouncing the &apos;zoom&apos; event as I noticed the zoom behaviour was a little slow on mobile. Simplifying the map boundaries would be another way of dealing with this issue.</p><h3 id="looking-ahead">Looking ahead</h3><p>If you&apos;ve inspected some of these path elements &#xA0;you&apos;ll have noticed ids for each of these boundaries. These match up with the boundary ids we have in the sale prices spreadsheet. In part II I&apos;ll cover matching these up so we can shade each boundary using <a href="https://d3js.org/d3-scale-chromatic/sequential"><code>d3.scaleSequential</code> and <code>d3.interpolateCool</code></a>.</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/05/image-12.png" class="kg-image" alt="Choropleth mapping house sale prices - Part 1. Data Prep" loading="lazy" width="726" height="213" srcset="https://creatingwithdata.com/content/images/size/w600/2024/05/image-12.png 600w, https://creatingwithdata.com/content/images/2024/05/image-12.png 726w" sizes="(min-width: 720px) 720px"></figure><p>On that note, you can always <a href="https://glitch.com/edit/#!/creating-with-data?path=scothousing%2Findex.html%3A208%3A4">view or remix the full visualisation&apos;s code on Glitch</a>.</p>]]></content:encoded></item><item><title><![CDATA[Building a Stripe data export app with Bubble]]></title><description><![CDATA[How I built a Stripe customer data export app in Bubble, including various first impressions of using Bubble and Nocode tools.]]></description><link>https://creatingwithdata.com/building-a-stripe-data-export-app-with-bubble/</link><guid isPermaLink="false">660d75ce67b5570294a178ac</guid><category><![CDATA[nocode]]></category><category><![CDATA[APIs]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Thu, 04 Apr 2024 16:42:59 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2024/04/Screenshot-from-2024-04-04-17-31-18.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2024/04/Screenshot-from-2024-04-04-17-31-18.png" alt="Building a Stripe data export app with Bubble"><p></p><p>Over the past few weeks I&apos;ve been dabbling with Nocode tool <a href="https://bubble.io/">Bubble.io</a>. I&apos;ve played with Make, Airtable, and IFTTT before, but nothing as involved as Bubble. As a developer, Nocode holds two attractions, the promise of accelerated development, and second, the opportunity to empower non-technical colleagues.</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-38.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="702" height="327" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-38.png 600w, https://creatingwithdata.com/content/images/2024/04/image-38.png 702w"></figure><h3 id="introducing-timely-exports">Introducing Timely Exports</h3><p>To learn, I decided to set myself a project - an app to periodically email an export of data, &apos;<a href="https://post-emu.bubbleapps.io/version-test">Timely Exports</a>&apos;. Recently, I&apos;ve been working with the Stripe API a bit so I went with Stripe as the platform. The ideal workflow for this app was: user signs up to the app, chooses what resource they&apos;d like to export (e.g. Customers, Checkout Sessions etc) and then choose how frequently they&apos;d like to receive email exports. Done.</p><figure class="kg-card kg-image-card kg-card-hascaption"><a href="https://docs.stripe.com/api/customers/list"><img src="https://creatingwithdata.com/content/images/2024/04/image-39.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="627" height="606" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-39.png 600w, https://creatingwithdata.com/content/images/2024/04/image-39.png 627w"></a><figcaption>An example response from the Stripe Customers API endpoint&#xA0;</figcaption></figure><p>With Bubble, you more or less immediately have an application deployed with user accounts and email integration. The User account including login / sign-up forms are available right from the start. This means that save for UI details/branding, you can jump into implementing business logic straight away. </p><h3 id="v0-implementation-plan">v0 Implementation Plan</h3><p>All this application really needs to know is a little about the Stripe account in question (i.e. a name plus API key), and the details of what to export (resource name and periodicity).</p><p>But before jumping into setting up recurring events or allowing the user to select a resource, I needed to check I could achieve the following:</p><ol><li>Allow the user to provide their (restricted) API key </li><li>Successfully call the Stripe API</li><li>Flatten the JSON response into a tabular CSV string</li><li>Save the string to a file</li><li>Attach said file to an email and send it</li></ol><p></p><h3 id="making-stripe-api-requests-with-bubble">Making Stripe API requests with Bubble &#xA0;</h3><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-19.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="987" height="780" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-19.png 600w, https://creatingwithdata.com/content/images/2024/04/image-19.png 987w" sizes="(min-width: 720px) 720px"></figure><p>There are lots of Stripe plugins for Bubble with API actions ready to go. However, I quickly found that few support all the capabilities of the API. This meant I needed to hand roll an API action to retrieve say, a list of customers. The way this is done in Bubble is you use the API connector plugin and configure the endpoints using a form. </p><p>This seemed simple enough. Fill in the form for each API request using the docs <a href="https://docs.stripe.com/api/customers/list">Stripe API docs</a> for reference. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2024/04/image-21-1.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="624" height="151" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-21-1.png 600w, https://creatingwithdata.com/content/images/2024/04/image-21-1.png 624w"><figcaption>Retrieve a list of customers</figcaption></figure><p>In Bubble, the form is setup so that the authentication method is shared across all endpoints with credentials saved in advance so you don&apos;t provide them when it comes to invoking the API action.</p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/image-22.png" width="946" height="466" loading="lazy" alt="Building a Stripe data export app with Bubble" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-22.png 600w, https://creatingwithdata.com/content/images/2024/04/image-22.png 946w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/image-23.png" width="949" height="596" loading="lazy" alt="Building a Stripe data export app with Bubble" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-23.png 600w, https://creatingwithdata.com/content/images/2024/04/image-23.png 949w" sizes="(min-width: 720px) 720px"></div></div></div></figure><p>Stripe requests are authenticated by using HTTP Basic Auth and using the secret key as the username, and leaving the password blank (no idea why they&apos;ve gone down that route).</p><p></p><h3 id="hitting-the-nocode-wall">Hitting the Nocode wall</h3><p>The above is sensible enough, but not at all what I wanted! </p><p>The token should be provided when we invoke the API action - just like any other parameter (e.g. &apos;limit&apos; in this case). Unfortunately, it doesn&apos;t look like this plugin allows for passing auth details when you invoke API actions. </p><p>However, the API builder plugin allows you to pass in request header information. Among other things, this is what most of the various authentication methods actually do. The HTTP Basic Auth method Base64 encodes the username and password so you end up with an Authorization header that&apos;s something like this: </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2024/04/image-24.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="900" height="84" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-24.png 600w, https://creatingwithdata.com/content/images/2024/04/image-24.png 900w" sizes="(min-width: 720px) 720px"><figcaption>Encode &quot;this_is_your_api_secret:in_stripe_this_blank&quot; on base64encode.org and you&apos;ll see where the above string comes from</figcaption></figure><p>I was almost away to see if there was a way to Base64 encode until I remembered a simpler way to use basic auth. Prepending the user:pass combo with &apos;@&apos; in front of the domain:</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-25.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="789" height="126" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-25.png 600w, https://creatingwithdata.com/content/images/2024/04/image-25.png 789w" sizes="(min-width: 720px) 720px"></figure><p>Using square brackets here means Bubble understands that part as a parameter. Problem solved! &#xA0;</p><p>Using the initialize call button I was able to see that I could succesfully call the Stripe API. It seems Bubble requires you to do this so it can anticipate the contents of the API response.</p><p></p><h3 id="flattening-the-json-response">Flattening the JSON response</h3><p>The next step was translating JSON into a &apos;flattened&apos; tabular format that would make sense as a CSV.</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-26.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="999" height="526" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-26.png 600w, https://creatingwithdata.com/content/images/2024/04/image-26.png 999w" sizes="(min-width: 720px) 720px"></figure><p>I found a plugin for this, but for whatever reason I couldn&apos;t get it to work. I flailed around trying to see if I could cobble together a few Bubble actions to achieve building a CSV string but that turned out to be pretty onerous. Also, it was fixed to the &apos;Customer&apos; API response rather than being abstract. At this point I decided to see if I could inject some code via my own plugin.</p><p>I thought this was going to be a huge hassle, but what&apos;s nice about plugins in bubble is that many plugins are openly readable and you can see exactly how they&apos;re written. </p><p>Using a few examples helped me jump into writing a JSON to CSV string function in JS that I could invoke in an action workflow where the input would be the raw response (as text), parse it, then flatten it into a CSV using the JSON keys as headers. For laziness/speed reasons I decided to ignore nested keys. </p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-27.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="737" height="550" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-27.png 600w, https://creatingwithdata.com/content/images/2024/04/image-27.png 737w" sizes="(min-width: 720px) 720px"></figure><p>Aside from the raw body text, this function can take in a path specifier. This was to provide a bit of flexibility so you can tell the function where the collection is to turn into a CSV. For this purpose I require <a href="https://www.npmjs.com/package/jsonpath">the &apos;jsonpath&apos; package</a> that helps you navigate JSON objects using a path string. </p><p>There&apos;s one other parameter this plugin action takes, and that&apos;s &apos;encode_output&apos;. This is an option to Base64 encode the output which comes into play for file uploads in Bubble. &#xA0; </p><p></p><h3 id="what-happened-to-the-nocode">What happened to the nocode? </h3><p>I know. I know. There probably was a Nocode option out there for achieving this. From what I&apos;ve gathered, full on nocoders would probably turn to looping in outside services like Make and Zapier in order to achieve bits of logic that aren&apos;t available in Bubble. I get the impression that any one nocode tool can only get you 80% of the way before you hit a wall.</p><p>Anyway, I digress.</p><p></p><h3 id="file-uploads">File Uploads </h3><p>The next step was to take the result of this function, a big long string, and upload it as a file. Bubble has a &apos;Upload as CSV&apos; action, but it demands that you specify an object / collection of objects from your db to upload. Once again I needed to figure out something myself. This is where the forums were very helpful. I discovered a thread that explained Bubble&apos;s file upload API. Using this I was able to construct an API action to upload to Bubble&apos;s file storage. &#xA0; &#xA0; </p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-28.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="828" height="591" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-28.png 600w, https://creatingwithdata.com/content/images/2024/04/image-28.png 828w" sizes="(min-width: 720px) 720px"></figure><p>Bubble&apos;s file upload API expects &apos;contents&apos; text to be Base64 encoded so that&apos;s why I added that option in the JSON to CSV action.</p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/image-29.png" width="468" height="291" loading="lazy" alt="Building a Stripe data export app with Bubble"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/image-30.png" width="395" height="417" loading="lazy" alt="Building a Stripe data export app with Bubble"></div></div></div></figure><p>This is what the workflow looks like, chaining together the results of the various steps up to the API call action. </p><p></p><h3 id="sending-email">Sending Email</h3><p>Sending email using Bubble is dead easy! There&apos;s an inbuilt action. The bit that wasn&apos;t immediately clear was how to attach the newly uploaded file. </p><p>It turns out the response body of the upload API request has the URL to the file. This is what you need to provide in order to attach a file in the email action. Simple enough.</p><p>Here&apos;s the workflow and UI ended up with. </p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/image-31.png" width="698" height="433" loading="lazy" alt="Building a Stripe data export app with Bubble" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-31.png 600w, https://creatingwithdata.com/content/images/2024/04/image-31.png 698w"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/image-32.png" width="701" height="525" loading="lazy" alt="Building a Stripe data export app with Bubble" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-32.png 600w, https://creatingwithdata.com/content/images/2024/04/image-32.png 701w"></div></div><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/image-36.png" width="913" height="694" loading="lazy" alt="Building a Stripe data export app with Bubble" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-36.png 600w, https://creatingwithdata.com/content/images/2024/04/image-36.png 913w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/image-34.png" width="941" height="602" loading="lazy" alt="Building a Stripe data export app with Bubble" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-34.png 600w, https://creatingwithdata.com/content/images/2024/04/image-34.png 941w" sizes="(min-width: 720px) 720px"></div></div></div></figure><p>I added a Test button that doesn&apos;t do the upload step, but rather just displays the unencoded CSV text.</p><p>Since I don&apos;t want to end up storing anyone&apos;s data, I added a final workflow step to delete the uploaded file.</p><h3 id="the-end-result">The End Result</h3><p>A spreadsheet, in my email inbox. Much Wow. These are the headings you get out of it. There&apos;d be a few more if I bothered to sort out the nested object keys:</p><!--kg-card-begin: markdown--><pre><code>id  object  address balance created currency  default_currency  default_source  delinquent  description discount  email invoice_prefix  invoice_settings  livemode  metadata  name  phone preferred_locales shipping  tax_exempt  test_clock
</code></pre>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2024/04/image-37.png" class="kg-image" alt="Building a Stripe data export app with Bubble" loading="lazy" width="1093" height="506" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-37.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/04/image-37.png 1000w, https://creatingwithdata.com/content/images/2024/04/image-37.png 1093w" sizes="(min-width: 720px) 720px"><figcaption>An example customer export spreadsheet&#xA0;</figcaption></figure><p>If you&apos;d like to try this app for yourself head over to <a href="https://post-emu.bubbleapps.io/version-test">https://post-emu.bubbleapps.io/version-test</a></p><h3 id="next-steps">Next Steps</h3><p>In order to fulfil the original project vision, I&apos;ll need to provide a few more Stripe resource options for export (e.g. Checkout Sessions, Events etc.), and then persist the user&apos;s &apos;Export Configurations&apos; &#xA0;to the database.</p><p>The workflow itself remains more or less the same, but I belive it becomes what Bubble calls a backend workflow in order to be invoked as a recurring event. I know Bubble has provision for recurring events so I feel like the above isn&apos;t far off. What I don&apos;t know, however, is how to securely store my users&apos; API keys. A cursory look through the plugins does reveal a few encryption options though so it seems doable.</p><h3 id="some-thoughts-on-nocode">Some thoughts on Nocode</h3><p>While playing around with Bubble on this project and a few other &apos;hello worlds&apos; I hit several walls. This, in some senses, isn&apos;t dissimilar to my experiences learning new (code-based) frameworks. Each framework has assumptions built-in for ease and to accelerate development. The flip-side of this is that when they don&apos;t match these backfire and slow you down. </p><p>Like any technology, the trick is to know what you can and cannot easily do in order to be productive. &#xA0;</p>]]></content:encoded></item><item><title><![CDATA[The making of Super Map World - Part 2. Core Architecture]]></title><description><![CDATA[I explain the choices and data architecture behind Super Map World.]]></description><link>https://creatingwithdata.com/the-making-of-super-map-world-part-2/</link><guid isPermaLink="false">65ef2d45819d1a02a5316790</guid><category><![CDATA[ruby-on-rails]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Mon, 01 Apr 2024 15:01:22 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2024/04/image-2-3.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2024/04/image-2-3.png" alt="The making of Super Map World - Part 2. Core Architecture"><p></p><p><em>This is the 2nd post in a series about building <a href="https://supermap.world/">Super Map World</a> (SMW). SMW is a web-application featuring over 17,000 maps and map graphics. They&apos;re free and they&apos;re customisable using a map edit feature. Head <a href="https://creatingwithdata.com/the-making-of-super-map-world-part-1/">over here for the first post</a> of the series. &#xA0;</em></p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-17.png" class="kg-image" alt="The making of Super Map World - Part 2. Core Architecture" loading="lazy" width="1374" height="873" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-17.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/04/image-17.png 1000w, https://creatingwithdata.com/content/images/2024/04/image-17.png 1374w" sizes="(min-width: 720px) 720px"></figure><p>To spend as much time focused on the core of the application I needed a simple template that would cut out the fiddling with project setup. My focus was on figuring out how to best represent and store the generatively designed maps from my prototype. </p><p>I wanted something that would get me started with user accounts, email integration, and all the other bits and pieces that make web-applications work. My two main requirements were, 1. nothing over-engineered, and 2. nothing relying on libraries that I wasn&apos;t somewhat familiar with. </p><h3 id="a-quickstart-with-speedrail">A Quickstart with Speedrail</h3><p>Using Github search for Rails templates I eventually came across Speedrail. Speedrail was made by Ryan Kulp for his &apos;<a href="https://www.founderhacker.com/">Founder/Hacker</a>&apos; coding and product-launch course. It pretty much ticked all the boxes for me in my search for a simple, modern, Rails 7 template featuring:</p><ul><li>Devise for authentication</li><li>Tailwind CSS</li><li>Stripe for payments</li><li>Rspec for testing</li><li>Postmark for email</li><li>Importmap for managing libraries </li></ul><p>Getting started with Speedrail was a matter of cloning the repo, running the rename command <code>bin/speedrail new_app_name</code>, configuring environment variables in <code>config/application.yml</code>, then using it as you would any Rails application.</p><h3 id="core-architecture">Core architecture</h3><p>The most important part of any web-application is its data architecture. The question now was how to store the various elements I had in my prototype, namely, map styles, and the generatively designed maps themselves.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2024/04/image-8.png" class="kg-image" alt="The making of Super Map World - Part 2. Core Architecture" loading="lazy" width="426" height="285"><figcaption>Map of Switzerland showing district boundaries</figcaption></figure><p>I needed something that could capture all the possible variations in a map rendering. Properties of a map can include several things including: </p><ul><li>geographic data i.e. the file it was rendered from, resolution, recommended projection</li><li>source metadata e.g. where data was sourced, licencing details</li><li>projection i.e. is it mercator, orthographic, something else?</li><li>theme i.e. colours, shadows, graticule yes/no, borders yes/no, etc.</li><li>location details e.g. centre point coordinates, name of the location</li><li>visual adjustments e.g. margins and offset</li></ul><p>It took me a while to come up with a schema that could represent all this while being 1. normalised, and efficent in database terms, and 2. easily indexed and searchable. </p><p>I came up with a 4 part system for capturing maps:</p><ul><li>MapStyle &#x2013; represents all the styling information that could be applied to rendering a map.</li><li>MapConfig &#x2013; represents a source of geographic data and its relevent metadata. </li><li>MapFraming &#x2013; represents how the geographic data is framed and projected. This means projection plus any adjustments such as rotation, scaling, or margins.</li><li>MapRendering &#x2013; represents the rendering or a final image generated from all these factors. It has a number, a slug, and ties together these above representations via associations.</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2024/04/image-2.png" class="kg-image" alt="The making of Super Map World - Part 2. Core Architecture" loading="lazy" width="828" height="675" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-2.png 600w, https://creatingwithdata.com/content/images/2024/04/image-2.png 828w" sizes="(min-width: 720px) 720px"><figcaption>ER diagram of how they fit together</figcaption></figure><p>While the above architecture seems clear now, I only thought about having MapStyle, MapConfig, and MapRendering. I couldn&apos;t figure out whether the framing information should sit with MapStyle or with MapRendering. I passed it back and forth quite a bit until I realised I was dealing with something that deserved a life of its own.</p><figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/theImage.png" width="426" height="426" loading="lazy" alt="The making of Super Map World - Part 2. Core Architecture"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/684635.png" width="426" height="426" loading="lazy" alt="The making of Super Map World - Part 2. Core Architecture"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/04/138139.png" width="426" height="426" loading="lazy" alt="The making of Super Map World - Part 2. Core Architecture"></div></div></div></figure><p>Essentially, a MapFraming tells us how to use the geographic data to depict something then MapStyle provides the theme.</p><p>To illustrate the relationships between these different models, let&apos;s consider the above 3 images or &apos;renderings&apos;. They all share the same data, represented by the MapConfig. The two maps on the right share the same MapFraming, they are both orthographic projections, rotated to be centred on India. They don&apos;t share the same MapStyle, unlike the two on the left that do. </p><p>Representing the above in the database there&apos;s 1 MapConfig, 2 MapFramings, 2 MapStyles, and 3 MapRenderings</p><p>In the Super Map World database there are around 450 MapFramings, and roughly 87 different styles. The combination of the two means &#xA0;(450 x 87) we can generate 39150 different renderings.</p><h3 id="locations-and-searching">Locations and searching</h3><p>You may have noticed in the ER diagram that MapFraming has a location_id. Location is associated with MapFraming. Its a model I use to support the search feature. The location table has been populated with countries and their ISO codes. </p><p>That said, the model isn&apos;t restricted to countries. It&apos;s intended to support any location on the globe that could be depicted with a map. Technically, a river or a city or a mountain could be the focul point of a map on SMW. &#xA0; </p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-9.png" class="kg-image" alt="The making of Super Map World - Part 2. Core Architecture" loading="lazy" width="789" height="708" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-9.png 600w, https://creatingwithdata.com/content/images/2024/04/image-9.png 789w" sizes="(min-width: 720px) 720px"></figure><p>Currently, I have &apos;The World&apos; as an entry in the location table because a world Mercator map isn&apos;t really depicting anything other than the world itself. </p><p>This location name information helps generate slug and description of map renderings. If you go to a map rendering&apos;s page on SMW, you&apos;ll notice this used in the description field along with the projection, boundary details (if any), and colour choice. &#xA0;</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-10.png" class="kg-image" alt="The making of Super Map World - Part 2. Core Architecture" loading="lazy" width="536" height="715"></figure><p></p><h3 id="reasonable-colors">Reasonable Colors</h3><p>Reading through the description field you might wonder where the colour names come from. In the previous post, I explained the style generator randomly selected from a list of CSS colour names in order to come up with a theme. If you know your CSS names you&apos;ll notice these names don&apos;t exist in the CSS colour set.</p><p>After my initial prototype, I decided to ditch CSS colour names as they aren&apos;t quite uniform in their distribution. Thankfully, I came across &apos;<a href="https://reasonable.work/colors/">Reasonable Colors</a>&apos; by <a href="https://www.matthewhowell.net/">Matthew Howell</a>. </p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-11.png" class="kg-image" alt="The making of Super Map World - Part 2. Core Architecture" loading="lazy" width="743" height="522" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-11.png 600w, https://creatingwithdata.com/content/images/2024/04/image-11.png 743w" sizes="(min-width: 720px) 720px"></figure><p>What&apos;s neat about this project is its simplicty and approach to accessibility and contrast. &#xA0;</p><blockquote>because this is all built within the LCH color space and <a href="https://www.w3.org/TR/WCAG20/#relativeluminancedef">the relative luminance</a> for each shade is pinned within certain ranges, those contrast rules work across all 24 color sets. Mix and match shades from any color, even the grays. &#xA0;</blockquote><p>Each colour comes with 6 pre-defined shades. For SMW, it provided a comprehensive yet manageable set of colours to choose from. </p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-13.png" class="kg-image" alt="The making of Super Map World - Part 2. Core Architecture" loading="lazy" width="522" height="387"></figure><p>Constraining to this set also meant it wasn&apos;t too onerous to come up with a few extra colour names to make the descriptions a little more interesting. More importantly, it meant I could come up with a search feature that wouldn&apos;t demand the user to put in hex or rgb codes. </p><p>When you click on a colour on the search, or on a map rendering page it&apos;ll use the colour name plus shade index to query map styles and associated map renderings. &#xA0; &#xA0; &#xA0;</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2024/04/image-14.png" class="kg-image" alt="The making of Super Map World - Part 2. Core Architecture" loading="lazy" width="790" height="424" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-14.png 600w, https://creatingwithdata.com/content/images/2024/04/image-14.png 790w" sizes="(min-width: 720px) 720px"><figcaption>Colour search - note the ?colour=amber-2 in the query string</figcaption></figure><h3 id="jsonb-fields">JSONB fields </h3><p>Looking at the ER diagram you might also be wondering where exactly all this theme information is being stored given MapStyle only has 6 fields. Answer: it&apos;s all stuffed in the &apos;body&apos; field. </p><p>That might sound horrible but I quite like it for this case. You see SMW uses PostgreSQL, and PostgreSQL supports what&apos;s called a &apos;JSONB&apos; field where you can store arbitrary JSON objects. In my previous SMW post, I described my approach to generating map styles and the schema I came up with. Not wanting to alter or flatten this into a collection of relational DB tables, I opted to keep it as is, in a JSONB field. &#xA0; &#xA0; </p><p>What&apos;s neat about this is its flexible and we can still use Postgres to query values in there. I do use a custom Rails validator to ensure nothing funny goes on.</p><p>Here&apos;s how I query map renderings for a given colour or colour shade combination in map_rendering.rb: &#xA0;</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2024/04/image-16.png" class="kg-image" alt="The making of Super Map World - Part 2. Core Architecture" loading="lazy" width="1251" height="194" srcset="https://creatingwithdata.com/content/images/size/w600/2024/04/image-16.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/04/image-16.png 1000w, https://creatingwithdata.com/content/images/2024/04/image-16.png 1251w" sizes="(min-width: 720px) 720px"></figure><p>When a map style is generated I include a list of the colours and shades used in the style. This makes for handy reference and indexing. I realise it might be worth extracting this information out into a field of its own for faster indexing. For the moment, it seems to work fine.</p><h3 id="next-up-data-and-rendering">Next up: data and rendering</h3><p>Aside from map styles I haven&apos;t quite explained where I got the geographic data from, and how that goes on a journey to becoming the map renderings you see on SMW. If that post doesn&apos;t get too extensive I&apos;ll cover a bit of the Stimulus and d3 JavaScript that brings things together. &#xA0;</p><p><em>Btw, if you haven&apos;t already, do play around with <a href="https://supermap.world/">SMW</a>! Feedback is appreciated! You can also find <a href="https://www.instagram.com/supermapworld/">SMW on Instagram</a>.</em></p><p><em>I will be extra grateful if you could share with your carto, map, or graphic-design inclined friends. This can contribute to the post on SEO / marketing (even though I don&apos;t have much to teach in that regard).</em></p>]]></content:encoded></item><item><title><![CDATA[The making of Super Map World - Part 1.]]></title><description><![CDATA[<p><a href="https://supermap.world/">Super Map World</a> is a site that offers over 17,000 maps and map graphics. They&apos;re colourful. They&apos;re free! And best of all, they&apos;re customisable using a map edit feature.</p><p>It&apos;s one of the biggest side projects I&apos;ve committed to.</p>]]></description><link>https://creatingwithdata.com/the-making-of-super-map-world-part-1/</link><guid isPermaLink="false">65cea42f819d1a02a53164b8</guid><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Wed, 21 Feb 2024 13:49:55 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2024/02/cover.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2024/02/cover.png" alt="The making of Super Map World - Part 1."><p><a href="https://supermap.world/">Super Map World</a> is a site that offers over 17,000 maps and map graphics. They&apos;re colourful. They&apos;re free! And best of all, they&apos;re customisable using a map edit feature.</p><p>It&apos;s one of the biggest side projects I&apos;ve committed to. Over the course of the next few posts I&apos;m going to write up my approach to, and experience of, building Super Map World (SMW).</p><p>I&apos;ll cover:</p><ul><li>The tech stack (Ruby on Rails, StimulusJS, D3js, Tailwind) </li><li>Gathering geographic data for 200+ countries &#xA0;</li><li>Deployment and configuring a CDN</li><li>Challenges and how they were overcome (or worked-around)</li><li>Lessons on launching SMW onto the web &#xA0;</li><li>Developing an add-on for Adobe Express</li></ul><figure class="kg-card kg-video-card"><div class="kg-video-container"><video src="https://creatingwithdata.com/content/media/2024/02/FullSMWEditorVideo.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" playsinline preload="metadata" style="background: transparent url(&apos;https://creatingwithdata.com/content/images/2024/02/media-thumbnail-ember394.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div></figure><p></p><h3 id="version-0">Version 0 </h3><p>My favourite types of data-viz / creative tech projects for Creating with Data have involved generative design and maps. I began to wonder if there was some scope to combine these elements. </p><p>Taking a generative design approach is a great way to turn out effective and aesthetically pleasing graphics. Why not try it with maps? </p><p>I began a playing around with d3.js, rendering world maps in different styles. &#xA0; </p><figure class="kg-card kg-video-card"><div class="kg-video-container"><video src="https://creatingwithdata.com/content/media/2024/02/smw-version1.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" playsinline preload="metadata" style="background: transparent url(&apos;https://creatingwithdata.com/content/images/2024/02/media-thumbnail-ember272.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div></figure><p>This version randomly selected colours for the land, water, and (optionally) graticule from a list of <a href="https://en.wikipedia.org/wiki/Web_colors">CSS named colours</a>. Graticule, shadow, and stroke widths were randomly decided too. </p><p>This was the job of a <code>generateStyle</code> function, that output a <code>mapStyle</code> object containing the various visual properties to be applied at time of render. Here&apos;s a little snippet from the function:</p><!--kg-card-begin: markdown--><pre><code>{
  &quot;land&quot;: {
    &quot;hasShadow&quot;: false,
    &quot;visible&quot;: true,
    &quot;fillStyle&quot;: randomColours[0],
    &quot;strokeVisible&quot;: (Math.random() &gt;= 0.5),
    &quot;strokeStyle&quot;: randomColours[1],
    &quot;lineWidth&quot;: 2 * Math.random() + 0.05
  },
  &quot;water&quot;: {
    &quot;fillStyle&quot;: randomColours[2],
    &quot;strokeStyle&quot;: randomColours[3],
    &quot;strokeVisible&quot;: (Math.random() &gt;= 0.5),
    &quot;lineWidth&quot;: 2 * Math.random() + 0.95
  },
  &quot;graticule&quot;: {
    &quot;visible&quot;: hasGraticule,
    &quot;strokeStyle&quot;: randomColours[4],
    &quot;lineWidth&quot;: 2*Math.random() + 0.05
  }
 ...
</code></pre>
<!--kg-card-end: markdown--><p>Land, water, graticule, are all elements of any map you might generate. Later on, I added &quot;borders&quot; to the object to control the style of boundaries. Most of the keys here correspond more or less to the names of the functions call when drawing with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fill">Web Canvas API</a>. &#xA0; &#xA0;</p><h3 id="contrast-correction">Contrast correction</h3><p>At first, random selection meant some colours were the same or too close. Landmasses and lines ended up barely visible in the odd rendering. The theme generator needed to correct for contrast so I ended up with a function that reselected until contrast was high enough. &#xA0;</p><!--kg-card-begin: markdown--><pre><code>// Function to randomly select pairs of colors until the contrast value is above min

function findColorsWithContrast(color1, requiredContrast) {
  if (requiredContrast === undefined) { requiredContrast = 1.22; }
  let color2, contrast;
  let i = 0;

  do {
    if (color1 === undefined) { color1 = getRandomColorName(); }
    color2 = getRandomColorName();
    contrast = calculateContrast(color1, color2);
    contrast = Math.round(contrast * 100) / 100;
    //console.debug(`%c&#x25A0;&#x25A0;&#x25A0;&#x25A0; ${i} ${contrast} ${color1} ${color2}`, `color: ${color1}; background: ${color2}; font-size: 20px;`);
    i+=1;
  } while (contrast &lt;= requiredContrast);

  return {
    color1: color1,
    color2: color2,
    contrast: contrast
  };
}
</code></pre>
<!--kg-card-end: markdown--><p>Since colours in CSS can be defined in a number of different ways (e.g. rgb, hsla, &apos;black&apos;, etc.) d3&apos;s <a href="https://d3js.org/d3-color">&apos;d3-color&apos; module</a> was dead handy. It helps you parse those different colour spaces and manipulate them. Digging into colour spaces was an eye opener. </p><h3 id="do-you-know-your-colours">Do you know your colours?</h3><p>This is a bit of a tangent, but one worth mentioning. For most of my career I&apos;ve defaulted to RGB and HEX but this experience helped me understand the benefits of using <a href="https://en.wikipedia.org/wiki/HSL_and_HSV">HSLA</a> colour expressions. It also introduced me to the LCH colour space. <a href="https://lea.verou.me/blog/2020/04/lch-colors-in-css-what-why-and-how/">Lea Verou&apos;s article on the topic is incredible</a> - </p><blockquote>&quot;Today, the gamut (range of possible colors displayed) of most monitors is closer to <a href="https://en.wikipedia.org/wiki/DCI-P3">P3</a>, which has a <a href="https://twitter.com/svgeesus/status/1220029106248716288">50% larger volume than sRGB</a>. CSS right now cannot access these colors at all. Let me repeat: We have no access to one third of the colors in most modern monitors.&quot;</blockquote><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-20-13-18-22.png" class="kg-image" alt="The making of Super Map World - Part 1." loading="lazy" width="789" height="514" srcset="https://creatingwithdata.com/content/images/size/w600/2024/02/Screenshot-from-2024-02-20-13-18-22.png 600w, https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-20-13-18-22.png 789w" sizes="(min-width: 720px) 720px"><figcaption>Verou on LCH. I am still getting my head round how this works!</figcaption></figure><h3 id="the-vision-appears">The vision appears</h3><p>When I set out, the intention was to come up with an interesting example to post here on Creating with Data.</p><p>Rather, what happened was that I was seduced by all the possibilities and combinations that could be realised with the same approach - randomising all the different projections, styling boundaries, highlighting regions, using transparency, using geographic data from different sources, so many possibilities!</p><p>A thassalaphilia kicked in. I wanted to dive in deeper. </p><p>Extending the features of the single page experiment, I began to imagine a web-application that could be used to quickly create attractive maps and map graphics. Something in the spirit of colour palette inspiration tools such as <a href="https://coolors.co/">Coolors</a> or <a href="https://colorhunt.co/">ColorHunt</a>, but also with the utility of a easy infographic and dataviz tools such as <a href="https://www.datawrapper.de/">Datawrapper</a> or <a href="https://www.rawgraphs.io/">RAWGraphs</a>. &#xA0;</p><figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-21-13-14-25.png" width="1044" height="590" loading="lazy" alt="The making of Super Map World - Part 1." srcset="https://creatingwithdata.com/content/images/size/w600/2024/02/Screenshot-from-2024-02-21-13-14-25.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/02/Screenshot-from-2024-02-21-13-14-25.png 1000w, https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-21-13-14-25.png 1044w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-21-13-15-29.png" width="895" height="536" loading="lazy" alt="The making of Super Map World - Part 1." srcset="https://creatingwithdata.com/content/images/size/w600/2024/02/Screenshot-from-2024-02-21-13-15-29.png 600w, https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-21-13-15-29.png 895w" sizes="(min-width: 720px) 720px"></div></div><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-21-13-16-33.png" width="1005" height="466" loading="lazy" alt="The making of Super Map World - Part 1." srcset="https://creatingwithdata.com/content/images/size/w600/2024/02/Screenshot-from-2024-02-21-13-16-33.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/02/Screenshot-from-2024-02-21-13-16-33.png 1000w, https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-21-13-16-33.png 1005w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-21-13-16-44.png" width="1142" height="769" loading="lazy" alt="The making of Super Map World - Part 1." srcset="https://creatingwithdata.com/content/images/size/w600/2024/02/Screenshot-from-2024-02-21-13-16-44.png 600w, https://creatingwithdata.com/content/images/size/w1000/2024/02/Screenshot-from-2024-02-21-13-16-44.png 1000w, https://creatingwithdata.com/content/images/2024/02/Screenshot-from-2024-02-21-13-16-44.png 1142w" sizes="(min-width: 720px) 720px"></div></div></div><figcaption>Clockwise from top left, RAWGraphs, DataWrapper, Coolors, ColorHunt</figcaption></figure><p>The start point was a single html file plus a couple of JS files. The goal was an application that could support users and offer them value added premium features &#x2013; all while supporting open access. </p><h3 id="%F0%9F%8E%B5-i-hear-that-train-a-comin">&#x1F3B5; I hear that train a-comin&apos; </h3><p>As this was becoming a bit more ambitious. I decided to reach for the web-framework that I know and love best, <a href="https://rubyonrails.org/">Ruby on Rails</a>. It&apos;s arguable a JS-based framework would have made more sense considering I was already knee deep in Javascript. That said, I know first hand that employing shiny new tools is a deadly distraction for any new project. </p><p>Getting this up quickly was also a priority. Learning a new framework was not on the critical path. And, as much as I get a rush typing `rails new`, I decided the smarter move was to start with an open source template app.</p><p>In the next post, I&apos;ll go through the first steps in setting up the Rails app that became <a href="https://supermap.world/">Super Map World</a>.</p><p><em>Btw, if you haven&apos;t already, do play around with <a href="https://supermap.world/">SMW</a>! Feedback is appreciated! You can also find <a href="https://www.instagram.com/supermapworld/">SMW on Instagram</a>.</em></p><p><em>I will be extra grateful if you could share with your carto, map, or graphic-design inclined friends. This can contribute to the post on SEO / marketing (even though I don&apos;t have much to teach in that regard).</em></p>]]></content:encoded></item><item><title><![CDATA[Take your first steps with Data Driven Documents (webinar recording)]]></title><description><![CDATA[<p></p><p>Last week I ran a webinar for <a href="https://community.thedatalab.com/">The Data Lab Community</a>. The aim of the webinar was to provide a hands-on introduction to D3, and cover:</p><ul><li>What D3 is, and what it&apos;s not</li><li>Capabilities of the library</li><li>How to use &apos;data-bindings&apos; to power your visualisations</li><li>How</li></ul>]]></description><link>https://creatingwithdata.com/taking-your-first-steps-with-d3-js-webinar-recording/</link><guid isPermaLink="false">651ebae8819d1a02a5316481</guid><category><![CDATA[d3.js]]></category><category><![CDATA[tutorial]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Thu, 05 Oct 2023 13:45:08 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2023/10/Screenshot-from-2023-10-05-14-40-00-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2023/10/Screenshot-from-2023-10-05-14-40-00-1.png" alt="Take your first steps with Data Driven Documents (webinar recording)"><p></p><p>Last week I ran a webinar for <a href="https://community.thedatalab.com/">The Data Lab Community</a>. The aim of the webinar was to provide a hands-on introduction to D3, and cover:</p><ul><li>What D3 is, and what it&apos;s not</li><li>Capabilities of the library</li><li>How to use &apos;data-bindings&apos; to power your visualisations</li><li>How to manipulate SVG elements to represent your data</li><li>Where to go next</li></ul><p>You can find the <a href="https://creating-with-d3.glitch.me/">tutorial materials over here</a> if you want to follow along.</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/l3fQmEAySFI?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="Take Your First Steps with Data Driven Documents (D3.js)"></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Code & Learn: Set your mind in motion with a project vision]]></title><description><![CDATA[Figuring out where to start with a new programming language or framework can be challenging. Here's why a project based approach could work..]]></description><link>https://creatingwithdata.com/code-and-learn-set-your-mind-in-motion-with-a-project-vision/</link><guid isPermaLink="false">64d5523c819d1a02a5315ea6</guid><category><![CDATA[learning]]></category><category><![CDATA[advice]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Mon, 21 Aug 2023 14:22:53 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2023/08/Cajal-Interneuronal-plexuses-RVSD.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2023/08/Cajal-Interneuronal-plexuses-RVSD.jpg" alt="Code &amp; Learn: Set your mind in motion with a project vision"><p>Figuring out where to start with a new programming language or framework can be challenging. Searching for advice can inundate you with long lists of prerequisites and concepts to learn. Forums will turn up tech veterans that demand newcomers &quot;start from the ground up&quot;.</p><p>The newcomers should know the fundamentals before they even begin to touch that framework or library. For them to do so would be to meddle with things beyond their comprehension. Heavens! It could lead to their breaking the fabric of the Internet. Or something.<br><br>I&apos;m sorry to say I opened Pandora&apos;s box and it worked out just fine. <br><br>I didn&apos;t know Ruby before I started learning Ruby on Rails. I learnt the two in tandem. Sometimes it was confusing what was Ruby, and, what was Rails. I had a decent grasp of desktop software development, but not web application development. I didn&apos;t have a clear knowledge of REST architecture. <br><br>Regardless, I ended up proficient in Rails because I took on personal projects, and was keen to see my ideas realised.</p><p></p><h3 id="start-with-a-vision">Start with a vision</h3><p>&quot;Come up with a project idea you&apos;ll enjoy and try to build it&quot; has been a mainstay of advice to anyone I&apos;ve met learning to code over the years. <br><br>There&apos;s a terrific post from Ian Johnson called <a href="https://medium.com/@enjalot/how-do-you-learn-d3-js-ccffc151419b">&apos;How do you learn d3.js?&apos;</a>. In it he asks several well-regarded information and visualisation designers about their respective journies. All of them mention a vision of telling a story with data or creating something that didn&apos;t exist before.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4AC;</div><div class="kg-callout-text"><em>...everything I was learning was driven by &#x201C;I want to do X&#x2026; now how do I do that?&#x201D; - </em><a href="https://medium.com/@enjalot/how-do-you-learn-d3-js-ccffc151419b">Zan Armstrong</a></div></div><p>It&apos;s reassuring to know this approach works for others and not just me.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2023/08/Screenshot-from-2023-08-10-23-21-51-1.png" class="kg-image" alt="Code &amp; Learn: Set your mind in motion with a project vision" loading="lazy" width="1849" height="958" srcset="https://creatingwithdata.com/content/images/size/w600/2023/08/Screenshot-from-2023-08-10-23-21-51-1.png 600w, https://creatingwithdata.com/content/images/size/w1000/2023/08/Screenshot-from-2023-08-10-23-21-51-1.png 1000w, https://creatingwithdata.com/content/images/size/w1600/2023/08/Screenshot-from-2023-08-10-23-21-51-1.png 1600w, https://creatingwithdata.com/content/images/2023/08/Screenshot-from-2023-08-10-23-21-51-1.png 1849w" sizes="(min-width: 720px) 720px"><figcaption>&quot;My vision, one nation, one tribe. One day&apos;ll come the might to move any mountain. Move any mountain.&quot; - The Shamen (<a href="https://www.youtube.com/watch?v=j-TSNcoe8pA">Move any Mountain</a> Lyrics)</figcaption></figure><p></p><h3 id="why-projects">Why projects?</h3><p>Getting excited and remaining engaged is one of the main reasons I advocate taking on a personal project. That motivation will provide the fuel for you to continue.<br><br>Motivation isn&apos;t the only reason. When you try to realise an idea you&apos;ll be required to overcome equally unique challenges. There may be examples and libraries to help, but you&apos;ll need to adapt any prior content to the context of your project. This process of creative synthesis is of a higher order of thinking and problem solving. <br><br>You might find it ambitious to set to work on a passion project on day 1 of learning a new technology. Still, it&apos;s definitely worth thinking about. Contemplating your project vision is a way to set your mind in motion.</p><p>Carrying that vision with you while you&apos;re learning will predispose you to notice important details that crop up in lessons or in your own reading. You can think of this as a net, deployed subsconsciously to gather valuable nuggets of insight. </p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://creatingwithdata.com/content/images/2023/08/3334-2.jpg" class="kg-image" alt="Code &amp; Learn: Set your mind in motion with a project vision" loading="lazy" width="1899" height="1572" srcset="https://creatingwithdata.com/content/images/size/w600/2023/08/3334-2.jpg 600w, https://creatingwithdata.com/content/images/size/w1000/2023/08/3334-2.jpg 1000w, https://creatingwithdata.com/content/images/size/w1600/2023/08/3334-2.jpg 1600w, https://creatingwithdata.com/content/images/2023/08/3334-2.jpg 1899w" sizes="(min-width: 720px) 720px"><figcaption>The net that is cast. This is the <a href="https://history.nih.gov/pages/viewpage.action?pageId=1016727">Cingulate Cortex as drawn by Santiago Ram&#xF3;n y Cajal</a>. This part of the brain is responsible for among many things, learning and memory.</figcaption></figure><h3 id="constraints-can-guide">Constraints can guide</h3><p>Building a project may sound unstructured compared to following a set syllabus or roadmap, however, choosing a project provides constraints. Constraints are your friend because learning any technology can be overwhelming. There are heaps of resources on the web. This should make things easier but unfortunately it can make life hard when you&apos;re unsure where to start.</p><p>If you&apos;re able to break down the component requirements of your project, you&apos;ll have a roadmap to guide you. </p><p>Let&apos;s say your aim is to create your first data visualisation using d3.js. It doesn&apos;t involve any interactivity or fetches of remote data. That&apos;s fine. You can dispense with chapters on event handling and asynchronous programming. You&apos;ve just saved yourself a lot of energy and time!</p><p></p><h3 id="caveats-for-project-based-learning">Caveats for project based learning</h3><p>I believe those are the strongest reasons to value a project-based approach to learning. There are some other benefits too such as building up a portfolio of projects for demonstration and providing a sense of achievement. That said, I&apos;d like to add a few caveats. <br><br><strong>Caveat 1: Don&apos;t bite off more than you can chew</strong><br>This sounds a fairly obvious point to make. Taking on something too complex will lead to frustration. You knew that part. The thing is, experienced developers occasionally underestimate how complex projects or features may be. You will do the same. I recommend choosing projects that you can add to and enhance over time.</p><p><br><strong>Caveat 2: Recognising when to step back </strong><br>At some point working on your project you&apos;ll get stuck. Most of the time (hopefully) you&apos;ll be able to debug or Stackoverflow your way out of it. </p><p>There may come a point where you&apos;re <em>really</em> stuck. Not only that, you&apos;re not sure what to search for, or even how to articulate a request for help. This is a sign you need to learn more about an underlying technology or a fundamental concept. Step away from the project and pick up a book.</p><p>Alternatively, this could also be a sign that you&apos;re not getting all the information you need to diagnose the problem. You may need to learn more about the ways you can make use of your IDE or developer-tools to set breakpoints, read, or output logs.</p>]]></content:encoded></item><item><title><![CDATA[D3.js Spinning Globe Code Walkthrough]]></title><description><![CDATA[<p>In this walkthrough, we look at the code behind this <a href="https://creating-with-data.glitch.me/periwinkle-planet/index.html">spinning globe graphic</a> rendered to web canvas using D3.js.</p><p>Links:</p><ul><li><a href="https://creating-with-data.glitch.me/periwinkle-planet/index.html">Spinning globe graphic</a></li><li><a href="https://glitch.com/edit/#!/creating-with-data?path=periwinkle-planet%2Findex.html%3A1%3A0">Code on Glitch</a></li><li><a href="https://d3js.org/d3-geo">d3-geo documentation</a></li></ul><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/7S8nrDwDv0s?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="D3 Spinning Globe Walkthrough"></iframe></figure>]]></description><link>https://creatingwithdata.com/d3-js-spinning-globe-code-walkthrough/</link><guid isPermaLink="false">64b54b78e5600702dcf47c94</guid><category><![CDATA[code-walkthrough]]></category><category><![CDATA[d3.js]]></category><category><![CDATA[maps]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Thu, 27 Jul 2023 12:08:40 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2023/07/globe-cover-1.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2023/07/globe-cover-1.jpg" alt="D3.js Spinning Globe Code Walkthrough"><p>In this walkthrough, we look at the code behind this <a href="https://creating-with-data.glitch.me/periwinkle-planet/index.html">spinning globe graphic</a> rendered to web canvas using D3.js.</p><p>Links:</p><ul><li><a href="https://creating-with-data.glitch.me/periwinkle-planet/index.html">Spinning globe graphic</a></li><li><a href="https://glitch.com/edit/#!/creating-with-data?path=periwinkle-planet%2Findex.html%3A1%3A0">Code on Glitch</a></li><li><a href="https://d3js.org/d3-geo">d3-geo documentation</a></li></ul><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/7S8nrDwDv0s?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="D3 Spinning Globe Walkthrough"></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Using a text input to filter map data with Leaflet JS]]></title><description><![CDATA[<p>In this follow-up video, we add another method of filtering data on our doctors&apos; practices map visualisation. <br><br>If you&apos;re not too familiar with Leaflet or JavaScript in general, I recommend watching <a href="https://creatingwithdata.com/mapping-and-filtering-data-with-leaflet-js/">the first Leaflet.js video</a> where we create our visualisation from scratch.</p><p>You can find the</p>]]></description><link>https://creatingwithdata.com/leaflet-js-filtering-text/</link><guid isPermaLink="false">63089189ed2f802dc223bfba</guid><category><![CDATA[dataviz]]></category><category><![CDATA[leafletjs]]></category><category><![CDATA[javascript]]></category><category><![CDATA[maps]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Thu, 20 Jul 2023 09:00:00 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2023/07/cover-blog.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2023/07/cover-blog.png" alt="Using a text input to filter map data with Leaflet JS"><p>In this follow-up video, we add another method of filtering data on our doctors&apos; practices map visualisation. <br><br>If you&apos;re not too familiar with Leaflet or JavaScript in general, I recommend watching <a href="https://creatingwithdata.com/mapping-and-filtering-data-with-leaflet-js/">the first Leaflet.js video</a> where we create our visualisation from scratch.</p><p>You can find the <a href="https://creating-with-data.glitch.me/leaflet-filtering/searching.html">complete map visualisation here</a> and <a href="https://glitch.com/edit/#!/creating-with-data/leaflet-filtering/">code for the start and end point on Glitch</a>. </p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/SGRGfzlj4Kc?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="Using a text input to filter and map data with Leaflet"></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Dataviz: Connecting the Atlantic]]></title><description><![CDATA[An animated globe visualisation showing a timeline of trans-Atlantic subsea telegraph connections starting in 1866]]></description><link>https://creatingwithdata.com/dataviz-connecting-the-atlantic/</link><guid isPermaLink="false">64aee5dce5600702dcf47bd6</guid><category><![CDATA[dataviz]]></category><category><![CDATA[d3.js]]></category><category><![CDATA[javascript]]></category><category><![CDATA[maps]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Wed, 12 Jul 2023 18:09:41 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2023/07/Screenshot-from-2023-07-12-18-28-53.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2023/07/Screenshot-from-2023-07-12-18-28-53.png" alt="Dataviz: Connecting the Atlantic"><p><a href="https://www.nytimes.com/1866/07/30/archives/by-ocean-telegraph-european-news-to-july-27-highly-important.html">&#x201C;A treaty of peace has been signed between Austria and Prussia&#x201D;</a> &#x2013; the first message that was transmitted across the Atlantic via telegraph. That&apos;s the first connection that appears in <a href="https://creating-with-data.glitch.me/connecting-atlantic/index.html">this animated globe visualisation</a>. It shows a timeline of various trans-Atlantic subsea telegraph connections starting with the first successfully laid cable in 1866. </p><figure class="kg-card kg-video-card"><div class="kg-video-container"><video src="https://creatingwithdata.com/content/media/2023/07/output.mp4" poster="https://img.spacergif.org/v1/960x540/0a/spacer.png" width="960" height="540" playsinline preload="metadata" style="background: transparent url(&apos;https://creatingwithdata.com/content/images/2023/07/media-thumbnail-ember230.jpg&apos;) 50% 50% / cover no-repeat;"></video><div class="kg-video-overlay"><button class="kg-video-large-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button></div><div class="kg-video-player-container"><div class="kg-video-player"><button class="kg-video-play-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/></svg></button><button class="kg-video-pause-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/><rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/></svg></button><span class="kg-video-current-time">0:00</span><div class="kg-video-time">/<span class="kg-video-duration"></span></div><input type="range" class="kg-video-seek-slider" max="100" value="0"><button class="kg-video-playback-rate">1&#xD7;</button><button class="kg-video-unmute-icon"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/></svg></button><button class="kg-video-mute-icon kg-video-hide"><svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24"><path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/></svg></button><input type="range" class="kg-video-volume-slider" max="100" value="100"></div></div></div></figure><h3 id="background">Background</h3><p>I&apos;ve been looking out for something to combine globes / geospatial data and animation with d3 for a tutorial video. Listening to the audio book of Andrew Carnegie&apos;s autobiography, he describes one of his first job as a messenger boy for a telegraph firm, along with interesting anecdotes about the infrastructure of the time (mid-19th Century). This gave me the idea to look into the history a little more and I discovered some excellent resources. The highlight was <a href="https://atlantic-cable.com/">this site jam-packed with timelines and history on undersea communication infrastructure.</a> You have to love the classic table element web-design.</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2023/07/image.png" class="kg-image" alt="Dataviz: Connecting the Atlantic" loading="lazy" width="1000" height="1093" srcset="https://creatingwithdata.com/content/images/size/w600/2023/07/image.png 600w, https://creatingwithdata.com/content/images/2023/07/image.png 1000w" sizes="(min-width: 720px) 720px"></figure><p>They host some <a href="https://atlantic-cable.com/Maps/index.htm">lovely maps too</a>!</p><h3 id="geocoding-and-converting-to-json-with-chatgpt">Geocoding and converting to JSON with ChatGPT</h3><p>Helpfully the timeline information was already in a table which I saved to CSV format. It didn&apos;t have coordinate data for the routes and I knew I&apos;d rather work in JSON. I thought I&apos;d take a shot at re-structuring the table into JSON and asking for coordinates for the start and end points of the routes. To make things simpler I split up certain routes so they had a single start and end point. I haven&apos;t double checked every coordinate provided by ChatGPT but they&apos;ve been accurate enough for the purposes so far!</p><figure class="kg-card kg-image-card"><img src="https://creatingwithdata.com/content/images/2023/07/image-1.png" class="kg-image" alt="Dataviz: Connecting the Atlantic" loading="lazy" width="631" height="531" srcset="https://creatingwithdata.com/content/images/size/w600/2023/07/image-1.png 600w, https://creatingwithdata.com/content/images/2023/07/image-1.png 631w"></figure><p> &#xA0; &#xA0;</p>]]></content:encoded></item><item><title><![CDATA[Mapping and filtering prescriptions data with LeafletJS (in-depth)]]></title><description><![CDATA[<p>In this video, we visualise data from a spreadsheet of doctors&apos; practices and prescriptions adding two methods for filtering. We deal with the issue of combining the two separate filters as one has a knock-on effect on the other. <br><br>NB- the <strong>data is synthetic</strong>, it&apos;s just modelled</p>]]></description><link>https://creatingwithdata.com/mapping-and-filtering-data-with-leaflet-js/</link><guid isPermaLink="false">62f6aa76ed2f802dc223bf44</guid><category><![CDATA[leafletjs]]></category><category><![CDATA[OpenData]]></category><category><![CDATA[javascript]]></category><category><![CDATA[tutorial]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Tue, 11 Jul 2023 10:54:00 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2023/07/blog-cover.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2023/07/blog-cover.png" alt="Mapping and filtering prescriptions data with LeafletJS (in-depth)"><p>In this video, we visualise data from a spreadsheet of doctors&apos; practices and prescriptions adding two methods for filtering. We deal with the issue of combining the two separate filters as one has a knock-on effect on the other. <br><br>NB- the <strong>data is synthetic</strong>, it&apos;s just modelled on the real thing! <br><br>This video is on the long side. If you&apos;re more interested in the visualisation than the control/filtering stuff you can just watch the first 20-25 mins!</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/xWIRCDJE80E?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="Mapping and filtering data with Leaflet"></iframe></figure><p><strong>Notes</strong></p><p>You can find the <a href="https://creating-with-data.glitch.me/leaflet-filtering/start.html">starter template</a> and <a href="https://creating-with-data.glitch.me/leaflet-filtering/complete.html">the finished article on Glitch</a>. &#xA0;<br><br>Edit: Make sure to <a href="https://creatingwithdata.com/leaflet-js-filtering-text/">check out the follow-up video</a>!</p>]]></content:encoded></item><item><title><![CDATA[D3.js Choropleth Map Code Walkthrough]]></title><description><![CDATA[<p>Maps are some of the most shared and sought after forms of visualisation. In this &#xA0;video, I walk through <a href="https://creating-with-data.glitch.me/choropleth.html">an example</a> that visualises unemployment data at the US county level. The code was updated from this <a href="https://bl.ocks.org/mbostock/4060606">bl.ocks.org v4 example</a> to use d3.js version 7. &#xA0;The</p>]]></description><link>https://creatingwithdata.com/choropleth-map/</link><guid isPermaLink="false">62e704d6ed2f802dc223beba</guid><category><![CDATA[d3.js]]></category><category><![CDATA[maps]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Mon, 03 Jul 2023 09:14:00 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2023/07/vlcsnap-2023-07-03-16h58m50s308.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2023/07/vlcsnap-2023-07-03-16h58m50s308.png" alt="D3.js Choropleth Map Code Walkthrough"><p>Maps are some of the most shared and sought after forms of visualisation. In this &#xA0;video, I walk through <a href="https://creating-with-data.glitch.me/choropleth.html">an example</a> that visualises unemployment data at the US county level. The code was updated from this <a href="https://bl.ocks.org/mbostock/4060606">bl.ocks.org v4 example</a> to use d3.js version 7. &#xA0;The walkthrough touches on using <a href="https://bl.ocks.org/mbostock/4060606">d3-fetch</a> to retrieve data on the web, as well as some basics of rendering geographic data.</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/FsDyelH58F0?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="D3.js Choropleth Map Code Walkthrough"></iframe></figure><p>Notes:</p><ul><li><a href="https://glitch.com/edit/#!/creating-with-data?path=choropleth.html%3A1%3A0">Choropleth map code</a> on Glitch</li><li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map">&apos;Map&apos; object documentation</a> on MDN </li><li><a href="https://github.com/topojson/topojson">Topojson repo</a> on GitHub</li><li><a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Introducing">&apos;Introducing asynchronous JavaScript&apos;</a> article on MDN </li></ul>]]></content:encoded></item><item><title><![CDATA[Creating data-driven fabric designs with JavaScript - P2. coding the generative designer]]></title><description><![CDATA[<p>This screencast shows how to code a generate fabric patterns and visualise them as shirts using SVG and D3js. The patterns are composed of images &#xA0;gathered from an archive of botanical watercolours held by the <a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbm5nMktyNjZIMWlxenZZMDYySXJFZDh4d2VDUXxBQ3Jtc0treUdUTVdZSUZtcURCcF8zU2pxTy04S2FsU1ZJdkktQzlMaGprU1RHd0M2cnRWd29QWDFFOEEzU0lKbVAxbmRwTS1FVElITVlfVHo1bUpJTWJLZ1d3ek94RE5EaWNJdmVmdUJYYjRqdDljaW9WRTc4NA&amp;q=https%3A%2F%2Fnaldc.nal.usda.gov%2Fusda_pomological_watercolor%3Fper_page%3D100&amp;v=_3ItNiEzNFw">US Department of Agriculture</a>.<br><br>For some context about the project I&apos;ve <a href="https://creatingwithdata.com/fruit-of-the-algorthim-a-generative-fabric-designer/">written</a></p>]]></description><link>https://creatingwithdata.com/creating-data-driven-fabric-designs-with-javascript-part-2/</link><guid isPermaLink="false">649063177a18d2029593c616</guid><category><![CDATA[d3.js]]></category><category><![CDATA[screencast]]></category><category><![CDATA[svg]]></category><dc:creator><![CDATA[Rory Gianni]]></dc:creator><pubDate>Mon, 19 Jun 2023 14:43:15 GMT</pubDate><media:content url="https://creatingwithdata.com/content/images/2023/06/blog-cover.png" medium="image"/><content:encoded><![CDATA[<img src="https://creatingwithdata.com/content/images/2023/06/blog-cover.png" alt="Creating data-driven fabric designs with JavaScript - P2. coding the generative designer"><p>This screencast shows how to code a generate fabric patterns and visualise them as shirts using SVG and D3js. The patterns are composed of images &#xA0;gathered from an archive of botanical watercolours held by the <a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbm5nMktyNjZIMWlxenZZMDYySXJFZDh4d2VDUXxBQ3Jtc0treUdUTVdZSUZtcURCcF8zU2pxTy04S2FsU1ZJdkktQzlMaGprU1RHd0M2cnRWd29QWDFFOEEzU0lKbVAxbmRwTS1FVElITVlfVHo1bUpJTWJLZ1d3ek94RE5EaWNJdmVmdUJYYjRqdDljaW9WRTc4NA&amp;q=https%3A%2F%2Fnaldc.nal.usda.gov%2Fusda_pomological_watercolor%3Fper_page%3D100&amp;v=_3ItNiEzNFw">US Department of Agriculture</a>.<br><br>For some context about the project I&apos;ve <a href="https://creatingwithdata.com/fruit-of-the-algorthim-a-generative-fabric-designer/">written a post</a> about how I used an early version of the designer to get the fabric printed and then tailored. <br><br>As the title indicates, this is part 2, if you&apos;d like to know more about how the images were gathered and prepared from the watercolour archive with NodeJS, &#xA0;<a href="https://creatingwithdata.com/creating-data-driven-fabric-designs-with-javascript-p1-scraping-and-data-preparation-with-nodejs/">check-out part 1</a>.</p><p>Here&apos;s the screencast, and here&apos;s a link to the <a href="https://glitch.com/edit/#!/creating-with-data?path=shirt-designer%2Fstart.html%3A5%3A62">starter code on Glitch</a>.</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/_3ItNiEzNFw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="Creating data-driven fabric designs // Part 2: Generative shirt design with d3js"></iframe></figure>]]></content:encoded></item></channel></rss>