<?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[Derek Swingley]]></title><description><![CDATA[Notes to future me.]]></description><link>https://derekswingley.com/</link><generator>Ghost 0.10</generator><lastBuildDate>Wed, 23 Jul 2025 00:53:19 GMT</lastBuildDate><atom:link href="https://derekswingley.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Re-visiting splitting a large .csv]]></title><description><![CDATA[<p>Follow-up on a years-old post titled <a href="https://derekswingley.com/2016/10/02/options-for-splitting-a-large-csv-into-smaller-files/">Options for splitting a large .csv into smaller files</a>:  <code>awk</code>, technically <code>gawk</code>, is the best way to do this. </p>

<p>I wanted tinker with a 4.5 GB .csv file. It has data from 2012 - 2020 so I wanted to split it by year.</p>]]></description><link>https://derekswingley.com/2020/05/15/re-visiting-splitting-a-large-csv/</link><guid isPermaLink="false">16d7b643-712b-4be5-82bd-d65977b7a603</guid><category><![CDATA[csv]]></category><category><![CDATA[awk]]></category><category><![CDATA[gawk]]></category><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Fri, 15 May 2020 22:29:35 GMT</pubDate><content:encoded><![CDATA[<p>Follow-up on a years-old post titled <a href="https://derekswingley.com/2016/10/02/options-for-splitting-a-large-csv-into-smaller-files/">Options for splitting a large .csv into smaller files</a>:  <code>awk</code>, technically <code>gawk</code>, is the best way to do this. </p>

<p>I wanted tinker with a 4.5 GB .csv file. It has data from 2012 - 2020 so I wanted to split it by year. There isn't a year column but each row has a date column in the format <code>mm/dd/yyyy</code>. It turns out <code>gawk</code> does make this pretty easy, even if the syntax to do it is funky:</p>

<pre><code class="language-awk">gawk -F ',' 'NR==1{ h=$0 }NR&gt;1{ print (!a[substr($2,7,4)]++? h ORS $0 : $0) &gt; substr($2,7,4)".csv" }' big.csv  
</code></pre>

<p>This mostly came from a <a href="https://unix.stackexchange.com/a/420945/192380">Unix &amp; Linux StackExchange answer</a> and the description there does a good job of explaining what's going on. In addition, my small changes:</p>

<ul>
<li>specify the delimiter with <code>-F</code> (gawk defaults to whitespace)</li>
<li>use <code>substr()</code> to pull out the year from the second column (<code>$2</code>) </li>
<li>add column headers when starting a new file</li>
</ul>

<p>Losing column headers was why I'd avoided awk in the past. By keeping them in a variable, <code>h</code>, and using that variable to write headers when encountering a new year, <code>!a[substr($2,7,4)]</code>, this does exactly what you want when splitting one .csv file into many.</p>

<p>It's simple to get gawk from homebrew, so the bar for using this tool is pretty low. This command gets through that 4.5 GB .csv file in a couple minutes.</p>]]></content:encoded></item><item><title><![CDATA[Put statements in positive form]]></title><description><![CDATA[<p>How often have you seen code where the absence of something is negated? Sounds confusing, because it is. </p>

<p>Here's an example, assuming a config object with some properties, the value for <code>disableSomething</code> is a boolean: <br>
<code>!config.disableSomething</code></p>

<p>That result of that line will be true if <code>disableSomething</code> is false. Used</p>]]></description><link>https://derekswingley.com/2020/05/10/put-statements-in-positive-form/</link><guid isPermaLink="false">b1c77e57-a030-4ee9-97d7-ccd9bd4d71e9</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Sun, 10 May 2020 19:53:29 GMT</pubDate><content:encoded><![CDATA[<p>How often have you seen code where the absence of something is negated? Sounds confusing, because it is. </p>

<p>Here's an example, assuming a config object with some properties, the value for <code>disableSomething</code> is a boolean: <br>
<code>!config.disableSomething</code></p>

<p>That result of that line will be true if <code>disableSomething</code> is false. Used as a condition, we would be doing something if this config property is not disabled. Wouldn't it be better to say <code>enable</code>? It seems like a minor point, but this type of double negative increases complexity and likelihood of a misunderstanding when a positive equivalent will do. A positive form is simpler to process (for a programmer, not necessarily for a computer):  use <code>enableSomething</code> instead of <code>disableSomething</code>.</p>

<p>Disable/enable are a common pair, but nearly as common, at least when dong frontend work, is show/hide. Much as I prefer to say something is enabled, I much prefer to say show instead of hide.</p>

<p>A section from Strunk &amp; White's <a href="https://www.gutenberg.org/files/37134/37134-h/37134-h.htm"><em>The Elements of Style</em></a> is titled <a href="https://www.gutenberg.org/files/37134/37134-h/37134-h.htm#Rule_11">"Put Statements in a Positive Form"</a>. That perfectly summarizes the point I'm making. <em>The Elements of Style</em> applies to programming too!</p>

