Block versus function

Hi,

I have a working chart (the details are not important) whose structure starts as

LinePlot = {
  const container = html`<div class="tooltip-demo">
    <style>
      div.tooltip-demo > div.tooltip {
        position: fixed;
        display: none;
        padding: 12px 6px;
        background: #fff;
        border: 1px solid #333;
        pointer-events: none;
      }
    </style>
  </div>`; 
  
  //const container = Container()
  
  const width = 500
  const height = 100
  
   const svg = d3
    .select(container)
    .append("svg")
    .attr("viewBox", `0 0 ${width} ${height}`);
....

However, I would like to modularize it. This was done in two ways. First with a block, then with a function. In the function case, everything works fine. I define:

function Container() {
    const container = html`<div class="tooltip-demo">
    <style>
      /* 
       * Be sure to set the tooltip's DOM element's styles!
       */
      div.tooltip-demo > div.tooltip {
        position: fixed;
        display: none;
        padding: 12px 6px;
        background: #fff;
        border: 1px solid #333;
        pointer-events: none;
      }
    </style>
  </div>`;
  return container
}

so that the original block becomes

LinePlot = {
  const container = Container()
  const width = 500
  const height = 100
  ...
  return container

with no other changes. Everything is clean and the chart is unmodified in all respects, as I expected. However, the following approach did not work:

Container = {
    const container = html`<div class="tooltip-demo">
    <style>
      /* 
       * Be sure to set the tooltip's DOM element's styles!
       */
      div.tooltip-demo > div.tooltip {
        position: fixed;
        display: none;
        padding: 12px 6px;
        background: #fff;
        border: 1px solid #333;
        pointer-events: none;
      }
    </style>
  </div>`;
  return container
}

This is a simple block. I expected the contents of this block to be isolated from other blocks, but this did not appear to be the case. The LinePlot block is now:

Container = {
    const container = html`<div class="tooltip-demo">
    <style>
      div.tooltip-demo > div.tooltip {
        position: fixed;
        display: none;
        padding: 12px 6px;
        background: #fff;
        border: 1px solid #333;
        pointer-events: none;
      }
    </style>
  </div>`;
  return container
}

The image appears on top of the Container block, and above the LineChart block, I get
2021-04-15_14-51-20

I do not understand how this is possible. While Observable is reactive, I thought the block contents were isolated for each other. How is it that my plot shows up over the Container block? Until I understand this, it will be difficult to make progress in my modularization and encapsulation efforts.

Thanks for any advice on this. Gordon.

1 Like

Function and block are not equivalent. Calling the function will always return a new DOM element, while with the block form your Container cell’s value will be one specific element.

A DOM node can only exist at one position in the DOM. If you reparent it (e.g. by embedding it into another html template literal), it will be moved out of the cell’s output wrapper.

Now, you’ll only see HTMLDivElement {} if the returned element already has a parent element, precisely to avoid unwanted reparenting by Observable’s preview. This can happen if you don’t return the outermost element from your cell, e.g.:

Container = {
  const container = html`<div class="tooltip-demo">`;
  const parent = html`<div>${container}`;
  return container;
}

I have to assume that your code examples are incomplete, so it’s hard to determine the specific reason in your case. Perhaps you can link-share your notebook?

Thanks for this explanation, which almost makes sense. I am new to Javascript and Observable (I started Javascript when it first came out but never got into it, opting for several other languages. The => operator lowers my entry barrier).

Yes, I kept the example short to save space. Here is a link to a cleaned up version. I do not fully understand publishing and republishing, so let me know if you cannot access it. Thanks!

What is also confusing is the import command to steal/borrow material from other notebooks. Cell names are defined as blocks most of the time. If I import a block from another notebook, is that equivalent from the “block” scenario in the code I provided?

Ah yes, it’s like I thought. I’ve added some comments below that can hopefully explain to you what is happening, and why:

LinePlotBlock = {
  // You're getting a reference to the DOM element
  // that is the value of your ContainerBlock cell.
  // This is still the exact same element that you
  // see in the previous cell above.
  const container = ContainerBlock
    
  const width = 500
  const height = 100

  // Each time you run this cell, you append a new
  // SVG element to your ContainerBlock element.
   const svg = d3
    .select(container)
    .append("svg")
    .attr("viewBox", `0 0 ${width} ${height}`);

Instead you need to create a new element:

const container = html`<div>`;

Or in “vanilla” Javascript, without Observable’s html helper:

const container = document.createElement("div");

Or with D3:

const container = d3.create("div");
// ...
const svg = container
  .append("svg")
  .attr("viewBox", `0 0 ${width} ${height}`);
// ...
return container.node();

Thanks again. But I am still confused. You wrote:

But if you look again, at ContainerBlock, the first few lines read:

Container = {
    const container = html`<div class="tooltip-demo">
    <style>
      div.tooltip-demo > div.tooltip {
        position: fixed;

So a new container is created following one of the three approaches you covered.

const container = html`...

is a constant variable whose value is redefined everytime the Container is called. So I do not understand how it is possible that the LinePlotBlock can even be invoked if I run the Container cell. There is absolutely nothing (that I see) in the Container cell that references anything within the LinePlotBlock cell. The const container in both cells should be completely independent.

I feel I am missing something very important. Thanks.

As I’ve mentioned earlier, cellName = {/* ... */} is not the same as cellName = function() {/* ... */}. Both run once, but in the first example the value that is returned within the block becomes the cell’s value, while for the latter the function is the returned value that becomes the cell’s value.

This notebook should explain Observable’s concept of cells and data flow more clearly:

You can also take a look at your notebook’s dependency graph here:

I don’t see a Container cell in your notebook, which means that you will probably have to republish your notebook, so that others can see the changes as well.

Thank you! I will study this carefully as these concepts seem very important. I have republished my notebook. It would be nice if notebooks could be “frozen” somehow. There is Container cell. Instead I have two cell names: ContainerBlock and ContainerFunction to make things clearer. I marked the problem solved.

Gordon

A few more examples to build your intuition.

These two examples are equivalent:

name = "foo"
name = {
  return "foo";
}

We can use the string value in other cells:

"Hello, my is " + name

Functions as value

Again, these two are equivalent:

name = function() {
  return "foo";
}
name = {
  return function() {
    return "foo";
  };
}

But since the cell value is now a function, we have to call the function in order to obtain the returned string:

"Hello, my is " + name()

References

In Javascript, objects are passed around by reference. An object is basically everything that’s not a string, number or boolean.

So when you assign the ContainerBlock cell value (the DOM element) to a variable, you’re not assigning a new copy, but a reference to the exact same DOM element.

1 Like

Hi,. I think a simpler distinction between blockmand function is that blocks evaluate immediately when created, while functions are not. Functions will lead to less issues if I am seeking to modularize code. A function can take on a different value each time it is called (evaluation only occurs at call time) if it’s parameters change. A block always has an associated value, which is the same over the entire notebook. A block is functionally equivalent to a function with no arguments, at least if only using let and const to declare variables.

Does themabove sound right?

I recommend that you forget about “blocks”, and instead only think in terms of “cells” and “cell values”. What value you assign to a cell (i.e. return in it) is completely up to you.

If you want to create a reusable API and/or abstractions, then yes, functions are the way to go.

However, for quick explorations it is often enough to process a cell’s input values “on the fly” without going the extra step of defining a function in another cell first.

Note that authors can still import your notebook’s cells and override some of them with their own implementations. E.g., given a notebook “@johndoe/my-chart” with two cells “chart” and “data”, an author can provide their own data by importing like this:

import {chart} with {myData as data} from '@johndoe/my-chart'

where “myData” is a cell that is defined in the importing notebook.

You might also be interested in this discussion of various strategies for reusability:

Thanks. I have started along the suggested direction.