More from Applied Cartography
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"
Hello, 2025! (As always, Death Cab remains the soundtrack by which you should read this.) Headlining this month was my annual review; everything else is flotsam. But, speaking of flotsam: A quick development note on the order attribute in CSS Notes on Linear and Fathom Why Buttondown has an 'alternatives' page Some thoughts on a nascent new newsletter project Letters to a young bartender A warning about hidden settings Only two pieces of media that I finished: Case Histories (meh) and Luck be a Landlord (much better) I have much to write; the backlog is longer than ever before, both literally and figuratively. Our water is out; our daughter is happy; we are keeping sane, and warm, and busy, and we are sleeping lightly but well.
(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
The tragedy in Washington D.C. this week was horrible, and a shocking incident. There should and will be an investigation into what went wrong here, but every politician and official who spoke at the White House today explicitly blamed DEI programs for this crash. The message may as well
Mark your calendars for March 21-22, 2025, as we come together for a special Arduino Day to celebrate our 20th anniversary! This free, online event is open to everyone, everywhere. Two decades of creativity and community Over the past 20 years, we have evolved from a simple open-source hardware platform into a global community with […] The post Join us for Arduino Day 2025: celebrating 20 years of community! appeared first on Arduino Blog.
Disruptive technologies call for rethinking product design. We must question assumptions about underlying infrastructure and mental models while acknowledging neither change overnight. For example, self-driving cars don’t need steering wheels. Users direct AI-driven vehicles by giving them a destination address. Keyboards and microphones are better controls for this use case than steering wheels and pedals. But people expect cars to have steering wheels and pedals. Without them, they feel a loss of control – especially if they don’t fully trust the new technology. It’s not just control. The entire experience can – and perhaps must — change as a result. In a self-driving car, passengers needn’t all face forward. Freed from road duties, they can focus on work or leisure during the drive. As a result, designers can rethink the cabin experience from scratch. Such changes don’t happen overnight. People are used to having agency. They expect to actively sit behind the wheel with everyone facing forward. It’ll take time for people to cede control and relax. Moreover, current infrastructure is designed around these assumptions. For example, road signs point toward oncoming traffic because that’s where drivers can see them. Roads transited by robots don’t need signals at all. But it’s going to be a while before roads are used exclusively by AI-driven vehicles. Human drivers will share roads with them for some time, and humans need signs. The presence of robots might even call for new signaling. It’s a liminal situation that a) doesn’t yet accommodate the full potential of the new reality while b) trying to accommodate previous ways of being. The result is awkward “neither fish nor fowl” experiments. My favorite example is a late 19th Century product called Horsey Horseless. Patent diagram of Horsey Horseless (1899) via Wikimedia Yes, it’s a vehicle with a wooden horse head grafted on front. When I first saw this abomination (in a presentation by my friend Andrew Hinton,) I assumed it meant to appeal to early adopters who couldn’t let go of the idea of driving behind a horse. But there was a deeper logic here. At the time, cars shared roads with horse-drawn vehicles. Horsey Horseless was meant to keep motorcars from freaking out the horses. Whether it worked or not doesn’t matter. The important thing to note is people were grappling with the implications of the new technology on the product typology given the existing context. We’re in that situation now. Horsey Horseless is a metaphor for an approach to product evolution after the introduction of a disruptive new technology. To wit, designers seek to align the new technology with existing infrastructure and mental models by “grafting a horse.” Consider how many current products are “adding AI” by including a button that opens a chatbox alongside familiar UI. Here’s Gmail: Gmail’s Gemini AI panel. In this case, the email client UI is a sort of horse’s head that lets us use the new technology without disrupting our workflows. It’s a temporary hack. New products will appear that rethink use cases from the new technology’s unique capabilities. Why have a chat panel on an email client when AI can obviate the need for email altogether? Today, email is assumed infrastructure. Other products expect users to have an email address and a client app to access it. That might not always stand. Eventually, such awkward compromises will go away. But it takes time. We’re entering that liminal period now. It’s exciting – even if it produces weird chimeras for a while.
A quick intro to interfacing common OLED displays to bare-metal microcontrollers.