Circle Connection Diagram

Hello! Wondering if anyone would have recommendations or resources for making a circular diagram containing nodes with connections linked across to other nodes. This would be pulling data from a .csv file. What I am looking for is sometimes called a ‘non-ribbon chord diagram’ or ‘hierarchical edge bundling’.

My data is set up as follows… I have 4 questions that were asked throughout the day for a multitude of days. (i.e. What are you doing? Who are you with? Where are you? Are you doing what you want to be doing?) I would like to connect the answers (mostly string, one boolean) to each other with a line or bezier curve.

This is my inspiration image.

Any help is appreciated,
Thanks -K

1 Like

Sample sketch follows:

class Point {
  String t;
  float a;
  Point(int i){
    a = i * TWO_PI/54 + (random(1) < 0.5? 0:PI); //random(TWO_PI);
    t = "" + char('A'+i);
  }
  void draw(){
    pushMatrix();
    rotate(a);
    text(t, 220, 0 );
    ellipse(200,0,10,10);
    popMatrix();
  }
}

float atox(float a){
  return( 200 * cos( a ) );
}

float atoy(float a){
  return( 200 * sin( a ) );
}


Point[] points = new Point[26];

void setup(){
  size(600,600);
  textSize(28);
  for(int i = 0; i < 26; i++) points[i] = new Point(i);
}

void draw(){
  background(0);
  translate(width/2, height/2);
  for(int i = 0; i < 26; i++){
    points[i].draw();
  }
  stroke(255);
  for(int i = 0; i < 25; i++){
    line( atox( points[i].a ),   atoy( points[i].a ),
          atox( points[i+1].a ), atoy( points[i+1].a ) );
  }
}

I don’t even know how helpful this actually is. It’s certainly possible to do this.

2 Likes

Hi,

Before thinking on how to do the graph, I think the first step is to process your data to represent the connections between the nodes.

The output should be a diagonal matrix with all the nodes in the rows and columns and each cell containing the weight of the connection between those 2 nodes.

When that’s done, you can start thinking on how to draw the graph. I had made something that looks similar in a past project:

It is not completely the same but you still have the same ideas going on. Hopefully you might find interesting bit of code to help you:

final float diameter = 700; //the diameter of the circle //<>//

void setup() {
  size(1000, 1000); 
  background(0);
  translate(height/2, width/2);
  noFill();
  stroke(255);
  textAlign(CENTER, CENTER);
  strokeWeight(5);

  //Variables
  String dataString; //all the PI number as string
  int[] digit; //All the digit of PI
  int[] digitCount = new int[10]; //The amount of each deach from 0 to 9
  float[] digitWeight = new float[10]; //The proportion of each digit from 0 to 9
  final float angleGap = 0.03; //The gap between each arc
  float[] digitAngleSize = new float[10]; //The size of each digit arc 
  float currentAngle; //Use to keep track of the last arc drawn on screen in order the draw the next one after
  PFont myFont; //The font used for the text
  color[] digitColor = new color[10]; //The color of each digit
  float[] currentDigitAngle = new float[10]; //Keep track of the last angle used for each digit (each time a line goes to the digit, the line is offset)
  float[] digitAngleStep = new float[10]; //Store the step to add every time the line goes to a digit

  //Initialize variables
  myFont = createFont("data/Comfortaa-Regular.ttf", 40);
  textFont(myFont);
  textSize(diameter / 30);
  text("PI", 0, 0);

  digitColor[0] = color(236, 166, 15);
  digitColor[1] = color(232, 125, 27);
  digitColor[2] = color(227, 45, 42);
  digitColor[3] = color(210, 0, 64);
  digitColor[4] = color(168, 11, 95);
  digitColor[5] = color(135, 46, 134);
  digitColor[6] = color(91, 77, 163);
  digitColor[7] = color(46, 116, 157);
  digitColor[8] = color(30, 154, 119);
  digitColor[9] = color(91, 176, 90);


  //Load data from string
  dataString = loadStrings("Data/PI_1000_digits.txt")[0];

  //Transform data to integer and get count
  digit = new int[dataString.length()];

  for (int i = 0; i < dataString.length(); i++) {
    digit[i] = Character.getNumericValue(dataString.charAt(i));
    digitCount[digit[i]]++;
  }


  //Determine angle step
  //digitAngleStep = (TWO_PI - (10 * angleGap)) / (digit.length - 9);


  //Find weight and angle
  for (int i = 0; i < 10; i++) {
    digitWeight[i] = (float)digitCount[i] / digit.length;
    digitAngleSize[i] = digitWeight[i] * (TWO_PI - (10 * angleGap));
    if (digitCount[i] == 1) {
      digitAngleStep[i] = 0;
    } else {
      digitAngleStep[i] = digitAngleSize[i] / (digitCount[i] - 1);
    }
  }


  //Draw arcs and text
  currentAngle = (3 * PI / 2) - (digitAngleSize[0] / 2);
  for (int i = 0; i < 10; i++) {
    //Arcs
    stroke(red(digitColor[i]), green(digitColor[i]), blue(digitColor[i]));
    noFill();
    arc(0, 0, diameter, diameter, currentAngle, currentAngle+digitAngleSize[i]);
    currentDigitAngle[i] = currentAngle;

    //Text
    fill(red(digitColor[i]), green(digitColor[i]), blue(digitColor[i]));
    text(i, 1.1 * (diameter / 2) * cos((2*currentAngle+digitAngleSize[i])/2), 1.1 * (diameter/2) * sin((2*currentAngle+digitAngleSize[i])/2));

    currentAngle = currentAngle + digitAngleSize[i] + angleGap;
  }


  //Draw bezier curves
  for (int i = 0; i < digit.length - 1; i++) {
    drawBezier(currentDigitAngle[digit[i]], currentDigitAngle[digit[i+1]], digitColor[digit[i]]);
    currentDigitAngle[digit[i]] += digitAngleStep[digit[i]];
  }
}


