d3.jsd3-force-directed

d3 force not applying x scale properly


I want to apply a scale to circles generated with d3 force from an array.

This code produces the right x axis, but there are loads of circles produced with the class 'goalamount' and they are all offscreen by tens of thousands of pixels. There should only be six circles in the goalamount class and they should all scale to the xaxis - what am I doing wrong?

const data = [{
    x: 2020,
    cx: 0,
    colour: "#69306D",
    scY: 0,
    y2: 50,
    rad: 10,
    amt: 5000
  },
  {
    x: 2020,
    cx: 0,
    colour: "#247BA0",
    scY: 0,
    y2: 50,
    rad: 10,
    amt: 5000
  },
  {
    x: 2020,
    cx: 0,
    colour: "#3F762C",
    y1: 0,
    y2: 50,
    rad: 10,
    amt: 5000
  },
  {
    x: 2020,
    cx: 0,
    colour: "#F25F5C",
    y1: 0,
    y2: 50,
    rad: 10,
    amt: 5000
  },
  {
    x: 2022,
    cx: 0,
    colour: "#0C3957",
    y1: 0,
    y2: 170,
    rad: 10,
    amt: 5000
  },
  {
    x: 2055,
    cx: 0,
    colour: "#BF802F",
    y1: 0,
    y2: 50,
    rad: 10,
    amt: 15000
  }
];

const maxYear = Math.max.apply(Math, data.map(function(o) {
  return o.x;
}));

const svg = d3.select("svg");
const pxX = svg.attr("width");
const pxY = svg.attr("height");
let tickLabelOffset = 170;

let minDotX = Math.min.apply(Math, data.map(function(o) {
  return o.y1;
}))
if (minDotX < -20) {
  tickLabelOffset += minDotX + 20;
}

const makeScale = (arr, accessor, range) => {
  return d3.scaleLinear()
    .domain(d3.extent(arr, accessor))
    .range(range)
    .nice()
}

const thisYear = new Date().getFullYear()

let tickTens = [];
for (let i = thisYear; i < maxYear; i++) {
  if (i % 10 === 0) {
    tickTens.push(i)
  }
}


const scX = makeScale(data, d => d.x, [0, pxX - 200]);
const scX1 = makeScale(data, d => d.x, [0, pxX - 2020]);
const scY = d3.scaleLinear().domain([0, 100]).range([0, 100]);

const g = d3.axisBottom(scX).tickValues(
  tickTens.map((tickVal) => {
    return tickVal
  })
)

const rad = d3.scaleLinear()
  .domain(d3.extent(data, d => d.rad))
  .range([3, 10]);

const amt = d3.scaleLinear()
  .domain(d3.extent(data, d => d.amt))
  .range([20, 50]);

for (let dotindex = 0; dotindex < data.length; dotindex++) {
  if (data[dotindex - 1]) {
    if (data[dotindex - 1].x === data[dotindex].x) {
      data[dotindex].scY = data[dotindex - 1].scY - 20
    }
  }
}

const ticked = () => {
  var u = d3.select('svg')
    .append("g")
    .attr("class", "goalAmounts")
    .selectAll('goalAmounts')
    .data(data)

  u.enter()
    .append("circle")
    // .attr( "transform", "translate(" + 2000 + "," + 50 + ")")
    .attr("r", d => amt(d.amt))
    .merge(u)
    .attr("fill", d => d.colour)
    .attr("cx", d => scX(d.x))
    .attr("cy", d => scY(d.y2))

  u.exit().remove()
}


svg.append("g")
  .attr("transform", "translate(" + 50 + "," + (pxY - 200) + ")")
  .call(g)
  .selectAll(".tick text")
  .attr("fill", "#7A7A7A")

svg.selectAll("circle")
  .data(data)
  .enter()
  .append("g")
  .attr("class", "circles")
  .append("circle")
  .attr("transform", "translate(" + 100 + "," + 650 + ")")
  .attr("fill", "white")
  .attr("stroke", d => d.colour)
  .attr("stroke-width", "2px")
  .attr("cx", d => scX(d.x))
  .attr("cy", d => scY(d.y2))
  .attr("r", d => rad(d.rad));

svg.selectAll(".domain")
  .attr("stroke", "#BDBDBD")
  .attr("stroke-width", "2px")
  .attr("transform", "translate(" + 50 + "," + 150 + ")")

svg.selectAll(".tick line")
  .attr("stroke", "#BDBDBD")
  .attr("stroke-width", "4px")
  .attr("transform", "translate(" + 50 + "," + 150 + ")")

svg.selectAll(".tick text")
  .attr("font-size", 20)
  .attr("transform", "translate(" + 50 + "," + tickLabelOffset + ")")
  .attr("font-weight", "bold")
  .attr("dy", "0.5em")

d3.forceSimulation(data)
  .force('charge', d3.forceManyBody())
  .force('center', d3.forceCenter(pxX / 2, pxY / 2))
  .force('collision', d3.forceCollide().radius(function(d) {
    return d.amt
  }))
  .on('tick', ticked);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="App">
  <svg id="demo1" width="1200" height="700">
  </svg>