<p>Much like writing in general, the rule of thumb should be to opt for the positive form rather than the negative. Saying something is "enabled" is positive, it conveys the existence of something. Saying something is "disabled" is negative. We are talking about the absence of something. Please avoid the negative form and the double negatives.</p>]]></content:encoded></item><item><title><![CDATA[Being right all the time]]></title><description><![CDATA[<p>A week or so ago I watched <a href="https://www.youtube.com/watch?v=R16AYZVejj8">Rhea Butcher's</a> talk from this year's <a href="https://xoxofest.com/">XOXO</a>. The whole thing is worth your time but the piece that prompted me to post here <a href="https://www.youtube.com/watch?v=R16AYZVejj8&amp;t=30m26s">starts just past 30 minutes</a>.</p>

<p>That is a perspective to aspire to. Something along these lines, by which I mean</p>]]></description><link>https://derekswingley.com/2019/11/07/being-right-all-the-time/</link><guid isPermaLink="false">17653b43-f94d-49c5-b462-5e12929dedb7</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Thu, 07 Nov 2019 22:41:00 GMT</pubDate><content:encoded><![CDATA[<p>A week or so ago I watched <a href="https://www.youtube.com/watch?v=R16AYZVejj8">Rhea Butcher's</a> talk from this year's <a href="https://xoxofest.com/">XOXO</a>. The whole thing is worth your time but the piece that prompted me to post here <a href="https://www.youtube.com/watch?v=R16AYZVejj8&amp;t=30m26s">starts just past 30 minutes</a>.</p>

<p>That is a perspective to aspire to. Something along these lines, by which I mean being humble and approaching differing views with openness, has been rattling around in my head for a while now. I've had "in an argument, being right and being wrong feels exactly the same" in a note on my phone for a couple years. This is a better articulation of that.</p>]]></content:encoded></item><item><title><![CDATA[How many sold in the last how many hours?]]></title><description><![CDATA[<p>Before today, I'd never heard of pawramp.com. I ended up there because I was considering buying one of their products as gift. When I ended up on a product page, I couldn't help but notice the blinking 🔥and the impressive stat that "65 sold in last 11 hours". </p>

<p>Hmmm.</p>]]></description><link>https://derekswingley.com/2019/10/31/how-many-sold-in-the-last-how-many-hours/</link><guid isPermaLink="false">32ec610c-7326-4ade-bd1a-ed4b86666700</guid><category><![CDATA[dark patterns]]></category><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Fri, 01 Nov 2019 00:03:15 GMT</pubDate><content:encoded><![CDATA[<p>Before today, I'd never heard of pawramp.com. I ended up there because I was considering buying one of their products as gift. When I ended up on a product page, I couldn't help but notice the blinking 🔥and the impressive stat that "65 sold in last 11 hours". </p>

<p>Hmmm. Why would they say last 11 hours. It was roughly 11am, I think, when I visited this site so maybe it's some kind of count since midnight? That's a stretch but plausible. <em>Or</em> this is one of those BS counters that's a pretty well worn dark pattern (booking.com is the main offender I'm aware of) to nudge people to buy something. I hit refresh a couple of times and got completely different numbers each time. <a href="https://pawramp.com/products/buy-now-paw-ramp">Try it yourself</a>. </p>

<p>So these are 💩numbers, made up to try to conjure some kind of peer pressure effect and convince people that yep, they need a ramp for paws.</p>

<p>Looking closer, the number sold in the last whatever hours do have their own placeholders in the page: <br>
<img src="https://derekswingley.com/content/images/2019/11/pawramp-dom.gif" alt=""></p>

<p>I started looking at scripts loaded via dev tools. <code>pawramp.js</code> seemed like an obvious candidate so I took a look at that. </p>

<p>The first function in that <a href="https://cdn.shopify.com/s/files/1/0079/1874/7711/t/10/assets/pawramp.js?6305">file</a> is <code>flashSoldBar</code>, and, it turns out, that's what's populating the sold count and number of hours. Here's the function:</p>

<pre><code class="language-js">function flashSoldBar(prefix) {  
  //if (!nathan_settings.flash_sold) return;
  var minQty = nathan_settings.flash_sold_min;
  var maxQty = nathan_settings.flash_sold_max;
  var minTime = nathan_settings.flash_min_time;
  var maxTime = nathan_settings.flash_max_time;
  minQty = Math.ceil(minQty);
  maxQty = Math.floor(maxQty);
  minTime = Math.ceil(minTime);
  maxTime = Math.floor(maxTime);

  var qty = Math.floor(Math.random() * (maxQty - minQty + 1)) + minQty;
  qty = parseInt(qty);
  if (qty &lt;= minQty) {
    qty = minQty;
  }
  if (qty &gt; maxQty) {
    qty = maxQty;
  }
  jQuery(".nt_flash_total_day" + prefix).html(qty);

  var time = Math.floor(Math.random() * (maxTime - minTime + 1)) + minTime;
  time = parseInt(time);
  if (time &lt;= minTime) {
    time = minTime;
  }
  if (time &gt; maxTime) {
    time = maxTime;
  }
  jQuery(".nt_flash_in_hour" + prefix).html(time);
}
</code></pre>

<p>and the settings object:  </p>

<pre><code class="language-js">{
  "flash_sold": true,
  "flash_sold_min": 13,
  "flash_sold_max": 81,
  "flash_min_time": 6,
  "flash_max_time": 24
}
</code></pre>

<p>From that we can see that the page is going to say anywhere from 13 - 81 items were sold in the last 6 - 24 hours. The fun thing is that the function is defined globally so we can call it from the dev tools console. If we call it from a timer, it'll repeatedly update the counts, take a look:</p>

<p><img src="https://derekswingley.com/content/images/2019/11/pawramp-interval.gif" alt=""></p>

<p>I like supporting small companies like this but I wish they'd quit with this bullshit. I was pretty sure I was going to buy their product without knowing how many people also supposedly bought it in the last 15 hours. How many of those 160 reviews are fake too? </p>

<p>What's that? Oh, yes, I did buy a pawramp. </p>]]></content:encoded></item><item><title><![CDATA[Get total vertex count for a shapefile]]></title><description><![CDATA[<p>I needed a quick way to get the total vertex count in a shapefile. The <code>ogrinfo</code> command includes vertex count in its output. This command:  </p>

<pre><code class="language-sh">$ ogrinfo -dialect SQLite -sql "SELECT sum(ST_NPoints(geometry)) AS vertex_count FROM OK_second_congressional_district" OK_second_congressional_district.shp
</code></pre>

<p>yields:  </p>

<pre><code class="language-sh">INFO: Open</code></pre>]]></description><link>https://derekswingley.com/2019/10/22/get-total-vertex-count-for-a-shapefile/</link><guid isPermaLink="false">95891833-a9d1-46c0-b262-1201e261990f</guid><category><![CDATA[ogr2ogr]]></category><category><![CDATA[shell]]></category><category><![CDATA[ogrinfo]]></category><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Tue, 22 Oct 2019 21:16:39 GMT</pubDate><content:encoded><![CDATA[<p>I needed a quick way to get the total vertex count in a shapefile. The <code>ogrinfo</code> command includes vertex count in its output. This command:  </p>

<pre><code class="language-sh">$ ogrinfo -dialect SQLite -sql "SELECT sum(ST_NPoints(geometry)) AS vertex_count FROM OK_second_congressional_district" OK_second_congressional_district.shp
</code></pre>

<p>yields:  </p>

<pre><code class="language-sh">INFO: Open of `OK_second_congressional_district.shp'  
      using driver `ESRI Shapefile' successful.

Layer name: SELECT  
Geometry: None  
Feature Count: 1  
Layer SRS WKT:  
(unknown)
vertex_count: Integer (0.0)  
OGRFeature(SELECT):0  
  vertex_count (Integer) = 203
</code></pre>

<p>But that is a lot to type (and remember).</p>

<p>Since the only thing that changes between running commands is the name of a shapefile, it's easy to call this from a shell script:</p>

<pre><code class="language-sh">#!/usr/bin/env bash
printf "\nLayer: $1\n"  
ogrinfo -dialect SQLite -sql "SELECT sum(ST_NPoints(geometry)) AS vertex_count FROM $1" $1.shp | grep "vertex_count (Integer) ="  
printf "\n"  
</code></pre>

<p>Using <code>$1</code> says take the second argument (in our case, the name of a layer / shapefile) and use that in the <code>ogrinfo</code> command. </p>

<p>To use this script, save it as file as somewhere (I called it vc), make it executable, copy it to a directory in <code>$PATH</code>, and now it's much easier to get that count I wanted:</p>

<pre><code class="language-sh">$ vc OK_second_congressional_district

Layer: OK_second_congressional_district  
  vertex_count (Integer) = 203
</code></pre>]]></content:encoded></item><item><title><![CDATA[Using the Census API to get county FIPS codes]]></title><description><![CDATA[<p>I came across a <a href="https://www.propublica.org/datastore/dataset/home-price-impact-of-tax-cuts-and-jobs-act-of-2017">.csv file</a> recently that had some data I wanted to see on a map but the .csv only had county names (formatted as 'some county (state)'). To get it on a map, I'd need to get <a href="https://en.wikipedia.org/wiki/FIPS_county_code">FIPS</a> codes for each county alongside this data.</p>

<p>It'd</p>]]></description><link>https://derekswingley.com/2019/10/13/using-the-census-api-to-get-county-fips-codes/</link><guid isPermaLink="false">32ac22cb-b91a-4990-9aca-f4f057ef1789</guid><category><![CDATA[node]]></category><category><![CDATA[javascript]]></category><category><![CDATA[census]]></category><category><![CDATA[FIPS]]></category><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Mon, 14 Oct 2019 04:16:56 GMT</pubDate><content:encoded><![CDATA[<p>I came across a <a href="https://www.propublica.org/datastore/dataset/home-price-impact-of-tax-cuts-and-jobs-act-of-2017">.csv file</a> recently that had some data I wanted to see on a map but the .csv only had county names (formatted as 'some county (state)'). To get it on a map, I'd need to get <a href="https://en.wikipedia.org/wiki/FIPS_county_code">FIPS</a> codes for each county alongside this data.</p>

<p>It'd been a while since I'd done something like this. I grabbed some shapefiles from the <a href="https://www2.census.gov/geo/tiger/TIGER2019/">Census</a> but got about half-way through before I decided I wanted to see if there was a Census API I could hit for FIPS codes. Luckily, it wasn't too hard to find what I needed. Here's the URL to get FIPS codes for every county in the US:  </p>

<pre><code>https://api.census.gov/data/2010/dec/sf1?get=NAME&amp;for=county:*  
</code></pre>

<p>Note that there are no state names in that response, but there are FIPS codes for states. There's probably a way to get state name in the response, but I found it easier to go get state names and FIPS codes for each state then combine that with the counties info. Here's the URL to get info about all US states:  </p>

<pre><code>https://api.census.gov/data/2010/dec/sf1?get=NAME&amp;for=state:*
</code></pre>

<p>With both of those responses saved to disk via a <a href="https://gist.github.com/swingley/968f2d42cff45e087175908a86582192">script</a> I built a <a href="https://gist.github.com/swingley/304d35d40a2c14f579c9625de3c2131c"><code>county (state)</code> to FIPS object</a> to get a FIPS code next to the data in question so I could put it on a map.</p>

<p>Once I had a lookup, I wrote a <a href="https://gist.github.com/swingley/1ee06f28069bb4eec5343f1f37c84c6c">node script to transform my .csv to a new .csv that includes FIPS code for each row</a>. I mapped that file over on <a href="https://www.datawrapper.de/_/s2A24/">Datawrapper</a>.</p>]]></content:encoded></item><item><title><![CDATA[Data visualization 30ish years ago]]></title><description><![CDATA[<p>First-hand account and enjoyable read about how data visualization was done a few decades ago:  <a href="https://medium.economist.com/data-visualisation-from-1987-to-today-65d0609c6017">Data visualisation, from 1987 to today by Graham Douglas</a>.</p>

<p>I knew I'd enjoy this when I read:  </p>

<blockquote>
  <p>Back in the 1980s we weren’t called data visualisers.</p>
</blockquote>]]></description><link>https://derekswingley.com/2018/09/06/data-visualization-30ish-years-ago/</link><guid isPermaLink="false">7fad3307-7242-47f3-aaa1-872ed5c14db8</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Thu, 06 Sep 2018 17:03:14 GMT</pubDate><content:encoded><![CDATA[<p>First-hand account and enjoyable read about how data visualization was done a few decades ago:  <a href="https://medium.economist.com/data-visualisation-from-1987-to-today-65d0609c6017">Data visualisation, from 1987 to today by Graham Douglas</a>.</p>

