Embedding multiple cells from notebook into a React App

What I want to do:
embed a chart, and embed the controls for that chart, from an observable notebook, into a simple reactjs app.

What Happens:
The chart embeds fine, but the control (a slider, imported into the notebook from jeremy’s inputs notebook) renders as below.

This is what it looks like on my observablehq notebook:
@kchalas/calendar-heatmap-too

This is how the control renders on the react app (i tried a simple html file also, with the same results):

What I did try:
I tried with the ‘embed code’ from each of the two cells from my notebook.

I also tried the approach described in the section ‘Rendering Cells’ of jeremy’s guide on embedding notebooks - i have two named cells: ‘cal’ (the calendar chart, which renders fine when i just include one cell), and ‘dataSelector’, which is not working, and apparently i must not have understood the module/runtime behavior, as it reproduces the chart in the second div and renders the slider control as in the screenshot above.

/*
new Runtime().module(notebook, name => {
  if (name === "chart") {
    return new Inspector(document.querySelector("#chart"));
  }
});
*/
//is this wrong to return two inspector objects? i tried other ways as well....
new Runtime().module(notebook, name => {
  if (name === "cal") {
    console.log("AHA CAL!");
    return new Inspector(document.querySelector("#div1"));//i've tried with both custom div IDs, and the ID returned by the 'embed code' cell-menu option.
  }
if (name === "dataSelector") {
    console.log("AHA SELECT!");
    return new Inspector(document.querySelector("#div2"));
  }
});

But cant figure out what I am doing wrong. I am sure it’s something simple, i must be missing a key step - but all the examples relating to embedding show a single cell (.e.g: name===‘chart’) but i’ve not been able to find an example with a chart and its controls all embedded into an external app.

I look forward to any advice, thanks in anticipation!

References referred

I believe i have followed recommended steps here:
//how-to-embed-a-notebook-in-a-react-app

here:
//downloading-and-embedding-notebooks
and here:
//breakout

1 Like

Hi, welcome!

Try changing this:

to this:

