How to build an associative graph using React + p5
Tags: react, graphical, javascript, p5
Published: 2024-05-12 19:21:23



## The idea
I've been working on a [chess opening database](https://fensterchess.com) in my copious spare time, and have begun experimenting with visualizations of said openings. One of those is a relationship graph (see above) that displays origin square to destination square of each opening move. 

## The Farmer in the Dell
Relationships (a type of association) can be found everywhere. In this post, I'll graph the relationships as described in a nursery rhyme called "The Farmer in the Dell".

>The farmer in the dell
The farmer in the dell
Hi-ho, the derry-o
The farmer in the dell

After establishing that there's a farmer that lives in a dell, a series of new characters are introduced via the verb "takes".

- The farmer takes a wife
- The wife takes the child
- The child takes the nurse
- The nurse takes the cow
- The cow takes the dog
- The dog takes the cat
- The cat takes the mouse
- The mouse takes the cheese
- The cheese stands alone

The word "takes" is way overloaded here, but that can be addressed later. From this list of relationships we can see that the farmer and the cheese have a single relation (terminal nodes in the graph to come), but all other characters have two.

## Graphing the characters
To display the relationships among the characters in the rhyme, we can start by arranging each in a circle.

![Our cast of characters](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/97uenplpmwheuspviw8w.png)

I'm using the JavaScript graphics library [p5](https://p5js.org/) inside a react component, like so:

```jsx
/* eslint-disable react/prop-types */
import { useRef, useEffect } from "react";
import p5 from "p5";

const characters = {
    farmer: {},
    wife: {},
    child: {},
    nurse: {},
    cow: {},
    dog: {},
    cat: {},
    mouse: {},
    cheese: {},
};

const Version1 = () => {
    const renderRef = useRef();

    useEffect(() => {
        let remove;

        new p5((p) => {
            remove = p.remove;
            p.setup = () => {
                const r = 250;
                const cast = Object.entries(characters);

                for (const [index, [key]] of Object.entries(cast)) {
                    const character = characters[key];
                    character.angle =
                        (p.TWO_PI / Object.keys(characters).length) * index;
                    character.location = [
                        r * p.sin(character.angle),
                        r * p.cos(character.angle),
                    ];
                }

                p.createCanvas(600, 600).parent(renderRef.current);
                p.background(150)
                //move 0,0 to the center of the canvas
                p.translate(p.width / 2, p.height / 2);
                p.ellipseMode(p.CENTER);
                p.textAlign(p.CENTER, p.CENTER);
                p.textFont("Georgia");

                for (const c in characters) {
                    const [x, y] = characters[c].location;
                    p.ellipse(x, y, 40);
                    p.text(c, x, y);
                }
            };
        });

        return remove;
    });

    return <div id="Version1" ref={renderRef}></div>;
};
```
Without delving too deep into p5, it's worth pointing out some features of this code.

### How to render graphics inside a React component
`    const renderRef = useRef();`
`    p.createCanvas(600, 600).parent(renderRef.current);`
`    return <div id="Version1" ref={renderRef}></div>;`

### useEffect() cleanup function
```jsx
    useEffect(() => {
        let remove;

        new p5((p) => {
            remove = p.remove;
        /* ... */
        });
        return remove;
   });
```
the p5 remove function 'cleans up' the previous render before useEffect is called again. 

### Setting up the character locations
```jsx
         p.setup = () => {
                const r = 250;
                const cast = Object.entries(characters);

                for (const [index, [key]] of Object.entries(cast)) { 
```
Note the two Object.entries() calls. This is done to get an index to calculate each character's angle from the center:
```jsx
     character.angle =
        (p.TWO_PI / Object.keys(characters).length) * index;
```