<p>I knew I'd enjoy this when I read:  </p>

<blockquote>
  <p>Back in the 1980s we weren’t called data visualisers.</p>
</blockquote>]]></content:encoded></item><item><title><![CDATA[Better JavaScript string sorting with collators]]></title><description><![CDATA[<p>Pop quiz:  how do you sort an array of strings so that any numbers (stored as strings), are sorted in the correct order? That is, if you have:</p>

<p><code>const vals = ['1743', '596', '94', '337']</code></p>

<p>After sorting, the expected result is:</p>

<p><code>['94', '337', '596', '1743']</code></p>

<p>JavaScript's default array sorting method doesn't</p>]]></description><link>https://derekswingley.com/2018/05/08/better-javascript-string-sorting-with-collators/</link><guid isPermaLink="false">ec957bd8-7cc3-439b-9250-c5f52674a0c7</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Tue, 08 May 2018 15:30:37 GMT</pubDate><content:encoded><![CDATA[<p>Pop quiz:  how do you sort an array of strings so that any numbers (stored as strings), are sorted in the correct order? That is, if you have:</p>

<p><code>const vals = ['1743', '596', '94', '337']</code></p>

<p>After sorting, the expected result is:</p>

<p><code>['94', '337', '596', '1743']</code></p>

<p>JavaScript's default array sorting method doesn't do what you need or expect. The reason is easy to find, and is the third sentence on the MDN page for <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort">Array.prototype.sort</a>:</p>

<blockquote>
  <p>The default sort order is according to string Unicode code points.</p>
</blockquote>

<p>For years and years, JS devs have been writing their own <a href="https://stackoverflow.com/a/6568100/1934">compare functions</a>. It's just one of those things that you learn to do even though it <em>feels</em> like something that should be baked into the language.</p>

<p>I recently had to deal with sorting some arrays similar to the above example. In my internet travels, I came across <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator">Intl.Collator</a> via <a href="https://stackoverflow.com/a/38641281/1934">StackOverflow</a>. </p>

<p>Initially, I was skeptical and confused as to why something related to i18n would have this capability, but it works!</p>

<p>It's a little more code than calling <code>.sort()</code>, but not much:</p>

<pre><code class="language-js">const collator = new Intl.Collator(undefined, {  
  numeric: true,
  sensitivity: 'base'
})
vals.sort(collator.compare)  
</code></pre>

<p>Now <code>vals</code> is in the expected order. No need to mess with various natural sort algorithm implementations, collator is in the language and <a href="https://caniuse.com/#search=collator">well supported</a>.</p>]]></content:encoded></item><item><title><![CDATA[Linear versus logarithmic scales]]></title><description><![CDATA[<p>Stephen Few's recent post titled <a href="http://www.perceptualedge.com/blog/?p=2838">Logarithmic Confusion</a> contains a succinct description of linear vs. logarithmic scales:</p>

<blockquote>
  <p>units along a logarithmic scale increase by rate (e.g., ten times the previous value for a log base 10 scale or two times the previous value for a log base 2 scale), not</p></blockquote>]]></description><link>https://derekswingley.com/2018/03/22/linear-versus-logarithmic-scales/</link><guid isPermaLink="false">76caa923-7b13-4213-ac5a-3d4dd5411989</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Thu, 22 Mar 2018 15:40:10 GMT</pubDate><content:encoded><![CDATA[<p>Stephen Few's recent post titled <a href="http://www.perceptualedge.com/blog/?p=2838">Logarithmic Confusion</a> contains a succinct description of linear vs. logarithmic scales:</p>

<blockquote>
  <p>units along a logarithmic scale increase by rate (e.g., ten times the previous value for a log base 10 scale or two times the previous value for a log base 2 scale), not by amount</p>
</blockquote>

<p>For whatever reason, that description resonates with me. I like it better than what's on <a href="https://en.wikipedia.org/wiki/Logarithmic_scale">wikipedia</a>.</p>

<p>The entire post is a worth reading. The gist is someone with a keen eye (Few) suspected a particular chart fit a pleasing narrative a little too well.</p>]]></content:encoded></item><item><title><![CDATA[No more twitter]]></title><description><![CDATA[<p>Back when twitter rolled out 280 characters to everyone, I realized I'd had enough. It wasn't <em>just</em> the doubling of the character limit, that was more of a coincidence. That change was an inflection point for me. It was also right around when the outrage du jour was that some</p>]]></description><link>https://derekswingley.com/2018/03/07/no-more-twitter/</link><guid isPermaLink="false">8c014d2b-3d60-41c9-b325-86e27ac8a3d7</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Wed, 07 Mar 2018 18:30:34 GMT</pubDate><content:encoded><![CDATA[<p>Back when twitter rolled out 280 characters to everyone, I realized I'd had enough. It wasn't <em>just</em> the doubling of the character limit, that was more of a coincidence. That change was an inflection point for me. It was also right around when the outrage du jour was that some nazi got verified. I don't recall the specifics and they don't matter. I told myself that it was time to stop directly going to Twitter. This was back in early November of last year.</p>

<p>It's been a few months, and I still end up on twitter once in a while (primarily from links I click on <a href="http://belong.io/">belong.io</a>) but I don't use any twitter clients and don't go to twitter.com on my own. I don't miss it and my gut tells me I'm better off without it. Without the distraction and time-suck, I have more time decompress, more time for thoughts to develop as opposed to constantly reaching for and craving more <em>more</em> <em><strong>more</strong></em>. I feel more put together. My current view on twitter, after distancing myself from it, is that if consumed regularly and at length, it is too much to handle in a healthy way. At least with the timeline I'd built, so that's why I quit.</p>

<p>In the months since abstaining from twitter, not a week goes by that I don't see someone else pontificating on the perils of social sites. The latest to get my attention is a <a href="https://www.nytimes.com/2018/03/07/technology/two-months-news-newspapers.html">piece in The New York Times</a>. The salient lines:</p>

<blockquote>
  <p>If something really big happens, you will find out.</p>
</blockquote>

<p>and:</p>

<blockquote>
  <p>I began to see it wasn’t newspapers that were so great, but social media that was so bad.</p>
</blockquote>

