【问题标题】:d3 forceX not positioning circles near their x axis valued3 forceX 不在其 x 轴值附近定位圆
【发布时间】:2020-10-26 10:31:59
【问题描述】:

我的 d3 图表看起来很棒 - 除了一件事之外它很漂亮 - 我的圆圈的 x 位置似乎与我的数组中给定的 x 值不太接近。

我有一个这样的对象数组:

  {
      x: 2020,
      cx: 0,
      colour: "#F25F5C",
      y1: 0,
      y2: 200,
      rad: 10,
      amt: 5000
    }, 

我有两组圆圈。第一个位置非常靠近 x 轴并指示确切的日期,它们的颜色与较大圆圈的颜色相匹配,其半径在更大的范围内变化,我将 d3.force 应用于这些以更有效地定位它们。

问题在于较大的圆的中心应该靠近由 (x, y2) 给出的数组中的值。但它们不是,而且我的 forceX 似乎没有工作。

这是我的完整代码:

import React, {useState, useEffect} from 'react';
import * as d3 from 'd3';
import './App.css';

function App() {
  const [maxYear, setMaxYear] = useState();
  const [data, setData] = useState(
   [{
      x: 2020,
      colour: "#69306D",
      y1: 0,
      y2: 50,
      rad: 10,
      amt: 2000
    },
    {
      x: 2020,
      colour: "#247BA0",
      y1: 0,
      y2: 100,
      rad: 10,
      amt: 5000
    },
    {
      x: 2020,
      colour: "#3F762C",
      y1: 0,
      y2: 150,
      rad: 10,
      amt: 7500
    },
    {
      x: 2020,
      colour: "#F25F5C",
      y1: 0,
      y2: 200,
      rad: 10,
      amt: 5000
    },
    {
      x: 2022,
      colour: "#0C3957",
      y1: 0,
      y2: 250,
      rad: 10,
      amt: 9000
    },
    {
      x: 2055,
      colour: "#BF802F",
      y1: 0,
      y2: 300,
      rad: 10,
      amt: 25000
    }
  ]
  );

  const thisYear = new Date().getFullYear()

  const initialiseData = () => {
    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 ) => {
      // console.log("RANGE", accessor, range)
      return d3.scaleLinear()
        .domain( d3.extent( arr, accessor ) )
        .range( range )
        .nice()
    }

    //SCALES//
    //-------------------------------------------------------------------------------------------------//

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

    const rad = d3.scaleLinear() //scale for radius of small dots on xaxis
      .domain(d3.extent(data, d => d.rad))
      .range([3, 10]);
    
    const amt = d3.scaleLinear() //scale for radius of large circle      .domain(d3.extent(data, d => d.amt))
      .domain(d3.extent(data, d => d.amt))
      .range([20, 150]);
    
    //-------------------------------------------------------------------------------------------------//
    //END OF SCALES//
    
    //stacks small dots on x axis
    for (let dotindex=0; dotindex<data.length; dotindex++) {
      if (data[dotindex - 1]) {
        if (data[dotindex - 1].x === data[dotindex].x) {
          data[dotindex].y1 = data[dotindex -1].y1 -20
        }
      }
    }

    //creates array of multiples of ten for x axis labels
    let tickTens = [];
    for (let i=thisYear; i<maxYear; i++) {
      if (i % 10 === 0) {
        tickTens.push(i)
      }
    }
    
    //maps array of multiples of ten to values at ticks on x axis
    const g = d3.axisBottom( scX ).tickValues(
      tickTens.map((tickVal) => {
        return tickVal
      })
    )

      //Groups data into arrays by goal year
      const _data = data.reduce(
          (r, v, _, __, k = v["x"]) => ((r[k] || (r[k] = [])).push(v), r),
          []
        )

      //CREATE X AXIS//
      //---------------------------------------------------------------------------//

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

      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")

      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.y1))
        .attr("r", d => rad(d.rad));

      //END OF CREATE X AXIS//
      //---------------------------------------------------------------------------//


      //CREATE LARGE CIRCLES//
      //---------------------------------------------------------------------------//
      const ticked = () => {
        goalAmounts
          .attr("cx", (d => (2 * d.x)))
          .attr("cy", d => d.y2);
      }

      let i = 0;

      const goalAmounts = svg.selectAll("circle .circles")
        .append( "g" )
        .attr("class", "goalAmounts")
        .data(data.sort(function(a, b) {
          return a.amt - b.amt;
      }).reverse())
        .enter()
        .append("circle")
        .attr( "transform", "translate(" + 650 + "," + 0 + ")")
        .attr("fill", d => {
          return d.colour}
        )
        .attr("cx", d => (d.x))
        .attr("cy", (d, index) => {
          let _y = scY(d.y2)
          if (_data[d.x].length > 1 && _data[i-1]) {
            i++
            _y =  _data[d.x][i -1].y2
          } else {
            i = 0
          }
          return _y
        })
        .attr("r", d => amt(d.amt));

      d3.forceSimulation(data)
      // .force('charge', d3.forceManyBody().strength(-200))
      .force('x', scX(d3.forceX().x(function(d) {
        return d.x
      })))
      .force("y", d3.forceY(d => (d.y2 * Math.random())))
      .force('collision', d3.forceCollide().radius(d => amt(d.amt/2)))
      .on("tick", ticked);

    //END OF CREATE LARGE CIRCLES//
    //---------------------------------------------------------------------------//
      
  }

  

  useEffect(() => {
    if (data) {
      setMaxYear(Math.max.apply(Math, data.map(function(o) { return o.x; })))
    }

    if (maxYear) {
      initialiseData();
    }

  }, [data,maxYear])

  return (
    <div className="App">
      <svg id="demo1" width="1200" height="700" style={{background: "white"}}/>
    </div>
  );
}