</div>


Solution

  • Use d3.forceX and d3.forceY instead, so you draw the nodes towards their intended position. Also, d3-force populates x and y properties of the nodes, so you need to use d.x1 or something instead. scX(d.x) caused the huge values of the nodes.

    const data = [{
        x1: 2020,
        cx: 0,
        colour: "#69306D",
        scY: 0,
        y2: 50,
        rad: 10,
        amt: 5000
      },
      {
        x1: 2020,
        cx: 0,
        colour: "#247BA0",
        scY: 0,
        y2: 50,
        rad: 10,
        amt: 5000
      },
      {
        x1: 2020,
        cx: 0,
        colour: "#3F762C",
        y1: 0,
        y2: 50,
        rad: 10,
        amt: 5000
      },
      {
        x1: 2020,
        cx: 0,
        colour: "#F25F5C",
        y1: 0,
        y2: 50,
        rad: 10,
        amt: 5000
      },
      {
        x1: 2022,
        cx: 0,
        colour: "#0C3957",
        y1: 0,
        y2: 170,
        rad: 10,
        amt: 5000
      },
      {
        x1: 2055,
        cx: 0,
        colour: "#BF802F",
        y1: 0,
        y2: 50,
        rad: 10,
        amt: 15000
      }
    ];
    
    const maxYear = Math.max.apply(Math, data.map(function(o) {
      return o.x1;
    }));
    
    const svg = d3.select("svg");
    const pxX = svg.attr("width");
    const pxY = svg.attr("height");
    let tickLabelOffset = 170;
    
    let minDotX = Math.min.apply(Math, data.map(function(o) {
      return o.y1;
    }))
    if (minDotX < -20) {
      tickLabelOffset += minDotX + 20;
    }
    
    const makeScale = (arr, accessor, range) => {
      return d3.scaleLinear()
        .domain(d3.extent(arr, accessor))
        .range(range)
        .nice()
    }
    
    const thisYear = new Date().getFullYear()
    
    let tickTens = [];
    for (let i = thisYear; i < maxYear; i++) {
      if (i % 10 === 0) {
        tickTens.push(i)
      }
    }
    
    const scX = makeScale(data, d => d.x1, [0, pxX - 200]);
    const scX1 = makeScale(data, d => d.x1, [0, pxX - 2020]);
    const scY = d3.scaleLinear().domain([0, 100]).range([0, 100]);
    
    const g = d3.axisBottom(scX).tickValues(
      tickTens.map((tickVal) => {
        return tickVal
      })
    )
    
    const rad = d3.scaleLinear()
      .domain(d3.extent(data, d => d.rad))
      .range([3, 10]);
    
    const amt = d3.scaleLinear()
      .domain(d3.extent(data, d => d.amt))
      .range([20, 50]);
    
    for (let dotindex = 0; dotindex < data.length; dotindex++) {
      if (data[dotindex - 1]) {
        if (data[dotindex - 1].x1 === data[dotindex].x1) {
          data[dotindex].scY = data[dotindex - 1].scY - 20
        }
      }
    }
    
    const ticked = () => {
      circles
        .attr("cx", d => d.x)
        .attr("cy", d => d.y);
    }
    
    
    svg.append("g")
      .attr("transform", "translate(" + 50 + "," + (pxY - 200) + ")")
      .call(g)
      .selectAll(".tick text")
      .attr("fill", "#7A7A7A")
    
    const circles = svg.append("g")
      .attr("class", "circles")
      .attr( "transform", "translate(" + 100 + "," + 100 + ")")
      .selectAll("circle")
      .data(data)
      .enter()
      .append("circle")
      .attr("fill", "white")
      .attr("stroke", d => d.colour)
      .attr("stroke-width", "2px")
      .attr("cx", d => scX(d.x1))
      .attr("cy", d => scY(d.y2))
      .attr("r", d => rad(d.rad));
    
    svg.selectAll(".domain")
      .attr("stroke", "#BDBDBD")
      .attr("stroke-width", "2px")
      .attr("transform", "translate(" + 50 + "," + 150 + ")")
    
    svg.selectAll(".tick line")
      .attr("stroke", "#BDBDBD")
      .attr("stroke-width", "4px")
      .attr("transform", "translate(" + 50 + "," + 150 + ")")
    
    svg.selectAll(".tick text")
      .attr("font-size", 20)
      .attr("transform", "translate(" + 50 + "," + tickLabelOffset + ")")
      .attr("font-weight", "bold")
      .attr("dy", "0.5em")
    
    d3.forceSimulation(data)
      .force("x", d3.forceX(d => scX(d.x1)))
      .force("y", d3.forceY(d => scY(d.y2)))
      .force('collision', d3.forceCollide().radius(d => rad(d.rad)))
      .on("tick", ticked);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
    <div class="App">
      <svg id="demo1" width="1200" height="700">
      </svg>
    </div>