<p>I couldn't agree more with those statements. So add me to the pile of people telling you to get off social media:  stop using big social media sites, you'll be better for it in the long run.</p>]]></content:encoded></item><item><title><![CDATA[Figuring out why running Ghostery on FiveThirtyEight locks up a browser]]></title><description><![CDATA[<p>I follow what's published on <a href="http://fivethirtyeight.com/">FiveThirtyEight</a> via their <a href="http://fivethirtyeight.com/all/feed">RSS feed</a>. On average, I probably click through to one article a week. A week or two ago, I clicked through to a FiveThirtyEight story and the browser tab where it was loading became unresponsive. My reaction was "somehow they managed to</p>]]></description><link>https://derekswingley.com/2018/01/26/why-running-ghostery-on-five-thirty-eight-locks-up-a-browser/</link><guid isPermaLink="false">8cdad288-36b4-401f-9956-ef363c1324bb</guid><category><![CDATA[adblocking]]></category><category><![CDATA[advertisting]]></category><category><![CDATA[tracking]]></category><category><![CDATA[fivethirtyeight]]></category><category><![CDATA[ghostery]]></category><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Fri, 26 Jan 2018 08:18:46 GMT</pubDate><content:encoded><![CDATA[<p>I follow what's published on <a href="http://fivethirtyeight.com/">FiveThirtyEight</a> via their <a href="http://fivethirtyeight.com/all/feed">RSS feed</a>. On average, I probably click through to one article a week. A week or two ago, I clicked through to a FiveThirtyEight story and the browser tab where it was loading became unresponsive. My reaction was "somehow they managed to get around <a href="https://www.ghostery.com/">Ghostery</a> and do some advertising/tracking shenanigans to freeze my browser, oh well", and I closed the tab. The tab hung around for a minute but then went away. I figured it was an isolated incident.</p>

<p>Fast-forward to this morning. I saw <a href="https://projects.fivethirtyeight.com/redistricting-maps/">a story I was interested in reading</a> in my feed reader so I clicked the link. New tab opens, and is immediately unusable. I can't scroll, I can't open dev tools, I can't close the tab. This time I was a bit more motivated to figure out what was going on.</p>

<p>Once I got the rogue tab closed, I whitelisted FiveThirtyEight. Then the page loaded and operated as expected. But by this point, I couldn't care less about the actual content—I wanted to know <em>why</em> the page froze when running an ad blocker.</p>

<p>If you've run an ad blocker for any amount of time, you have seen weird, half-rendered pages while cruising around the web. Occasionally sites will only render their content after some combination of advertising and tracking scripts have loaded. Others will show you a pop-up if they detect a blocker and ask to be whitelisted. Sometimes I whitelist, sometimes I give up and forget about whatever previously grabbed my attention.</p>

<p>This FiveThirtyEight experience was different. Missing content and broken pages, I understand that. A locked up browser tab from a reputable site I frequent is something different. I really wanted to figure out what was happening.</p>

<p><img src="https://derekswingley.com/content/images/2018/01/browser-task-manager-ghostery-pegged.png" alt="Google Chrome Task Manager showing Ghostery at greater than 100% utilization"></p>

<p>Ghostery was clearly the culprit, but why? I've been using it for a couple years now, and can't recall having a problem like this in the past.</p>

<p>I started re-blocking each tracker identified by Ghostery. I'd block one, reload. If the page worked, I went to the next one. None of the trackers from the big, recognizable companies (Google, Facebook, Adobe) were causing the problem. I could block those and the page still worked.</p>

<p>I narrowed down the problem to the advertising tracker called "<a href="https://apps.ghostery.com/apps/netratings_sitecensus">NetRatings SiteCensus</a>". I'd never heard of it, but apparently it's something from Nielsen (of TV ratings fame, I think).</p>

<p>Visiting the page in question with dev tools gave me a little more info:</p>

<p><img src="https://derekswingley.com/content/images/2018/01/dev-tools-too-many-requests.png" alt="Google Chrome Developer Tools' Network section showing an inordinate number of requests to a tracking script"></p>

<p>Over 2300 requests, running one request per millisecond based on the timestamp in the URL query string. That's enough to freeze any browser tab. The full URL to the script in question is:  <code>https://cdn-gl.imrworldwide.com/novms/js/2/ggcmb510.js?_=timestamp</code>. That script wasn't necessarily problematic, but rather all the requests to get it.</p>

<p>Looking at the "Initiator" for all these requests, I headed over to <code>https://secure.espn.com/combiner/c?js=jquery-1.10.2.js,plugins/jquery.pubsub.r5.js,analytics/visitorAPI_156.js,analytics/sOmni_161.js&amp;ver=20180126</code>. Digging through there, I found the place where some javascript was making a request to get the js file from imrworldwide.com:</p>

<p><img src="https://derekswingley.com/content/images/2018/01/code-inserting-tracking-script.png" alt="FiveThirtyEight javascript inserting additional javascript from a tracking company"></p>

<p>Now I was getting somewhere:  <code>espn.track.initNielsen</code> was trying to load the script in question, then executing a function to call <code>espn.track.nielsen</code>. Who was calling <code>initNielsen</code>? Well, <code>nielsen</code>, of course. The infinite loop was taking shape.</p>

<p>What about the next level up the stack? Who starts this <code>nielsen</code> → <code>initNielsen</code> → <code>nielsen</code> madness?</p>

<p>After diligently dissecting 8k lines of code... just kidding, I ⌘ + f-ed around until I figured out that <code>espn.track.nielsen</code> is first called by another function called <code>init</code> defined inside of another function called <code>espn.track.trackPage</code>. That one, <code>trackPage</code>, is called by another function called <code>loadOmniture</code> that's defined in a <code>&lt;script&gt;</code> tag in the page's HTML just inside the closing body tag. <em>Whew</em>.</p>

<p>Here's a recap starting with page load, rather than working backward from the two functions that endlessly call each other:</p>

<ol>
<li>Page loads  </li>
<li><code>&lt;script&gt;</code> tags in the page are executed  </li>
<li>One <code>&lt;script&gt;</code> runs <code>loadOmniture</code> if <code>!window.___skipShowAds</code> is true  </li>
<li><code>loadOmniture</code> calls <code>espn.track.trackPage</code> passing an object with a property called <code>enableNielsen</code> that is truthy  </li>
<li><code>trackPage</code> defines a local function called <code>init</code> and runs it  </li>
<li><code>init</code> calls <code>espn.track.nielsen</code> when the aforementioned <code>enableNielsen</code> property is true-ish  </li>
<li>our new friend <code>nielsen</code> checks if a global variable named <code>NOLCMB</code> exists  </li>
<li>when there's no variable named <code>NOLCMB</code>, <code>initNielsen</code> gets called  </li>
<li><code>initNielsen</code> tries to insert the Nielsen NetRatings SiteCensus script and runs <code>nielsen</code> again</li>
</ol>

<p>Ghostery blocks that script, so we get an endless loop where each loop iteration makes a request. </p>

<p>Why doesn't this happen without an ad blocker? That is because the Nielsen script in question, <a href="https://cdn-gl.imrworldwide.com/novms/js/2/ggcmb510"><code>https://cdn-gl.imrworldwide.com/novms/js/2/ggcmb510</code></a> defines a global named <code>NOLCMB</code> when it loads, thereby avoiding the infinite loop caused by <code>initNielsen</code> (steps 7 and 8 above).</p>

<p>The <code>NOLCMB</code> global acts as a flag as to whether or not the Nielsen tracker is loaded. The code naively assumes that when the tracker isn't loaded, it can load it. And so it tries over and over.</p>

<p>In summary:  request blocked? Do it again! Again! And again. Yep, again. Repeat thousands of times. <em>browser tab dies</em></p>

<p>This is the one of the few experiences I've had where running an ad-blocker made a page less usable. And since I'd like to avoid that, I've left my whitelisting of FiveThirtyEight in place.</p>]]></content:encoded></item><item><title><![CDATA[How to apologize]]></title><description><![CDATA[<p>This past Sunday, I watched <em>The Sound of Music</em> for the first time. Even when I realized it was nearly three hours, I still went through with it. And what to do you know, I enjoyed it. I even recommend making time to watch it if you've somehow avoided all</p>]]></description><link>https://derekswingley.com/2018/01/18/how-to-apologize/</link><guid isPermaLink="false">228d8610-7db0-4dc2-ab05-c882da7f283e</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Thu, 18 Jan 2018 18:30:00 GMT</pubDate><content:encoded><![CDATA[<p>This past Sunday, I watched <em>The Sound of Music</em> for the first time. Even when I realized it was nearly three hours, I still went through with it. And what to do you know, I enjoyed it. I even recommend making time to watch it if you've somehow avoided all these years, like I had.</p>