if (name === "viewof dataSelector") {

Note that view elements are represented internally by two different cells, the one named viewof X which is typically an HTML element and the one named X which is its value. Something similar can be said for mutable cells as well. See Introduction to views / Observable | Observable and A Brief Introduction to Viewof / Observable | Observable.

2 Likes

Hi Bryan, thanks for that, works great.
it helped to render the select dropdown properly.
but i think i’ve still got something here that’s causing the chart to render twice…looking into it and cutting out all extra stuff to figure this out…again, would appreciate any help!

this is all i have in the html:

<div id="selector"></div>
<div id="ac-wrapper"></div>
//...and in the app.js
    const runtime = new Runtime();
    runtime.module(notebook, name => 
          (name === "viewof dataSelector") && Inspector.into("#selector")());
    const runtime2 = new Runtime();//I've tried with the same runtime object, but that fails to render the chart even once.
    runtime2.module(define, name => (name === "cal") && Inspector.into("#ac-wrapper")());

Yeah, I was puzzled by that in your initial screenshot as well. Could you show more of your app.js (or some minimized version)? Am I correct that the two calendars are both rendering into the ac-wrapper div? If so, then I’d guess that the code which is rendering the cal cell is being called twice; since I’m not so experienced with React, you may actually be more familiar with React lifecycle methods and all that, so I’d start by checking that. You could also try putting some kind of conditional which prevents the code from being called twice.

Another issue is that having two runtimes is not going to work out well since the cells in one won’t be able to communicate with cells in the other.

thanks again - ok, i will have a look at the render lifecycle. Yes, that is what is happening - the original notebook has just one div ‘calendar’, which is being added twice into ac-wrapper.

this is the entire App.js with the two runtimes as it stands now. i will go back to the docs and read up!

import React, {Component} from "react";
import {Runtime, Inspector} from "@observablehq/runtime";
import notebook from "@kchalas/calendar-heatmap-too";
class App extends Component {
  componentDidMount() {
    const runtime = new Runtime();
    runtime.module(notebook, name => (name === "viewof dataSelector") && Inspector.into("#selector")());
    const runtime2 = new Runtime();
    runtime2.module(notebook, name => (name === "cal") && Inspector.into("#ac-wrapper")());
  }
  render() {
    return (
      <div className="App"></div>
    );
  }
}

export default App;

The very first thing to try is to use a single runtime, instead of two:

import React, {Component} from "react";
import {Runtime, Inspector} from "@observablehq/runtime";
import notebook from "@kchalas/calendar-heatmap-too";
class App extends Component {
  componentDidMount() {
    const runtime = new Runtime();
    runtime.module(notebook, name => {
      if (name === "viewof dataSelector") {
        return Inspector.into("#selector")()
      }
      if (name === "cal") {
        return Inspector.into("#ac-wrapper")();
      }
    });
  }
  render() {
    return (
      <div className="App"></div>
    );
  }
}

export default App;

Hi
Thanks, I did try that, but then it just renders the first thing and not the second, no matter what order i have the ‘cal’ or ‘ac-wrapper’ conditions in, i just get the dataSelector, not the calendar.

Got the same issue today too and unfortunately was not able to solve it. It’s has if the return Inspector was getting us out of the React Component. But I also tried with two sub components pass the same Inspector instance to them and failed.

Small difference still I could render one or the other of the cell depending on which one was called first.

it doesn’t seem to leave the iteration loop…it finds the element/cell - but doesnt render it. i’m going to have to step away from staring at the code and will read the docs over the weekend. :slight_smile:

componentDidMount() {
    /*
    const runtime = new Runtime();
    runtime.module(notebook, name => (name === "viewof dataSelector") && Inspector.into("#selector")());
    const runtime2 = new Runtime();
    runtime2.module(notebook, name => (name === "cal") && Inspector.into("#ac-wrapper")());
    */
   const runtime = new Runtime();
   runtime.module(notebook, name => {
     console.log(name);
     if (name === "cal") {
        console.log("AHA CAL");
        return Inspector.into("#ac-wrapper")();
     }
     if (name === "viewof dataSelector") {
        console.log("AHA SELECTOR");
        return Inspector.into("#selector")()
     }
   
   });  
  };

the above outputs this to console (all the ‘name’ values, the names of each of my cells…):
viewof dataSelector
App.js:23 AHA SELECTOR
App.js:17 dataSelector
App.js:17 cal
App.js:19 AHA CAL
App.js:17 createelements
App.js:17 parsedata
App.js:17 drawchart
App.js:17 drawmultiyearview
App.js:17 drawyearview
App.js:17 drawmonthview
App.js:17 drawdayview
App.js:17 drawbutton
App.js:17 drawweekview
App.js:17 remyearview
App.js:17 remmultiyearview
App.js:17 remmonthview
App.js:17 remweekview
App.js:17 removedayview
App.js:17 hidetooltip
App.js:17 hidebackbtn
App.js:17 timeformatter
App.js:17 launcher
App.js:17 launchWithData
App.js:17 init
App.js:17 settings
App.js:17 calendarHeatmap
App.js:17 moment
App.js:17 d3

I took a closer look at your notebook’s code and I see one big issue:

The cal cell in itself does not contain any code; instead it’s acted upon by other functions in your notebook which instead perform queries to find the div. This is considered an anti-pattern in Observable.

Now it is clear why nothing appears to happens when you have the Inspector attempt to load the cal cell – the cal cell is being loaded but the cells that fill the calendar div with your chart are not being loaded.

Thus one quick workaround is to use the following:

import React, {Component} from "react";
import {Runtime, Inspector} from "@observablehq/runtime";
import notebook from "@kchalas/calendar-heatmap-too";
class App extends Component {
  componentDidMount() {
    const runtime = new Runtime();
    runtime.module(notebook, name => {
      if (name === "viewof dataSelector") {
        return Inspector.into("#selector")()
      }
      if (name === "cal") {
        debugger;
        return Inspector.into("#ac-wrapper")();
      }
      else return true; // this ensures that all notebook cells are actually run
    });
  }
  render() {
    return (
      <div className="App">
        <div id="selector"></div>
        <div id="ac-wrapper"></div>
      </div>
    );
  }
}

export default App;

I’m testing with the create-react-app procedure described in @jashkenas’s react tutorial, so my index.html file has only one root div like this:

  <body>
    <div id="root"></div>
  </body>

I would recommend however rewriting your notebook – I may try to make some suggestions later tonight.

Edit The above solution doesn’t quite work yet; upon changing the select element, additional copies of the calendar are appended to the ac-wrapper div. This is also related to the overall architecture of your notebook.

Edit 2 Oh, I guess that’s how your notebook currently behaves on observablehq.com too. Nonetheless, this seems unintended and I’ll see if I can address this as well.

1 Like

These notebooks make extensive use of side-effects (for example: mutating the calendarHeatmap object, modifying the contents of the container element defined in another cell, assigning an onresize listener to the window). I’m afraid you are going to have a hard time getting this to work reliably without some significant refactoring, although return true to force the evaluation of side-effect cells is a reasonable workaround.

Perhaps we could provide some better examples to guide you here.

I imagine you’ve already seen the D3 Calendar View?

If you want to support drilldown into particular weeks or days, one example to consider is the hierarchical bar chart:

Probably the key design principle for the hierarchical bar chart is that the chart cell contains the state, and calls out to functions to initiate the desired transitions in response to user events. And so there’s no “uncontrolled” mutation : the chart cell controls what happens.

That’s what I would recommend for your calendar heatmap, too, though you could of course design it as a class rather than a set of functions if you prefer. But in either case I’d recommend keeping the state on an instance variable (this, or a local variable), rather than mutating a global (such as the calendarHeatmap object).

If there are more examples we could provide here I would be happy to write them!

2 Likes

Probably not related but I got this code working

  componentDidMount() {
    runtime.module(define, name => {
      if (name === "controles") {
        return Inspector.into("#controles")();
      }
      if (name === "chart-user-form-point") {
        return Inspector.into("#chart-user-form-point")();
      }
    });
  }

I remembered falsly reading something about Inspector falling back to the element with the same id as the cell name. you may have tried that because my code is other ways very similar to yours.

You could try embeding the ‘viewof cell’ in an html cell too which you’ll give a name that will be the same as the id ref.

The Inspector doesn’t have any such magic. You can either point the Inspector at a specific DOM element for a specific cell using the Inspector constructor or you can use Inspector.into to send all variables into the given container, such as the document.body.

Sorry for misleading…

It’s all good @maliky! No apology needed.

@bgchen thank you again for the fix (return true did the job for getting the chart displayed)! I hadn’t noticed the additional renderings occured in the source notebook - this does not happen in my first version before i forked it. i should be able to figure that out.

@mbostock thanks for your detailed suggestions as always, I will set about doing some refactoring to use this .

clearly lots to learn, i’m just starting out with observable, but it’s all amazing.

1 Like

in my new version, I had no svg initially, and my createElements kept appending a new one everytime. sorted it out now.

Glad you were able to make some progress. I think @mbostock has given a much better set of tips than I would be able to. Feel free to ask again if there’s something else you get stuck on!