Thanks all for reading. I have been able to solve the problem after 1 more sleepless night :)

I did skip the "g" elements, as they were not really necessary, trying to get a structure like this:

<line dp-1></line>
<circle dp-1></circle>
<line dp-2></line>
<circle dp-2></line>

Doing this I could use two separate data bindings and data arrays (which is needed to use the force simulation). To achieve the correcting sorting (line - circle - line - circle - ...) i used "d3.insert" instead of append with a dynamically computed "before" element. Below are the most important parts of the code. Hope this helps someone eventually.


// Drawing the data ...
public update() {
    function (d: any) { return d.category }
    enter => enter.append("circle")
      .style('opacity', 0)

if (this.settings.labels.showLinks && this.settings.labels.showLinks) {
      enter => enter.insert('line', (d, i) => {
        console.log("JOIN", d)
        return document.getElementById('dP_' +
        // .style('opacity', 0)

   * Draws the data circles ...
   * @param circle 
  private drawData = (circle) => {
  .attr("id", (d, i) => { return 'dP_' + d.category })
  .attr("class", "dataPoint")
  .style("fill", d => this.color(d.color))
  .style("stroke-width", this.settings.dataPoints.stroke.width)
  .style("stroke-opacity", this.settings.dataPoints.stroke.opacity)
  .style("stroke", this.settings.dataPoints.stroke.color)
  .style('opacity', 1)
  .attr("r", d => this.rScale()(d.r))
  .attr("cx", d => this.xScale()(d.x))
  .attr("cy", d => this.yScale()(d.y))

   * draws the lines to connect labels to data points
   * @param g 
private drawLabelLine = (line) => {
  .attr("class", "label-link")
  .attr("stroke", this.settings.labels.linkStroke.color)
  .attr("stroke-width", this.settings.labels.linkStroke.width)
  .attr("opacity", this.settings.labels.linkStroke.opacity)

// adding and doing the force simulation ...
if (this.settings.labels.force) {
    .force("charge", d3.forceManyBody().strength(this.settings.labels.chargeStrength))
    .force("link", d3.forceLink(forceData.links)
    .on("tick", ticked);


Since you didn't include your data, I can give you a high-level conceptual way of solving it on the data side:

Essentially, merging the 3 arrays into one object grouped by the 'color' property (could be any property) using reduce. Then append each circle, line and text into each 'g' element we create for each color.

Note: the links array is pointless to have x1 and x2 and y1 and y2 values as we can get them from the circles and labels arrays. Also, if possible, you could just define your data like my combinedData from the start.

const circles = [
  {shape: "circle", color: "green", x: 2, y: 2, r: 0.5},
  {shape: "circle", color: "blue", x: 4, y: 4, r: 1},
  {shape: "circle", color: "red", x: 8, y: 8, r: 1.5},
const links = [
  {shape: "line", color: "green", x1: 2, y1: 2, x2: 1, y2: 1},
  {shape: "line", color: "blue", x1: 4, y1: 4, x2: 2, y2: 6},
  {shape: "line", color: "red", x1: 8, y1: 8, x2: 9, y2: 4},
const labels = [
  {shape: "text", color: "green", x: 1, y: 1, text: "A"},
  {shape: "text", color: "blue", x: 2, y: 6, text: "B"},
  {shape: "text", color: "red", x: 9, y: 4, text: "C"},

const combinedData = [...circles, ...links, ...labels].reduce((aggObj, item) => {
  if (!aggObj[item.color]) aggObj[item.color] = {};
  aggObj[item.color][item.shape] = item;
  return aggObj;
}, {});


const groups ='svg').selectAll('g')
  .attr('class', ([k,v]) => k);
    .attr('fill', ([k,v]) =>
    .attr('r', ([k,v]) =>
    .attr('cx', ([k,v]) =>
    .attr('cy', ([k,v]) =>
    .attr('stroke', ([k,v]) => v.line.color)
    .attr('stroke-width', 0.1)
    .attr('x1', ([k,v]) => v.line.x1)
    .attr('y1', ([k,v]) => v.line.y1) 
    .attr('x2', ([k,v]) => v.line.x2)
    .attr('y2', ([k,v]) => v.line.y2)     
    .attr('fill', "#cfcfcf")
    .attr('x', ([k,v]) => v.text.x - 0.6)
    .attr('y', ([k,v]) => v.text.y - 0.6)
    .attr('width', 1.1)
    .attr('height', 1.1)
    .attr('alignment-baseline', "middle")
    .attr('text-anchor', "middle")
    .attr('fill', ([k,v]) => v.text.color)
    .attr('font-size', 1)
    .attr('x', ([k,v]) => v.text.x)
    .attr('y', ([k,v]) => v.text.y)
    .text(([k,v]) => v.text.text)
<script src=""></script>
<svg width="100%" viewbox="0 0 12 12">


Elements in groups:

enter image description here


<svg width="100%" viewBox="0 0 12 12">
  <g class="green">
    <circle fill="green" r="0.5" cx="2" cy="2"></circle>
    <line stroke="green" stroke-width="0.1" x1="2" y1="2" x2="1" y2="1"></line>
    <rect fill="#cfcfcf" x="0.4" y="0.4" width="1.1" height="1.1"></rect>
    <text alignment-baseline="middle" text-anchor="middle" fill="green" font-size="1" x="1" y="1">A</text>
  <g class="blue">
    <circle fill="blue" r="1" cx="4" cy="4"></circle>
    <line stroke="blue" stroke-width="0.1" x1="4" y1="4" x2="2" y2="6"></line>
    <rect fill="#cfcfcf" x="1.4" y="5.4" width="1.1" height="1.1"></rect>
    <text alignment-baseline="middle" text-anchor="middle" fill="blue" font-size="1" x="2" y="6">B</text>
  <g class="red">
    <circle fill="red" r="1.5" cx="8" cy="8"></circle>
    <line stroke="red" stroke-width="0.1" x1="8" y1="8" x2="9" y2="4"></line>
    <rect fill="#cfcfcf" x="8.4" y="3.4" width="1.1" height="1.1"></rect>
    <text alignment-baseline="middle" text-anchor="middle" fill="red" font-size="1" x="9" y="4">C</text>

Output (crude example):

enter image description here

Related Query

More Query from same tag