<p>There are a number of memorable scenes, but the one I want to mention is when Captain Von Trapp, after being bluntly told by Maria that he should love his children, evicts Maria and says she must return to the Abbey. Immediately after, he hears his children singing a song that Maria taught them. He quickly realizes he was wrong to come down so hard on Maria. The next thing you know, he's righting his wrong. Which brings us to the dialog that inspired this post:</p>

<blockquote>
  <p>I behaved badly. I apologize. You were right. I don't know my children.</p>
</blockquote>

<p><strong>Wow</strong>. No bullshit there. A complete and total admission of being wrong, unconditional and unqualified. He's accepting responsibility for being an asshole and directly acknowledging his error. Not only that, there are no excuses; no "but I..." or "because...". It's short, to the point, and impossible to misunderstand. He also doesn't grovel or cower. He maintains self respect while being contrite.</p>

<p>In my experience, an apology of this caliber is rare. <a href="https://en.wikiquote.org/wiki/The_Rules_of_the_Game#Octave">Everyone has their reasons</a>. However, reasons (or intentions), do not excuse bad behavior. <em>Actions</em> matter, not <em>intentions</em>. </p>

<p>To satisfy the title of the post, let's break this down into tiny steps. To build an effective apology:</p>

<ol>
<li>Acknowledge you were wrong by <em>saying so</em>. It can be as simple as the Captain:  <strong>I behaved badly</strong>  </li>
<li>Apologize. Keep it simple:  <strong>I apologize</strong> or <strong>I'm sorry</strong>  </li>
<li>Attempt to make amends by addressing your failure or shortcoming.  </li>
<li>Avoid excuses.</li>
</ol>

<p>What's there is just as important as what's missing:  there are no excuses, no reasons for what was wrong. This is crucial because it means there is no shifting of responsibility or blame. Because there is ownership of wrongdoing, <strong>forgiveness from the other side can follow</strong>. An apology that results in forgiveness means you are likely to avoid having a grudge held against you. Finally, there <em>isn't</em> a promise about future behavior. </p>

<p>This form of apology is appropriate for one-off errors or mistakes. If you're doing something bad habitually, and subsequently need to assure someone that this really is the <em>last time</em>, that <em>you're so, so sorry</em>, you need to have a longer, more substantial conversation. You'll need more than 10-15 words.</p>

<p>When you do screw up, as we all do, apologize. If you don't shift the blame for <em>your</em> actions, you're more likely to learn from the experience. As a result, you will be less likely to repeat the same mistake. To apologize effectively, apologize directly, respectfully, and without excuses.</p>]]></content:encoded></item><item><title><![CDATA[A Goldilocks zone summary of modern javascript]]></title><description><![CDATA[<p>Summarize modern web development and provide historical context for how we ended up where we are today. </p>

<p>That's a difficult task but <a href="https://medium.com/@peterxjang">Peter Jang</a>'s <a href="https://medium.com/the-node-js-collection/modern-javascript-explained-for-dinosaurs-f695e9747b70">Modern JavaScript Explained For Dinosaurs</a> is a home run. It's suitable for greybeards and greenhorns alike.</p>

<p>From a single html file with a <code>&lt;script&</code></p>]]></description><link>https://derekswingley.com/2018/01/16/a-goldilocks-zone-summary-of-modern-javascript/</link><guid isPermaLink="false">4037e10d-aec8-4317-9772-ddf3ee2c2579</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Tue, 16 Jan 2018 19:33:28 GMT</pubDate><content:encoded><![CDATA[<p>Summarize modern web development and provide historical context for how we ended up where we are today. </p>

<p>That's a difficult task but <a href="https://medium.com/@peterxjang">Peter Jang</a>'s <a href="https://medium.com/the-node-js-collection/modern-javascript-explained-for-dinosaurs-f695e9747b70">Modern JavaScript Explained For Dinosaurs</a> is a home run. It's suitable for greybeards and greenhorns alike.</p>

<p>From a single html file with a <code>&lt;script&gt;</code> tag through configuring webpack, babel, and npm, his article is a great overview summarizing the current state of web development and how we got here. There's just enough history (he leaves <em>plenty</em> out, which is probably one reason it's so successful) and just enough detail as to why the current status quo is what it is. Not too much, not too little—it's just right. The panels from <a href="http://www.qwantz.com/">Dinosaur Comics</a> are a nice touch too.</p>]]></content:encoded></item><item><title><![CDATA[Spectre as an analogy for the state of tech in 2017]]></title><description><![CDATA[<p><a href="https://stratechery.com/2018/meltdown-spectre-and-the-state-of-technology/">Ben Thompson writing about Meltdown and Spectre</a>:</p>

