More from Applied Cartography
Guillermo posted this recently: What you name your product matters more than people give it credit. It's your first and most universal UI to the world. Designing a good name requires multi-dimensional thinking and is full of edge cases, much like designing software. I first will give credit where credit is due: I spent the first few years thinking "vercel" was phonetically interchangable with "volcel" and therefore fairly irredeemable as a name, but I've since come around to the name a bit as being (and I do not mean this snarkily or negatively!) generically futuristic, like the name of an amoral corporation in a Philip K. Dick novel. A few folks ask every year where the name for Buttondown came from. The answer is unexciting: Its killer feature was Markdown support, so I was trying to find a useful way to play off of that. "Buttondown" evokes, at least for me, the scent and touch of a well-worn OCBD, and that kind of timeless bourgeois aesthetic was what I was going for with the general branding. It was, in retrospect, a good-but-not-great name with two flaws: It's a common term. Setting Google Alerts (et al) for "buttondown" meant a lot of menswear stuff and not a lot of email stuff. Because it's a common term, the .com was an expensive purchase (see Notes on buttondown.com for more on that). We will probably never change the name. It's hard for me to imagine the ROI on a total rebrand like that ever justifying its own cost, and I have a soft spot for it even after all of these years. But all of this is to say: I don't know of any projects that have failed or succeeded because of a name. I would just try to avoid any obvious issues, and follow Seth's advice from 2003.
Consider a fan-out-ish model that you want to aggregate a bunch of: likes on a post, for instance. class Post(models.Model): created_at = models.DateTimeField() class Event(models.Model): post = models.ForeignKey(Post, on_delete=models.CASCADE) created_at = models.DateTimeField() Let's say we want to aggregate the number of likes on a post relative to the post's creation time (for instance, to visualize the growth of a post's popularity over time), yielding a list of tuples of the form [(minutes_since_post_creation, number_of_likes)]. def test_calculate_popularity_over_time(self): post = Post.objects.create(created_at=datetime.datetime(2020, 1, 1)) Event.objects.create(post=post, created_at=datetime.datetime(2020, 1, 1, 0, 1, 0)) Event.objects.create(post=post, created_at=datetime.datetime(2020, 1, 1, 0, 1, 0)) Event.objects.create(post=post, created_at=datetime.datetime(2020, 1, 1, 0, 1, 0)) Event.objects.create(post=post, created_at=datetime.datetime(2020, 1, 1, 0, 2, 0)) Event.objects.create(post=post, created_at=datetime.datetime(2020, 1, 1, 0, 3, 0)) Event.objects.create(post=post, created_at=datetime.datetime(2020, 1, 1, 0, 5, 0)) results = calculate_popularity_over_time(post) assert results == [ [1, 3], [2, 1], [3, 1], [4, 0], [5, 0], ] We might naively try to do this in-memory, by pulling everything out of the database and doing the math in Python, but that's going to be slow: import defaultdict def calculate_popularity_over_time(post): events = Event.objects.filter(post=post) minute_to_count = defaultdict(int) for event in events: minute_to_count[event.created_at.minute] += 1 return minute_to_count.items() This is an aggregation: databases are good at aggregations! Your first instinct might be to try to do this using ExtractMinute: from django.db.models.functions import ExtractMinute from django.db.models import F def calculate_popularity_over_time(post): events = Event.objects.filter(post=post).annotate( delta=F("created_at") - F("post__created_at") delta_in_minutes=ExtractMinute("delta") ) .order_by("delta_in_minutes") .values_list("delta_in_minutes") .annotate(count=Count("id")) But this has a bug. Can you spot it? Here's a hint: def test_calculate_popularity_over_time_different_hours(self): post = Post.objects.create(created_at=datetime.datetime(2020, 1, 1)) Event.objects.create(post=post, created_at=datetime.datetime(2020, 1, 1, 0, 1, 0)) Event.objects.create(post=post, created_at=datetime.datetime(2020, 1, 1, 1, 1, 0)) results = calculate_popularity_over_time(post) assert results == [ [1, 1], [61, 1], ] The problem is that ExtractMinute, well, extracts the minute, rather than truncates it. Extracting the minute from a duration of 1 hour, 30 minutes, and 5 seconds yields 30, not 90. Whatever are we to do? Well, we can take advantage of EPOCH to truncate the duration of a duration to its number of seconds, and then convert that to minutes: from django.db.models.functions import Extract from django.db.models import ExpressionWrapper, F class ExtractEpoch(Extract): lookup_name = "epoch" def calculate_popularity_over_time(post): events = Event.objects.filter(post=post).annotate( delta=F("created_at") - F("post__created_at") delta_in_minutes=ExpressionWrapper( ExtractEpoch(F("delta")) / 60, output_field=models.IntegerField() ), ) .order_by("delta_in_minutes") .values_list("delta_in_minutes") .annotate(count=Count("id")) I suspect there may be even more elegant and efficient ways to do this, but this satisfies a couple constraints: It delegates the heavy lifting to the database It doesn't require dropping down to raw SQL or registering intermediate tables It's fairly reusable and "Django-ish"
(Order in the CSS rule sense, not the metaphysical sense.) The time has come to begin exfiltrating some higher-traffic, higher-leverage parts of Buttondown's admin UI away from the autogenerated Django admin and into some more bespoke components. Take, for instance, this rather grisly (but useful) inline admin table: This contains useful information but is annoying to parse. It gets replaced with the following HTML, all still governed by Django and a wallop of Tailwind [1]: draft 8:14 p.m. about_to_send 8:16 p.m. in_flight 8:18 p.m. sent 8:20 p.m. It's a trivial thing, but lets me pull out a new toy that I learned from Mary a few weeks back: order, which lets you re-arrange the rendering order of elements in a flex container irrespective of their original order. For instance, consider the following: <div class="flex gap-1"> <div class="order-2">world</div> <div class="order-1">hello</div> </div> This, as you might expect, renders as: world hello Where this gets really useful is composition with groups. Consider the following: <div class="flex gap-1 w-[200px]"> <div class="group flex-1 flex items-center"> <div class="z-10 flex items-center justify-center size-6 bg-red-500"></div> <div class="w-full bg-gray-200 h-0.5"></div> </div> <div class="group flex-1 flex items-center"> <div class="z-10 flex items-center justify-center size-6 bg-red-500"></div> <div class="w-full bg-gray-200 h-0.5"></div> </div> <div class="group flex-1 flex items-center"> <div class="z-10 flex items-center justify-center size-6 bg-red-500"></div> <div class="w-full bg-gray-200 h-0.5"></div> </div> </div> This renders, unexcitingly, as: But! Add a tiny sprinkle of group-last: <div class="flex gap-1 w-[200px]"> <div class="group flex-1 flex items-center"> <div class="z-10 flex items-center justify-center size-6 bg-red-500 group-last:bg-blue-500" ></div> <div class="w-full bg-gray-200 h-0.5 group-last:order-first"></div> </div> <div class="group flex-1 flex items-center"> <div class="z-10 flex items-center justify-center size-6 bg-red-500 group-last:bg-blue-500" ></div> <div class="w-full bg-gray-200 h-0.5 group-last:order-first"></div> </div> <div class="group flex-1 flex items-center"> <div class="z-10 flex items-center justify-center size-6 bg-red-500 group-last:bg-blue-500" ></div> <div class="w-full bg-gray-200 h-0.5 group-last:order-first"></div> </div> </div> And you get: Obviously, that is a lot of HTML to write for a very simple outcome. There are a bunch of ways you could reduce this. Why so much repetition? Why specify group-last for every node when you know it's going to be the last one? Because in reality, the above HTML is actually, in Django, a pure and concise for loop: <div class="flex gap-1 w-[200px]"> {\% for item in items \%} <div class="group flex-1 flex items-center"> <div class="z-10 flex items-center justify-center size-6 bg-red-500 group-last:bg-blue-500" ></div> <div class="w-full bg-gray-200 h-0.5 group-last:order-first"></div> </div> {\% endfor \%} </div> Which suddenly looks much more pleasant and maintainable than its conditionally-rendered alternative: <div class="flex gap-1 w-[200px]"> {\% for item in items \%} <div class="group flex-1 flex items-center"> <div class="z-10 flex items-center justify-center size-6 {\% if forloop.last \%} bg-blue-500 {\% else \%} bg-red-500 {\% endif \%}" ></div> <div class="w-full bg-gray-200 h-0.5 {\% if forloop.last \%} order-first {\% endif \%}" ></div> </div> {\% endfor \%} </div> Not mobile friendly, I'm afraid! Not exactly a pressing concern for the admin site at the moment. ↩︎
I invited nickd to join our Linear instance yesterday, which reminded me that I had a slew of notes I wanted to publish on our own experience of migrating from GitHub Issues to Linear, and some reflections on it as a product now that we've been using it for a few months. One useful lens for understanding a product's positioning and strategy is to look at what its primitives are — the experiences and states that stand alone (and hopefully offer value) without any exogenous or endogenous connections. [1] For Linear, the core primitives are issue and projects; you cannot, for instance, tie issues to initiatives, not can you create a document that doesn't tie back to a project or issue. It does not exactly take oracular powers to assume that this is not going to be the case for much longer; it is easy and seductive to imagine a world where documents and writing of a more artificial, less project-based nature live in Linear, simply because that's the most pleasant place to write, read, and collate them. Regardless of Linear's success as a brand, it's very very clear that Linear's success as a product is simple: it is really, really good, in a way that is almost uninteresting. Linear offers very few novel features [2] and instead invested a lot of time, energy, and polish in... just getting everything correct, and making it work extremely extremely fast. (This is, to be clear, whatever the opposite of a backhanded compliment is.) [REDACTED BUTTONDOWN ENGINEER] commented that using Linear "felt so much better than using GitHub Issues that it made [them] want to actually spend time making sure their issues were up to date", and I echo that sentiment tenfold. There aren't a lot of bold new workflows or insights that using Linear has unlocked, but it has changed "backlog management" from one of my least favorite chores due to the lag of GitHub Issues into something that I genuinely look forward to doing because it feels suddenly tactile and cybernetic. Relatedly, what all of these nascent "Linear for X" tools seem to get wrong is that the specific design language and branding is downstream (or even orthogonal) to what makes Linear feel so good, which is its obsession with ergonomics. The hyper-designed table views and Things-style progress-indicator chart icons are nice, but they belie the obsession with performance. [3] In case anyone's reading from the Linear team, my (meager!) wishlist is as follows: Let me assign individual issues to initiatives Docs as a first-party primitive Let me mark certain views/issues as publicly visible (I know this one's niche!) And, similarly, much of the nuance in "land-and-expand"-style product development, where you wriggle your way into a company with a single wedge and then hope that the company jams more of its state and headcount and process into your product, is offer facility and stair-step value in doing so. ↩︎ The label taxonomy thing, maybe? And "pivot tables in a sidebar"? ↩︎ And performance is much harder to ape. ↩︎
More in technology
You may not have heard of Logan Bartlett, but he’s one of the most hilarious people on Twitter and does a really interesting podcast. (He had a cool episode with Marc Benioff recently.) We sat down for a discussion on managing through crisis, open source and AI, employee liquidity, future of WordPress, and more. You … Continue reading On Logan Bartlett Show →
noclip is quite the find, and I've lost more time than I'd like to admit in it this week. Basically, it has a bunch of 3D models from mostly PS2-Wii era games that you can explore in your browser. It's awesome and anyone with
How has this uncertainty affected software engineers at the Chinese-owned social network?
The silk industry has a rich history in Italy, but modern challenges have brought this centuries-old tradition to the brink of decline. Once a cornerstone of the rural economy in Italy, with a strong presence in Zagarolo, Rome, silk production has dwindled in the country due to industrial developments, synthetic fibers’ growing popularity, and fierce […] The post Tecnoseta revives the silk industry with open-source innovation appeared first on Arduino Blog.
Plus a giant leap for devolution, the nuclear industry being cowards, and some crazy humanoid robots