void drawBezier(float angle1, float angle2, color strokeColor) {
  float x1, y1, x2, y2, r, controlAngle1, controlAngle2, controlR;

  r = (diameter / 2) - 3;

  x1 = r * cos(angle1);
  y1 = r * sin(angle1);
  x2 = r * cos(angle2);
  y2 = r * sin(angle2);

  noFill();
  stroke(red(strokeColor), green(strokeColor), blue(strokeColor), 50);
  strokeWeight(2);

  if (angleDiff(x1, y1, x2, y2, 0, 0) < 0.3) {
    controlAngle1 = getAngle(x1, y1, 0, 0) + 0.5;
    controlAngle2 = getAngle(x2, y2, 0, 0) - 0.5;
    controlR = 0.9 * dist(x1, y1, 0, 0);
    bezier(x1, y1, x1 + controlR * cos(controlAngle1), y1 + controlR * sin(controlAngle1), x2 + controlR * cos(controlAngle2), y2 + controlR * sin(controlAngle2), x2, y2);
  } else {
    bezier(x1, y1, x1/2, y1/2, x2/2, y2/2, x2, y2);
  }
}





//Get angle from point 1 to point 2
float getAngle(float x1, float y1, float x2, float y2) {
  float angle;

  if (x2==x1) {
    if (y1>y2) {
      return HALF_PI;
    } else {
      return -HALF_PI;
    }
  }

  angle = atan((y2-y1)/(x2-x1));
  if (x1 > x2) {
    return angle + PI;
  } else {
    return angle;
  }
}


//Angle difference between pt1-2 and p1-3
float angleDiff(float x1, float y1, float x2, float y2, float x3, float y3) {
  float a1, a2, aDiff;
  a1 = getAngle(x1, y1, x2, y2);
  a2 = getAngle(x1, y1, x3, y3);

  aDiff = abs(a1 - a2);
  if (aDiff > PI) {
    if (a1>a2) {
      while (a1>a2) {
        a1 = a1 - TWO_PI;
      }
    } else {
      while (a1<a2) {
        a1 = a1 + TWO_PI;
      }
    }
  }
  aDiff = abs(a1 - a2);

  return aDiff;
}
6 Likes

Generative Design’s chapter on Sunburst Trees could also be a useful resource.

Sketch code is available here

3 Likes

or just post the first 19 lines of your csv…

I have 4 questions that were asked throughout the day for a multitude of days.

How exactly do you think of the circle layout?

In your image look at the circumference of the circle please; there is text.

Do you imagine your graphic as :

Day 1 : What are you doing?
Day 1 : Who are you with?
Day 1 : Where are you?
Day 1 : Are you doing what you want to be doing?
Day 2 : What are you doing?
Day 2 : Who are you with?
Day 2 : Where are you?
Day 2 : Are you doing what you want to be doing?
Day 3 : What are you doing?
Day 3 : Who are you with?
Day 3 : Where are you?
Day 3 : Are you doing what you want to be doing?

and this for 360 degree around the circle?

Or do you plan to cluster all questions? Or show all answers?

Chrisir

My plan was to show all answers in 4 category areas (clustered based on questions).
Example of answers:
What are you doing?: eating, watching Netflix, taking a walk, hanging out
Who are you with: Lisa, Joan, Mom
Where are you: Home, Work, Transporting
Are you doing what you want to be doing: Boolean- Yes or No

Ex:
Day 1 : eating, hanging out, Mom, Dad, Bob, Home, Yes
Day 2 : working, coworkers, Work, No
Day 3 : eating, chatting, coworkers, Work, Yes

I would like nodes for each string answer (clustered by question) and then linked to other nodes that were answered at the same time. Yes, that means across clusters/categories, but also within the same cluster/category (showing they were being done at the same time).

Hope this explanation helps.

Then follow jb4x’s advice

work step by step :

  • make a circle (see trig tutorial)

  • load data

  • text out data around the circle (using pushMatrix() rotate() text() ....)

  • draw lines

Is there a particular reason that you want to use Processing to write this component, and not adapting the existing one in d3? For example, are you trying to integrate it with other things, like camera control or generative audio?

d3 has an existing “hierarchical edge bundling” block here:

There is a Vega wrapper for it:

…and it runs in Observable notebooks: