Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Complex circle diagram

I have spent loads of time trying to replicate the attached diagram in SVG/D3.js. The closest I have come is by using the code attached. I have explored using the tree function but can only create one band of circles.

The issue with the attached code is it will be very hard to get it looking smooth and smart by manually typing which pixel coordinates each circle must sit.

For info, the dataset will do nothing more than colour these circles, so it can be entirely static in terms of shape. The code just needs to generate the shape, and I can then drive the colour based on the dataset.

image of what I am trying to create:

enter image description here

This is my code:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>D3 Test</title>
  <style type="text/css">

  </style>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
</head>

<body>
  <script type="text/javascript">
    //Width and height
    var w = 1000;
    var h = 1000;
    var dataset = [6, 2, 5, 4, 5, 5, 5, 5, 3, 4, 5, 6];
    var svg = d3.select("body")
      .append("svg")
      .attr("width", w)
      .attr("height", h);
    var circles = svg.selectAll("circle")
      .data(dataset)
      .enter()
      .append("circle");


    circles.attr("cx", function(d, i) {
      var Xaxis;
      if (i === 0) {
        Xaxis = "500";
      } else if (i === 1) {
        Xaxis = "400";
      } else if (i === 2) {
        Xaxis = "420";
      } else if (i === 3) {
        Xaxis = "452.5";
      } else if (i === 4) {
        Xaxis = "485";
      } else if (i === 5) {
        Xaxis = "515";
      } else if (i === 6) {
        Xaxis = "547.5";
      } else if (i === 7) {
        Xaxis = "580";
      } else if (i === 8) {
        Xaxis = "600";
      } else if (i === 9) {
        Xaxis = "600";
      } else if (i === 10) {
        Xaxis = "650";
      } else if (i === 11) {
        Xaxis = "700";
      } else if (i === 12) {
        Xaxis = "750";
      } else if (i === 13) {
        Xaxis = "750";
      } else if (i === 14) {
        Xaxis = "750";
      } else if (i === 15) {
        Xaxis = "750";
      } else if (i === 16) {
        Xaxis = "750";
      }
      return Xaxis;
    })
    circles.attr("cy", function(d, i) {
        var Yaxis;
        if (i === 0) {
          Yaxis = "500";
        } else if (i === 1) {
          Yaxis = "500";
        } else if (i === 2) {
          Yaxis = "535";
        } else if (i === 3) {
          Yaxis = "560";
        } else if (i === 4) {
          Yaxis = "585";
        } else if (i === 5) {
          Yaxis = "585";
        } else if (i === 6) {
          Yaxis = "560";
        } else if (i === 7) {
          Yaxis = "535";
        } else if (i === 8) {
          Yaxis = "500";
        } else if (i === 9) {
          Yaxis = "600";
        } else if (i === 10) {
          Yaxis = "550";
        } else if (i === 11) {
          Yaxis = "500";
        } else if (i === 12) {
          Yaxis = "450";
        } else if (i === 13) {
          Yaxis = "600";
        } else if (i === 14) {
          Yaxis = "550";
        } else if (i === 15) {
          Yaxis = "500";
        } else if (i === 16) {
          Yaxis = "450";
        }
        return Yaxis;
      })
      .attr("r", function(d, i) {
        var size;
        if (i === 0) {
          size = "30";
        } else if (i > 0) {
          size = "20";
        }
        return size;
      })
      .attr("fill", function(d, i) {
        var returnColor;
        if (d === 1) {
          returnColor = "green";
        } else if (d === 2) {
          returnColor = "lightgreen";
        } else if (d === 3) {
          returnColor = "gold";
        } else if (d === 4) {
          returnColor = "darkorange";
        } else if (d === 5) {
          returnColor = "red";
        } else if (d === 6) {
          returnColor = "lightgrey";
        }
        return returnColor;
      });
  </script>

</body>

</html>
like image 884
JNPer Avatar asked Dec 18 '22 12:12

JNPer


2 Answers

You can use some trigonometry to positioning your circles. This is what you need:

To positioning the circle in the x axis:

x-centre + (distance * sin(angle))

And to positioning it in the y axis:

