🏠 back to Observable

How to increment a global array element from within a nested data().enter()?


#1

What is the best way to change array elements by clicking on svg elements?

For an example of the desired behaviour, please see Martien » Button: I need a way to click on some svg element (a square, or a circle. say) and count the number of times I’ve clicked on it in a separate data structure (that I want to save as JSON later in order to load it back in some day).

In drawSquares(), I now increment a global array’s element. This works fine, because I know the array’s name and structure. However, when I’m in a nested array structure from a selectAll().data().enter(), how can I best get at the proper array element and increment it?

I’m poking in the area of data() and datum(), but still cannot get my head around it. There must be an elegant way to do that, right?

I’ve read and grasped Loops in Observable, but to no avail.

Any help or pointers very much appreciated.


#2

It sounds like you want a view—a user input, say rendered with SVG, that represents a value.

Here’s a button that counts the number of times it has been clicked:

viewof count = {
  let value = 0;
  const button = html`<button>Click me`;
  Object.defineProperty(button, "value", {get() { return value; }});
  button.onclick = () => ++value;
  return button;
}

(I used Object.defineProperty because if you just assign to button.value directly, it unhelpfully converts the number value into a string.)

Similarly, here’s a little DIV that cycles through some colors:

viewof color = {
  let i = 0;
  const div = html`<div style="width:33px;height:33px;">`;
  div.style.background = div.value = colors[i];
  div.onclick = () => {
    div.style.background = div.value = colors[i = (i + 1) % colors.length];
    div.dispatchEvent(new CustomEvent("input"));
  };
  return div;
}

If you want multiple buttons, you can do that, too. You can use D3 to do this, but I think it’s probably simpler if you stick with HTML template literals.

viewof color3 = {
  let value = [0, 0, 0];
  const view = html`${value.map((v, i) => {
    const div = html`<div style="display:inline-block;width:26px;height:26px;margin-right:4px;"></div>`;
    div.style.background = colors[v];
    div.onclick = () => {
      div.style.background = colors[value[i] = v = (v + 1) % colors.length];
      view.dispatchEvent(new CustomEvent("input"));
    };
    return div;
  })}`;
  view.value = value;
  return view;
}

Live examples:

Other suggested notebooks:


#3

Thanks @mike.