<blockquote>
  <p>I ended 2017 without my customary “State of Technology” post, and just as well: Spectre is a far better representation than anything I might have written. Faced with a fundamental imbalance (data fetch slowness versus execution speed), processor engineers devised an ingenious system</p></blockquote>]]></description><link>https://derekswingley.com/2018/01/11/spectre-as-an-analogy-for-the-state-of-tech-in-2017/</link><guid isPermaLink="false">3aa24bcf-8985-46b5-a04b-58326b4ce4e6</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Thu, 11 Jan 2018 17:31:49 GMT</pubDate><content:encoded><![CDATA[<p><a href="https://stratechery.com/2018/meltdown-spectre-and-the-state-of-technology/">Ben Thompson writing about Meltdown and Spectre</a>:</p>

<blockquote>
  <p>I ended 2017 without my customary “State of Technology” post, and just as well: Spectre is a far better representation than anything I might have written. Faced with a fundamental imbalance (data fetch slowness versus execution speed), processor engineers devised an ingenious system optimized for performance, but having failed to envision the possibility of bad actors abusing the system, everyone was left vulnerable.</p>
  
  <p>The analogy is obvious: faced with a fundamental imbalance (the difficulty of gaining and retaining users versus the ease of rapid iteration and optimization), Internet companies devised ingenious systems optimized for engagement, but having failed to envision the possibility of bad actors abusing the system, everyone was left vulnerable.</p>
  
  <p>Spectre, though, helps illustrate why these issues are so vexing:</p>
  
  <ul>
  <li>I don’t believe anyone intended to create this vulnerability</li>
  <li>The vulnerability might be worth it — the gains from faster processors have been absolutely massive!</li>
  <li>Regardless, decisions made in the past are in the past: the best we can do is muddle through</li>
  </ul>
  
  <p>So it is with the effects of Facebook, Google/YouTube, etc., and the Internet broadly. Power comes from giving people what they want — hardly a bad motivation! — and the benefits still may — probably? — outweigh the downsides. Regardless, our only choice is to move forward.</p>
</blockquote>

<p>As Thompson admits, there are some really strained analogies earlier in the piece. However, this final analogy couldn't be more apt. </p>]]></content:encoded></item><item><title><![CDATA[Migrating from Mapzen Search to Mapbox Geocoding]]></title><description><![CDATA[<p><a href="https://mapzen.com/blog/shutdown/">Mapzen's services</a> will go away at the end of the month 😢. I'd only used their services in scratching personal itches and the only public project I have using anything of theirs is my <a href="https://derekswingley.com/lab/trip-cast/">trip cast site</a>. Nevertheless, they were a group I enjoyed interacting with and their work over the</p>]]></description><link>https://derekswingley.com/2018/01/10/migrating-from-mapzen-search-to-mapbox-geocoding/</link><guid isPermaLink="false">8a72b31a-b46f-4496-bf33-766ef1da104a</guid><dc:creator><![CDATA[Derek Swingley]]></dc:creator><pubDate>Wed, 10 Jan 2018 19:12:42 GMT</pubDate><content:encoded><![CDATA[<p><a href="https://mapzen.com/blog/shutdown/">Mapzen's services</a> will go away at the end of the month 😢. I'd only used their services in scratching personal itches and the only public project I have using anything of theirs is my <a href="https://derekswingley.com/lab/trip-cast/">trip cast site</a>. Nevertheless, they were a group I enjoyed interacting with and their work over the past few years had pushed the envelope of general expectations for geospatial web services and projects. Their products worked reliably, smoothly, and easily. Their services were easy to interact with, thoroughly documented, and serve as great examples for anyone who wants to build services for the web. </p>

<p>I love the phrase <a href="http://www.folkloredesign.com/blog/2014/9/success-hides-problems">"success hides problems"</a> (credit to <a href="https://en.wikipedia.org/wiki/Edwin_Catmull">Ed Catmull</a>) and can't help but think there's an equally pithy phrase along the lines of "failure masks achievements" that conveys an equally important lesson. Mapzen is closing shop but their legacy will be great work that will live on because <a href="https://mapzen.com/blog/our-magna-carto/">so much of it is in the open</a>.</p>

<p>Since their search service will be gone by the end of the month, and since I'm not ready to stop using <a href="https://derekswingley.com/lab/trip-cast/">trip cast</a>, I needed an alternative. I was already using a Mapbox map so I decided see what was involved to drop-in Mapbox's geocoder in place of the Mapzen search service. Turns out, <a href="https://github.com/swingley/trip-cast/commit/0eca25bdc275e33b4fa5c00a1163897f64a0aeee">not much!</a> That link goes to the commit with the changes required to use Mapbox instead of Mapzen. Obviously there's a different URL for the service. Other than that, results from Mapbox hang properties for place info directly off of result objects instead of nesting them inside the usual GeoJSON properties object. Not a big deal. The two Mapbox documentation pages that helped me were <a href="https://www.mapbox.com/api-documentation/?language=JavaScript#request-format">API docs for response objects</a> and <a href="https://www.mapbox.com/api-playground/#/?_k=h64xf6">API playground page to search for places</a>. In limited testing, the results are what you'd expect from a place search/geocoding service.</p>

<p>Farewell Mapzen!</p>]]></content:encoded></item></channel></rss>