y-centre + (distance * cos(angle))

Where distance is the distance of the circles from the center and angle is the angle in radians. To calculate it, use:

rad = deg * Math.PI/180;

In the following snippet, I'm using groups to draw the layers of circles, and calculating the angle using the index of the circle in each group. I'm setting the number of circles using d3.range() and the number of layers in the same array that sets the distance from the centre. Check it:

var width = height = 500;
var svg = d3.select("body")
  .append("svg")
  .attr("width", width)
  .attr("height", height);

var color = d3.scaleOrdinal(d3.schemeCategory10)
  .domain(d3.range(16));

var data1 = d3.range(16);
var dataRadius = [70, 110, 150, 190, 230];

svg.append("circle").attr("cx", width / 2)
  .attr("cy", height / 2)
  .attr("r", 20)
  .attr("fill", "yellow");

var groups = svg.selectAll(".groups")
  .data(dataRadius)
  .enter()
  .append("g");

var circles = groups.selectAll(".circles")
  .data(data1)
  .enter()
  .append("circle");

circles.attr("cx", function(d, i) {
    var radius = this.parentNode.__data__;
    return width / 2 + (radius * Math.sin(i * (360 / (data1.length) * Math.PI / 180)))
  })
  .attr("cy", function(d, i) {
    var radius = this.parentNode.__data__;
    return height / 2 + (radius * Math.cos(i * (360 / (data1.length) * Math.PI / 180)))
  })
  .attr("r", function() {
    return this.parentNode.__data__ == 230 ? 24 : 14
  })
  .attr("fill", function(d, i) {
    return i == 13 || i == 14 ? color(i - 2) : "#ccc"
  });
<script src="https://d3js.org/d3.v4.min.js"></script>
like image 164
Gerardo Furtado Avatar answered Dec 30 '22 13:12

Gerardo Furtado


Prelude

Although this is a year old I would like to add my own two cents providing an entirely different approach as the one proposed by Gerardo. Every now and then I have advocated the use of the mighty SVG transformations available throught the transform attribute. Whatsoever, this mostly passes unnoticed for reasons that elude me. One such example is my answer to "SVG marker - can I set length and angle?". Most developers seem to prefer doing trigonometry on their own although I consider my solution less cumbersome and the resulting code much more clean and elegant as well as much more intuitively accessible.

To fully understand what is going on I encourage anybody not yet familiar with the topic to read and re-read the outstanding 3-part series on SVG coordinate systems and transformations by Sara Soueidan.


The approach I am going to present offloads all trigonometry to the browser whereby allowing for verbatim SVG code. If you did calculate the circles' positions on your own you would be left with coordinates like this:

<circle cx="314.67156727579004" cy="276.78784026555627"></circle>

The same circle could be generated with the following lines:

<g transform="rotate(30)">
  <circle transform="translate(70)"></circle>
</g>

Note, that some irrelevant attributes (r, fill) have been stripped from both snippets to just focus on the positioning. The latter is much more clear on what the developer wants to achieve:

  1. Rotate the coordinate system by 30 degrees.
  2. Translate the coordinate system by 70 pixels in the direction of the rotated x axis.
  3. Draw the circle at that new origin.

There is not even the need to have cx and cy attributes as these will default to 0 allowing us start off from the origin and later offset the entire graphic by wrapping it in another group translating it to whereever it seems fit. The resulting SVG will be structured like follows:

enter image description here

Have a look at the following snippet for a working demo:

const svg = d3.select("svg>g");

const g = svg.selectAll(null)
  .data(d3.range(30, 361, 30))
  .enter().append("g")
    .attr("transform", d => `rotate(${d})`);
    
g.selectAll(null)
  .data(d3.range(40, 171, 30))
  .enter().append("circle")
    .attr("transform", d => `translate(${d})`)
    .attr("r", (d, i) => i === 4 ? 15 : 10)
    .attr("fill", "#ccc");
  
<script src="https://d3js.org/d3.v4.js"></script>
<svg width="600" height="600">
  <g transform="translate(200,200)">
  </g>
</svg>
like image 24
altocumulus Avatar answered Dec 30 '22 13:12

altocumulus