Visual formatting of floating point variables

I recently tweaked my sunflower demo

https://beta.observablehq.com/@jbum/circles-spirals-and-sunflowers

So that the increment in the spiral animation (about 2/3rds down) displays. I used the following code.

    increment = { 
      let duration = 1000*180; 
      let phase = now%duration;
      let degrees = phase*360/duration;
      degrees = Math.floor(degrees*100)/100; // visual display to 2 decimal places
      return degrees; 
    }

Note the trick I used to force the display to 2 decimal places. Is there a better way to do this?
I’d prefer to have the actual variable unconstrained and just visually display it in %.02f format…

This looks a little cleaner (but is still mixing up visual formatting with variable truncation).

      while (true) {
        for (var d = 0; d < 360; d += .05) {
          yield Promises.delay(25,Math.floor(d*100)/100);
        }
      }
    }

We use JavaScript’s standard number.toString in the inspector, which uses as few digits as possible to uniquely identify the value. Unfortunately that means there’s no way to control the number of displayed digits, short of rounding as you are already doing.

That said, you could implement the loop another way to avoid floating point error:

increment = { 
  while (true) {
    for (var d = 0; d < 36000; d += 5) {
      yield Promises.delay(25, d / 100);
    }
  }
}

You could also have a separate cell which represents the pretty, human-readable number—but then you’d see the value in two places.

DOM.text(increment.toFixed(2))

If you want to be super fancy, and have complete control over both the value that’s exposed programmatically to the rest of your notebook separately from how it appears in the page, you can use viewof:

viewof increment = {
  const pre = html`<pre class="O--inspect O--number">`;
  pre.value = 0;
  const interval = setInterval(() => {
    pre.textContent = pre.value.toFixed(2);
    pre.value += 0.05;
    pre.dispatchEvent(new CustomEvent("input"));
  }, 25);
  yield pre;
  try { yield Promises.never; }
  finally { clearInterval(interval); }
}

In a sense, this makes the increment cell function like an input slider—it’s just that the user can’t control the slider; it’s only controlled programmatically by the internal timer interval. So the increment value is displayed as a PRE element (for pretty formatting), but the value exposed to the rest of the notebook is a number, pre.value. And by dispatching an input event whenever that value changes, the rest of the notebook automatically reacts.

The last part, yielding Promises.never, is so you can dispose of the interval if the cell is ever re-evaluated: when Observable re-runs the cell, it calls generator.return so you can perform any necessary cleanup.

That’s probably a little more detail that you were expecting but I wanted to convey that you have quite a few options to tackle this problem. :slight_smile:

1 Like

Another hacky workaround is to add some CSS (to e.g. the title cell):

<style>
.O--number {
  display: inline-block;
  max-width: 50px;
  white-space: nowrap;
  overflow: hidden;
}
.O--field .O--number {
  display: inline;
}
</style>