Content warning: Way more nitty-gritty than an end user of Beeminder could reasonably care about. But maybe you unreasonably care about it? Either way, Beeminder is going to be way better for everyone when this is done.
Background: Yellow Brick Whatnow?
To start at the very beginning… The Yellow Brick Road is the path on your graph that starts at the current value of the metric you’re minding and ends at where you want it to be, or perhaps soars upward indefinitely. (Related: Why Beeminder likes cumulative graphs.) Historically the yellow brick road has been defined in terms of the centerline and then so-called lanes on each side of that. The lane on the good side of the road we called being in the blue and represented two days of safety buffer. On the bad side of the centerline was being in the orange which was one day of buffer. (If you’re thinking of a standard do-more goal then “good” means above the road and “bad” below, but this is reversed for do-less or weight loss goals.)
That turned out to be a horrible design choice, brutally complicating all sorts of things that should be simple, like computing how much safety buffer you have, especially when the slope of the road changes.
Yellow Brick Half-Plane means scrapping the concept of lanes. Instead of defining the road in terms of the centerline, it’s defined in terms of the critical edge. Then you can still have colored zones defined in terms of the number of safe days. This has a million implications for the implementation but the primary user-visible difference will be that the thing you’re editing with the road editor is the one bright line that you can’t end the day on the wrong side of.
Status
We made a checklist of 9 hairy infrastructural changes to Beeminder before we can make the transition to Yellow Brick Half-Plane (YBHP) and we’re now more than halfway through it.
This is also intertwined with porting Beebrain – that’s the module that takes your datapoints and all your goal settings and computes the myriad statistics and draws the actual graph – from Python to Javascript. So here’s the list:
-
Die timezones.(Beeminder-proper should deal with all the timezone logic and just send datapoints to Beebrain specified as daystamps.) -
Die asof-null.(Beeminder-proper tells Beebrain explicitly the “as of” date for drawing the graph so we can purge the code in Beebrain that would infer it and so Beeminder-proper doesn’t have to worry about when Beebrain generated a graph or anything.) -
Die edgy.(This was such a clusterf*ck I don’t know where to begin. Ok, let me put some old notes in a footnote for posterity so we can marvel at the absurdity [1] and then move on!) -
Die reset.(Beebrain used to handle the convoluted logic of hard resets on goals that would hide old data and reset where the yellow brick road started. Nowadays you just zoom in if you don’t want to see old data but there’s fundamentally one road and one continuous dataset (cf UVI#2468).) -
Die inferred tini/vini.(Beebrain used to be in charge of inferring the start of the yellow brick road based on your first datapoint. Now Beeminder-proper fully specifies the yellow brick road (cf UVI#2405).) -
Die weight loss leniency.(More convoluted logic that should never have been in Beebrain’s purview (cf UVI#2475).) -
Die noisy width and auto-widening.(Now blogged; see also what I said in a beemail [2] (cf UVI#2474 and UVI#3271).) -
Die exponential roads.(These can be perfectly well approximated by piecewise linear functions. Weekly or monthly changes in the linear slope of the yellow brick road is always good enough. It was fun to write the code that did this all exquisitely correctly but, as they say, kill your darlings!) - Die lanes!
It’s been a long road, so to speak, but the light is at the end of the tunnel. I was just hashing out the latest design decisions for #7 with @bee and we thought we’d try doing it in public in a forum thread so, here we are!
PS: HT Kevin Lochner who, years ago, first said “Isn’t this yellow brick road more of a yellow brick half-plane?” to which we probably said “kind of but that sounds stupid” and HT @insti for articulating the problem with lanes as we’d implemented them and suggesting the theoretically elegant solution of defining the road in terms of the critical edge. Last but not least, HT @kenoubi for a putting a bounty on this.
Footnotes
[1] Notes on the “edgy” parameter for posterity:
Click if you must
Suppose you start a pushups goal of 3/day but want the first 3 due on the first day. This means starting the centerline of the yellow brick road above the initial datapoint so the initial datapoint starts on the edge of the road. So an “edgy” goal is one where the initial point is on the bottom (or top, depending on “yaw”) edge of the road. That means moving the start of the road up (or down) by one lane width compared to where it would be if it started at the initial datapoint. But we have a chicken and egg problem: We need the road function in order to compute the lane width and we need the initial point, (tini,vini), to compute the road function. In other words, we need (tini,vini) to compute the lane width but we need the lane width to compute (tini,vini), to shift it.
Solution (sort of): The lane width is generally equal to the daily rate of the road. So we can have the road start at the initial datapoint but one day earlier. Ie, (tini,vini) -= (86400,0). Then when we know the road function we can move tini forward a day and vini by the appropriate amount, vini = rdf(tini+86400).
The only problem with this approach is that it only puts the initial datapoint at the edge if the rate of the first segment of the road is the same as the rate of the most recent datapoint, since it’s the rate there that determines the overall lane width. Trying to do this Really Right gets very messy or even impossible without introducing worse problems than the initial point not being on the actual edge.
I think the right solution to the ‘edgy’ confusion is to get rid of the edgy parameter – goals are always edgy. To get the edgy=false behavior, just use the road matrix to specify one initial flat day. And in fact goals should always have an initial road row that says rate 0 up till yesterday or today (because otherwise if you add a datapoint before the first datapoint then you’ll make the road change).
[later] Bethany has now convinced me that edginess should not be in Beebrain’s purview. The start of the road is always given explicitly by tini and vini. When you create a goal that should be edgy you know the initial rate (eg, the generous estimate that the user provides for Set-A-Limit [now do-less] goals) so you can can bump vini up by that amount. So we’re going to do that! Phew!
[2] Excerpt from daily beemail: We’re killing off auto-widening yellow brick roads (finally!) and instead asking you explicitly when you create the goal what your max daily weight fluctuation is. Then we actually use a zero-width road but starting enough above your initial datapoint to account for your fluctuations.
In some ways this is a temporary step backwards but our intent is to end up with the best of both worlds where there’s a fixed bright line you have to stay below but we do the same data analysis to give you warnings about your proximity to that line that are at least as good as the current so-called lanes are.
And let me not mince words (I guess that’s easy when I’m only castigating my past self!): I now feel my entire auto-widening concept was deeply wrongheaded and I’m pretty embarrassed at the amount of effort I spent rationalizing it!