export default App;

请查看到目前为止代码在此沙箱中的工作方式:d3Sandbox

【问题讨论】:

  • 1.您的代码框充满了错误,无法正常工作。 2. 正如我在this answer 中解释的那样,您需要将x 重命名为x1 或其他名称,因为d.xd3-force 覆盖并设置为像素值,而不是您想要的年份
  • 这样吗? x1: 2020
  • 是的,完全正确。再看看答案,并具体检查我在哪里使用d.x1 以及我在哪里使用d.x。请注意,我从不将d.xscX 一起使用!

标签: javascript reactjs d3.js


【解决方案1】:

这是我认为的工作版本的代码?

 import React, {useState, useEffect} from 'react';
import * as d3 from 'd3';
import './App.css';

function App() {
  const [maxYear, setMaxYear] = useState();
  const [data, setData] = useState(
   [{
      x1: 2020,
      colour: "#69306D",
      y1: 0,
      y2: 50,
      rad: 10,
      amt: 2000
    },
    {
      x1: 2021,
      colour: "#247BA0",
      y1: 0,
      y2: 100,
      rad: 10,
      amt: 5000
    },
    {
      x1: 2030,
      colour: "#3F762C",
      y1: 0,
      y2: 150,
      rad: 10,
      amt: 7500
    },
    {
      x1: 2020,
      colour: "#F25F5C",
      y1: 0,
      y2: 200,
      rad: 10,
      amt: 5000
    },
    {
      x1: 2022,
      colour: "#0C3957",
      y1: 0,
      y2: 250,
      rad: 10,
      amt: 9000
    },
    {
      x1: 2055,
      colour: "#BF802F",
      y1: 0,
      y2: 300,
      rad: 10,
      amt: 25000
    }
  ]
  );

  const thisYear = new Date().getFullYear()

  const initialiseData = () => {
    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 ) => {
      // console.log("RANGE", accessor, range)
      return d3.scaleLinear()
        .domain( d3.extent( arr, accessor ) )
        .range( range )
        .nice()
    }

    //SCALES//
    //-------------------------------------------------------------------------------------------------//

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

    const rad = d3.scaleLinear() //scale for radius of small dots on xaxis
      .domain(d3.extent(data, d => d.rad))
      .range([3, 10]);
    
    const amt = d3.scaleLinear() //scale for radius of large circle      .domain(d3.extent(data, d => d.amt))
      .domain(d3.extent(data, d => d.amt))
      .range([20, 150]);
    
    //-------------------------------------------------------------------------------------------------//
    //END OF SCALES//
    
    //stacks small dots on x axis
    let _newData = data;
    for (let dotindex=0; dotindex<data.length; dotindex++) {
      if (_newData[dotindex - 1]) {
        if (_newData[dotindex - 1].x1 === _newData[dotindex].x1) {
          _newData[dotindex].y1 = _newData[dotindex -1].y1 -20
        }
      }
    }

    setData(_newData)

    console.log("DATA: ", data)

    //creates array of multiples of ten for x axis labels
    let tickTens = [];
    for (let i=thisYear; i<maxYear; i++) {
      if (i % 10 === 0) {
        tickTens.push(i)
      }
    }
    
    //maps array of multiples of ten to values at ticks on x axis
    const g = d3.axisBottom( scX ).tickValues(
      tickTens.map((tickVal) => {
        return tickVal
      })
    )

      //Groups data into arrays by goal year
      const _data = data.reduce(
          (r, v, _, __, k = v["x"]) => ((r[k] || (r[k] = [])).push(v), r),
          []
        )

      //CREATE X AXIS//
      //---------------------------------------------------------------------------//

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

      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")

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

      //END OF CREATE X AXIS//
      //---------------------------------------------------------------------------//


      //CREATE LARGE CIRCLES//
      //---------------------------------------------------------------------------//
      const ticked = () => {
        goalAmounts
          .attr("cx", (d => d.x))
          .attr("cy", d => d.y);
      }

      let i = 0;

      const goalAmounts = svg.selectAll()
        .data(data.sort(function(a, b) {
          return a.amt - b.amt;
      }).reverse())
        .enter()
        .append("g")
        .attr("class", "goals")
        .append("circle")
        .attr( "transform", "translate(" + 200 + "," + 100 + ")")
        .attr("fill", d => {
          return d.colour}
        )
        .attr("cx", d => {
          // console.log("DX1, ", d.x1,  (d.x1)
          return d.x1
        })
        .attr("cy", (d, index) => {
          let _y = scY(d.y2)
      }
        )
        .attr("r", d => amt(d.amt));

      d3.forceSimulation(data)
      // .force('charge', d3.forceManyBody().strength(-20))
      .force('x', (d3.forceX().x(function(d) {
        return scX2(d.x1)
      })))
      .force("y", d3.forceY(d => (d.y2 * Math.random())))
      .force('collision', d3.forceCollide().radius(d => amt(d.amt) + 10))
      .on("tick", ticked);

    //END OF CREATE LARGE CIRCLES//
    //---------------------------------------------------------------------------//
      
  }

  

  useEffect(() => {
    if (data) {
      console.log("DATA: ", data)
      setMaxYear(Math.max.apply(Math, data.map(function(o) { return o.x1; })))
    }

    if (maxYear) {
      console.log("MAXYEAR", maxYear)
      initialiseData();
    }

  }, [data, maxYear])

  return (
    <div className="App">
      <svg id="demo1" width="1200" height="700" style={{background: "white"}}/>
    </div>
  );
}

export default App;

【讨论】:

  • 很高兴看到你想通了!
  • 非常抱歉,我很痛苦 - 我承受着巨大的压力,无法过快地学习这一点,非常感谢您的帮助
  • 别担心,乐于助人
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-05
相关资源
最近更新 更多