Place the inputs side-by-side with a plot

Hi,

I generated a plot from inputs, but because the number of inputs are too large to fit in a common screen, users will have to scroll up and down to change inputs then check the plot. So I’d prefer to place the inputs on the left of the plot. The plot cheatsheet utilities did something like this with the createDemo function. But since my inputs are dependent to each other (e.g. the range of a slider depends on the selection in another input), it won’t work. For example in Untitled / Serge-Étienne Parent / Observable, is there a way to put an input block asides a plot in a html table?

Thanks!

I’m afraid you’ll have to forgoe Observable’s reactivity for this and handle the updates yourself. Here is an example:

{
  // Placeholder text node for the dynamic label part.
  const label = document.createTextNode('');
  // Combine both inputs into a single "view" to make our life easier.
  const inputs = Inputs.form({
    stroke_channel: Inputs.select(["sex", "sport"], {label: "Stroke channel"}),
    size_channel: Inputs.range([0, 10], {label: html`Dot size for ${label}`}),
  });
  
  // Prepare a placeholder for the plot.
  const container = html`<div><span>`;
  // This function receives the updated value and rerenders the plot, replacing the previous instance.
  const update = ({stroke_channel, size_channel}) => {
    // Update the label placeholder.
    label.nodeValue = inputs.value.stroke_channel;
    // Update the plot.
    const plot = Plot.dot(athletes, {x: "weight", y: "height", stroke: stroke_channel, r: size_channel}).plot();
    container.firstElementChild.replaceWith(plot);
  };

  // Update the plot when the value changes.
  inputs.oninput = () => update(inputs.value);
  // Render the initial plot.
  update(inputs.value);
  // Add some spacing and return the combined elements.
  inputs.style.paddingRight = '20px';
  return html`<div style="display:flex">${inputs}${container}`;
}

Another option would be to use a cell that produces “side effects”, i.e., that changes the output of other cells. In this scenario you would reparent both form inputs, and then inject the plot from another cell:

group = html`<div style="display:flex">
  <div style="padding-right:20px">
    ${viewof stroke_channel}
    ${viewof size_channel}
  </div>
  <div class="plot-target"><span /></div>
</div>`
group.querySelector('.plot-target').firstElementChild.replaceWith(
  Plot.dot(athletes, {x: "weight", y: "height", stroke: stroke_channel, r: size_channel}).plot()
)

However, using side effects is generally discouraged because they can break Observable’s reactivity in unexpected ways. Use them only if there is really no other option.

This is a great way to do it. But in my case, it will be recursive since the plotted data set is fetched from a url generated from the inputs. I think the best option is not to overthink the problem, leave the inputs at the top while stacking them with inputsGroup. Thanks for your time, I’m still learning a lot from your code. :pray:

1 Like

I totally forgot about a notebook that might help you with your problem:

And here’s how you’d use it:

  1. Import the helper:
    import {dashboard} from '@mootari/dashboard-with-inputs'
    
  2. Declare the display with two regions “left” and “right”:
    viewof display = dashboard(html`<div style="display:flex">
      <div data-region="left" style="padding-right:20px"></div>
      <div data-region="right"></div>
    `)
    
  3. Create your inputs and render them to the “left” region:
    stroke_channel = {
      const input = Inputs.select(["sex", "sport"], {label: "Stroke channel"});
      display('left', input, invalidation);
      return Generators.input(input);
    }
    
    size_channel = {
      const input = Inputs.range([0, 10], {label: "Dot size for " + stroke_channel});
      display('left', input, invalidation);
      return Generators.input(input);
    }
    
  4. Lastly, render the plot into the “right” region:
    display(
      'right',
      Plot.dot(athletes, {x: "weight", y: "height", stroke: stroke_channel, r: size_channel}).plot(),
      invalidation
    )
    

This will fully preserve Observable’s reactivity. Because you pass each cell’s invalidation promise, the dashboard helper will ensure that the element is properly removed before the new instance gets attached.

2 Likes