## Drawing the relationships
Now to draw a line from each taker to each taken character. First, I add a "takes" relation from farmer all the way down to cheese, then draw a line from the taker location to the taken location.
```js
const doRelations = () => {
    // for this version, all relations are the same.
    const keys = Object.keys(characters);
    keys.forEach((key, i) => {
        if (i + 1 < keys.length) {
            characters[key].takes = keys[i + 1];
        }
    });
};

/* ... */
for (const c in characters) {
    const character = characters[c];
    const [x, y] = character.location;

    if (character.takes) {
        const taken = characters[character.takes];
        const [x2, y2] = taken.location;
        p.line(x, y, x2, y2);
    }

    p.ellipse(x, y, 40);
    p.text(c, x, y);
}
```
The result:

![lines of relationships](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2uuseg78nr8mm4un3j02.png)

Not terribly interesting, but it's a start.

## Requirements analysis
Let's reexamine the word "takes". It's usage is very ambiguous and often nonsensical. Here's one attempt to disambiguate the rhyme:

- The farmer **_married_** a wife
- The wife **_married_** a farmer
- The wife **_adopts_** the child
- The wife **_employs_** the nurse
- The child **_needs_** the nurse
- The nurse **_cares for_** the child
- The farmer **_owns_** the cow
- The nurse **_milks_** the cow
- The farmer **_owns_** the dog
- The dog **_guards_** the cow
- The dog **_befriends_** the cat
- The cat **_befriends_** the dog
- The cat **_adopts_** the farmer
- The cat **_hunts_** the mouse
- The mouse **_eats_** the cheese

Let's see what the relationship graph looks like now:

![further relationships](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pmk8xtymd2m3ztx2tc5t.png)

Not bad, but now let's color-code the relationships and add a legend:

![relationships color-coded](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a9uan2jefdon3yobdegs.png)

## Drawing curves instead of lines
One problem with using lines is that in reciprocal relationships, such as "married", one line overwrites another, obscuring the former relationship. Bezier curves can be used instead of lines, and look better. Each bezier curve takes eight arguments, which are the (x,y) coordinates of:
1. the start point
1. the first control point
1. the second control point
1. the end point

What are these control points? Here's an illustration:

![bezier control points](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lejrrzql4cteparl1qdb.png)

The shape of the curve can be adjusted by moving the control points. In this case, I'll set the first control point to be halfway between the start location and the center of the graph, and similarly for the end point. 

```
    const cp1 = [x / 2, y / 2];
    const cp2 = [x2 / 2, y2 / 2];

    p.noFill();
    p.bezier(x, y, ...cp1, ...cp2, x2, y2);
```
The result is:

![curves instead of lines](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4eqoj2i5pwntjdfnxsby.png)

Though aesthetically better, the change didn't fix the overwrite problem. The control points can be adjusted for each character by adding a "fudge" factor to the control point equation:

```jsx
    const fudge = (v) => ((v*i*3)/40)

    const nx = x + fudge(x)
    const ny = y + fudge(y)
    const nx2 = x2 + fudge(x2)
    const ny2 = y2 + fudge(y2)

    i++;

    const cp1 = [nx / 2, ny / 2];
    const cp2 = [nx2 / 2, ny2 / 2];
```

Here, the variable **i** is a counter that is increased as each character in the rhyme is rendered. The final output is:

![further adjustment to curves](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mut2kdg0kt0i2k60ioko.png)

## Arrows? What about arrows?
One thing missing from the diagram is an indication of the direction of the relationship, e.g. "farmer owns dog" and not "dog owns farmer". However, there is no native support for arrowheads in p5, so it takes quite a bit of trigonometry to draw them...another post in itself. See links below for more info on the subject.

## References
A complete program can be found [here](https://github.com/JeffML/farmer-in-the-dell).

### Useful links:
[This example](https://editor.p5js.org/kevinhb92/sketches/zLDDLrg_) gets halfway to an arrow feature.

One thing is to have the tip of the arrow touch the edge of each character's circle, using the formula in the answer found [here](https://stackoverflow.com/questions/14096138/find-the-point-on-a-circle-with-given-center-point-radius-and-degree).