Your Buttons example color3 is most intriguing. I need some time to fully grasp the ${value.map((v, i) and colors[value[i] = v = (v + 1) % colors.length].

My true need is to have a field of those buttons like in X-Matrix: each cell is clickable. Clicking on a cell changes the connectedness between the two orthogonal items and cycles through none, weak, and strong.

In addition to that, the user must also be able to save and later load the whole x-matrix, including the current state of the clickers.

As you can see, I’m struggling with that in drawClickers().

How to best approach this?


#4

Let me break down the HTML construction part.

Here’s an HTML literal:

html`<h1>Hello, world!</h1>`

Here’s a (contrived) example of embedding a DOM element inside an HTML literal:

{
  const element = html`<i>world</i>`;
  return html`<h1>Hello, ${element}</h1>`;
}

Putting an array of DOM elements inside an HTML literal:

{
  const e0 = html`<i>0</i>`;
  const e1 = html`<i>1</i>`;
  const e2 = html`<i>2</i>`;
  const elements = [e0, e1, e2];
  return html`<h1>Hello, ${elements}</h1>`;
}

Dynamically generating an array of elements, and putting them inside an HTML literal:

{
  const numbers = [0, 1, 2];
  const elements = numbers.map(i => html`<i>${i}</i>`);
  return html`<h1>Hello, ${elements}</h1>`;
}

Doing it all in one go:

html`<h1>Hello, ${Array.from({length: 3}, (_, i) => html`<i>${i}</i>`)}</h1>`

#5

For a matrix, you can use nested array.map.

{
  const rows = [
    ["Apples", 21],
    ["Oranges", 13],
    ["Bananas", 42]
  ];
  return html`<table>${rows.map((row, i) => {
    return html`<tr>${row.map((value, j) => {
      return html`<td>${value}</td>`;
    })}</tr>`;
  })}</table>`;
}

Then you can use the indexes i and j on click to toggle the desired value.


#6

Thanks again Mike. Very insightful. Obliged.

I’ve copied your examples over to Martien » Buttons and played around with them a bit.

What I learned is:

  • putting an array into a string literal simply concatenates the entries as a single string,
  • you can nest string literals (like in the nested example.

One thing still puzzles me, though.

When I ‘dismantle’ the code inside your div.onlick in order to understand its workings, I can understand it, and the order in which things get interpreted. What I do not understand is that when I omit the v = a; statement, it stops showing the desired effect. It simply shows the v + 1 value, but does not change the entry in the originating array anymore.

This seems like a pattern used in various Observables, and maybe D3 code, and I wonder what the pattern is and what its name is.

Also, my mental model needs a jolt, since I still don’t get why changing the v results in the proper entry in color3[] to change, too. It’s the behaviour I want, but have trouble to understand and use effectively. A pointer to some background reading is very much appreciated.

My mental model is more like create some global array and change its values in functions and other places. I experience the model used here somewhat ‘inside out’.

viewof color3 = {
  let value = [0, 102, 0];
  const view = html`${value.map((v, i) => {
    const div = html`<div style="display:inline-block;width:26px;height:26px;margin-right:4px;"></div>`;
    div.style.background = colors[v % colors.length];
    div.onclick = () => {
      let a = (v + 1) % colors.length;
      v = a; // remove this, and it stops working. why?
      div.style.background = colors[(value[i] = a)];
      view.dispatchEvent(new CustomEvent("input"));
    };
    return div;
  })}`;
  view.value = value;
  return view;
}

One final remark.question: TMO, stuff like this is quite valuable in a tutorial or technique notebook, right?


#7

The behavior of embedded expressions shown above is specific to the html tagged template literal provided by the Observable standard library—it’s not a general property of template literals.

The default behavior of a template literal is to coerce each embedded expression to a string. So if you do this:

`${[0, 1, 2]}`

You get this:

"0,1,2"

Whereas when you use the html tagged template literal:

html`${[0, 1, 2]}`

It has special logic to deal with embedded expressions that are array values, and embeds each element in the array separately, rather than coercing the entire array to a string first. Thus, you get a text node without commas:

012

The html tagged template literal also has special logic to deal with embedded values that are DOM elements, inserting them into the generated DOM, rather than coercing the DOM elements to a string. That’s why you can say:

{
  const element = html`<i>world</i>`;
  return html`<h1>Hello, ${element}</h1>`;
}

And you get

<h1>Hello, <i>world</i></h1>

If the html tagged template literal behaved like a (untagged) template literal, then it’d be equivalent to:

{
  const element = html`<i>world</i>`;
  return html`<h1>Hello, ${element.toString()}</h1>`;
}

Which would give you:

<h1>Hello, [object HTMLElement]</h1>

So, because you can embed elements within the HTML literals, and likewise arrays of elements, you can generate HTML from data in a functional way using array.map.


#8

Your other question regarding the assignment:

v = a

If you remove this assignment, then the value of v never changes; it’s fixed based on the initial value array during the call to value.map. And so you compute the new color as v + 1, but since you didn’t increment v, a second click won’t return a new value.

Even though the code, inside div.onclick, assigns to the array value[i],

value[i] = a

This does not affect the value of v, which was captured in the value.map call. This behavior is intrinsic to JavaScript.


#9

Thanks very much Mike, for your quick and elaborate answer. I’ll study it carefully. I really appreciate that you take the time to answer it. Sometimes I get the feeling that my questions are basic questions about JavaScript and Observable and that I should have been able to find the answers myself. In other words, at times I’m unsure about wether to ask the question at all. Then again, Ill keep daring to ask.


#10

@mike, sorry to bother you with this again.

I’m stuck getting this to work in D3 and a D# example like view colors3 above would probably jumpstart me into the right direction.

When clicked, tt does cycle through the colors correctly, but the separate cell aap above only updates its values when refreshed (array opened or cell rerun).

Please enlighten me or point me to the proper docs on this. Thanks.

Maybe I’m missing ome fundamental JS or D3 knowledge…

What I have now is

viewof aap = {
  const sheet = d3
    .select(DOM.svg(width, height))
    .attr("viewBox", "0 0 10 10")
    .attr("preserveAspectRatio", "xMinYMin meet");

  let aap = [243, 32, 591, 3];
  let e = html`${sheet.node()}`;
  sheet
    .selectAll("_")
    .data(aap)
    .enter()
    .append("circle")
    .attr("fill", (d, i) => color(i))
    .attr("cx", (d, i) => 3 * i + 1)
    .attr("cy", 3)
    .attr("r", 1)
    .on("click", function(d, i, a) {
      console.log("d", d);
      console.log("aap[i]", aap[i]);
      return d3.select(this).attr("fill", `${color(++aap[i])}`);
    });
  e.value = aap;
  return e;

  function color(clicks) {
    return colors[clicks % colors.length];
  }
}

#11

You need to dispatch an input event if you want it to behave like a view.


#12

Thanks! It works. Much obliged.