【问题标题】:Scattered Plot with force layout具有力布局的散点图
【发布时间】:2019-09-20 20:10:44
【问题描述】:

我正在尝试在 D3 中使用两个轴 X 和 Y 的力模拟制作散点图,我想应用力布局,只是为了避免重叠点,但我得到了完全相反的效果(点是重叠的,它们的位置不是很好)

这是我目前的代码:

// Create SVG and margins

var margin = {top: 52, right: 78, bottom: 52, left: 78}
var myWidth = 900 - margin.left - margin.right
var myHeight = 450 - margin.top - margin.bottom

var svg = d3.select('body').append('svg')
  .attr('width', myWidth + margin.left + margin.right)
  .attr('height', myHeight + margin.top + margin.bottom)

var g = svg.append("g")
  .attr("transform", "translate(" + margin.left + ", " + margin.top + ")")

// Scale

var y = d3.scaleLinear()
  .domain([2,10])
    .range([myHeight, 0])

var x = d3.scaleLinear()
    .domain([0,100])
    .range([0, myWidth])

// Axis

var yAxisCall = d3.axisLeft(y).tickSize(10)
g.append("g")
  .attr("class", "y-axis")
  .call(yAxisCall)

var xAxisCall = d3.axisBottom(x).tickSize(10)
g.append("g")
  .attr("class", "x-axis")
  .attr("transform", "translate(0, " + myHeight + ")")
  .call(xAxisCall)

 // Helper Functions

var myIbus = function(d,i){
  if (d.ibus) {
    return d.ibus[1] ? (y((d.ibus[0] + d.ibus[1])/2)) : (y(d.ibus[0]))
  }
  else return 0
 }

var myABV = function(d,i){
  if (d.abv) {
    return d.abv[1] ? (x((d.abv[0] + d.abv[1])/2)) : (x(d.abv[0]))
  }
  else return 0
 }


// Force Simulation

var simulation = d3.forceSimulation(nodes)
  .force('collide', d3.forceCollide())
  .on('tick', ticked)


function ticked() {

  var myCircles = g.selectAll('circle')
    .data(nodes)

  myCircles.enter()
    .append('circle')
    .attr("cx", myIbus)
    .attr("cy", myABV)
    .attr("r", 8)

  myCircles.exit().remove()

}

我在https://bl.ocks.org/Jesus82/ad5c6fb46f8be5a9d3e763f8a1ba03d7 中有一个工作示例,其中包含我正在使用的数据(我想根据其 ABV 酒精百分比和 IBUS 苦味来可视化啤酒风格),并且数据在范围内,我只是使用他们的意思。

提前致谢!

【问题讨论】:

    标签: javascript d3.js data-visualization force-layout


    【解决方案1】:

    使用力模拟来避免散点图中的重叠点最重要的是使用d3.forceXd3.forceY 方法来设置位置,而d3.forceCollide 只是为了避免重叠。

    因此,您的模拟应该是:

    var simulation = d3.forceSimulation(nodes)
      .force('collide', d3.forceCollide().radius(8))
      .force('x', d3.forceX(myIbus))
      .force('y', d3.forceY(myABV))
      .on('tick', ticked);
    

    在你的ticked 函数中:

    function ticked() {
      myCircles.attr("cx", function(d) {
          return d.x
        })
        .attr("cy", function(d) {
          return d.y
        });
    };
    

    您可以使用这些力中的strengths:给forceX/Y 力提供更大的力使分散更准确,但重叠点更多;为forceCollide 提供更多强度会减少重叠,但会降低可视化的准确性。

    除此之外,你还有一些小问题:

    1. 将进入、更新和退出选择移动到外部ticked函数;
    2. 也许我错了,但您的 myIbusmyABV 方法似乎有错误的比例(只需交换它们)。
    3. 将数据变量初始化移动到更新选择之前。

    这是您的更新代码:

    <head>
      <meta charset="utf-8">
      <script src="https://d3js.org/d3.v4.min.js"></script>
      <style>
        body {
          margin: 0;
          position: fixed;
          top: 0;
          right: 0;
          bottom: 0;
          left: 0;
        }
      </style>
    </head>
    
    <body>
      <script>
        var nodes = [{
            name: 'abbey_dubbel',
            abv: [6, 7.6],
            ibus: [15, 25]
          },
          {
            name: 'abbey_tripel',
            abv: [7.5, 9.5],
            ibus: [20, 40]
          },
          {
            name: 'ale',
            abv: [0],
            ibus: [0]
          },
          {
            name: 'amber_ale',
            abv: [0],
            ibus: [0]
          },
          {
            name: 'amber_lager',
            abv: [4.7, 5.5],
            ibus: [18, 30]
          },
          {
            name: 'american_IPA',
            abv: [6, 14],
            ibus: [40, 70]
          },
          {
            name: 'american_pale_ale',
            abv: [4.5, 6.2],
            ibus: [30, 50]
          },
          {
            name: 'american_strong_ale',
            abv: [8, 12],
            ibus: [30, 60]
          },
          {
            name: 'baltic_porter',
            abv: [6.5, 9.5],
            ibus: [20, 40]
          },
          {
            name: 'barley_wine',
            abv: [8, 12],
            ibus: [50, 100]
          },
          {
            name: 'belgian_ale',
            abv: [8, 5.5],
            ibus: [20, 30]
          },
          {
            name: 'belgian_strong_ale',
            abv: [7.5, 10.5],
            ibus: [22, 35]
          },
          {
            name: 'berliner_weisse',
            abv: [2.8, 3.8],
            ibus: [3, 8]
          },
          {
            name: 'biere_de_garde',
            abv: [6, 8.5],
            ibus: [18, 28]
          },
          {
            name: 'black_IPA',
            abv: [5.5, 9],
            ibus: [50, 90]
          },
          {
            name: 'blond_ale',
            abv: [6, 7.5],
            ibus: [15, 30]
          },
          {
            name: 'brown_ale',
            abv: [4.2, 5.4],
            ibus: [20, 30]
          },
          {
            name: 'brut_ipa',
            abv: [5, 7.5],
            ibus: [40, 60]
          },
          {
            name: 'cider',
            abv: [0],
            ibus: [0]
          },
          {
            name: 'doppelbock',
            abv: [7, 10],
            ibus: [16, 26]
          },
          {
            name: 'dunkel',
            abv: [4.5, 5.6],
            ibus: [18, 28]
          },
          {
            name: 'ESB',
            abv: [4.6, 6.2],
            ibus: [30, 50]
          },
          {
            name: 'foreign_extra_stout',
            abv: [6.3, 8],
            ibus: [50, 70]
          },
          {
            name: 'fruit_beer',
            abv: [2, 8],
            ibus: [40]
          },
          {
            name: 'fruity_lambic',
            abv: [5, 7],
            ibus: [10]
          },
          {
            name: 'gose',
            abv: [4.2, 4.8],
            ibus: [5, 12]
          },
          {
            name: 'gueuze_lambic',
            abv: [5, 8],
            ibus: [10]
          },
          {
            name: 'imperial_IPA',
            abv: [7.5, 10],
            ibus: [60, 120]
          },
          {
            name: 'imperial_pils',
            abv: [0],
            ibus: [0]
          },
          {
            name: 'imperial_porter',
            abv: [4.8, 6.5],
            ibus: [25, 50]
          },
          {
            name: 'imperial_stout',
            abv: [5, 7.5],
            ibus: [40, 60]
          },
          {
            name: 'india_style_lager',
            abv: [0],
            ibus: [0]
          },
          {
            name: 'IPA',
            abv: [5, 7.5],
            ibus: [40, 60]
          },
          {
            name: 'lager',
            abv: [0],
            ibus: [0]
          },
          {
            name: 'lambic',
            abv: [5, 6.5],
            ibus: [10]
          },
          {
            name: 'landbier',
            abv: [4.7, 7.4],
            ibus: [16, 22]
          },
          {
            name: 'neipa',
            abv: [6, 9],
            ibus: [25, 60]
          },
          {
            name: 'old_ale',
            abv: [5.5, 9],
            ibus: [30, 60]
          },
          {
            name: 'pale_lager',
            abv: [4.6, 6],
            ibus: [18, 25]
          },
          {
            name: 'pilsener',
            abv: [4.4, 5.2],
            ibus: [22, 40]
          },
          {
            name: 'porter',
            abv: [4, 5.4],
            ibus: [28, 35]
          },
          {
            name: 'premium_lager',
            abv: [4.2, 5.8],
            ibus: [30, 45]
          },
          {
            name: 'quadrupel',
            abv: [8, 12],
            ibus: [20, 35]
          },
          {
            name: 'saison',
            abv: [3.5, 9.5],
            ibus: [20, 35]
          },
          {
            name: 'scotch_ale',
            abv: [6.5, 10],
            ibus: [17, 35]
          },
          {
            name: 'session_IPA',
            abv: [3, 5],
            ibus: [35, 60]
          },
          {
            name: 'smoked',
            abv: [0],
            ibus: [0]
          },
          {
            name: 'sour_red_brown',
            abv: [4.6, 6.5],
            ibus: [10, 25]
          },
          {
            name: 'sour_wild_ale',
            abv: [0],
            ibus: [0]
          },
          {
            name: 'specialty_grain',
            abv: [0],
            ibus: [0]
          },
          {
            name: 'stout',
            abv: [4, 6],
            ibus: [20, 40]
          },
          {
            name: 'sweet_stout',
            abv: [4, 6],
            ibus: [20, 40]
          },
          {
            name: 'weissbier',
            abv: [4.3, 5.6],
            ibus: [8, 15]
          },
          {
            name: 'weizen_bock',
            abv: [6.5, 9],
            ibus: [15, 30]
          },
          {
            name: 'wheat_ale',
            abv: [4, 5.5],
            ibus: [15, 30]
          },
          {
            name: 'witbier',
            abv: [4.5, 5.5],
            ibus: [8, 20]
          }
        ]
        // Create SVG and margins
    
        var margin = {
          top: 52,
          right: 78,
          bottom: 52,
          left: 78
        }
        var myWidth = 900 - margin.left - margin.right
        var myHeight = 450 - margin.top - margin.bottom
    
        var svg = d3.select('body').append('svg')
          .attr('width', myWidth + margin.left + margin.right)
          .attr('height', myHeight + margin.top + margin.bottom)
    
        var g = svg.append("g")
          .attr("transform", "translate(" + margin.left + ", " + margin.top + ")")
    
        // Scale
    
        var y = d3.scaleLinear()
          .domain([2, 10])
          .range([myHeight, 0])
    
        var x = d3.scaleLinear()
          .domain([0, 100])
          .range([0, myWidth])
    
        // Axis
    
        var yAxisCall = d3.axisLeft(y).tickSize(10)
        g.append("g")
          .attr("class", "y-axis")
          .call(yAxisCall)
    
        var xAxisCall = d3.axisBottom(x).tickSize(10)
        g.append("g")
          .attr("class", "x-axis")
          .attr("transform", "translate(0, " + myHeight + ")")
          .call(xAxisCall)
    
        // Helper Functions
    
        var myIbus = function(d, i) {
          if (d.ibus) {
            return d.ibus[1] ? (x((d.ibus[0] + d.ibus[1]) / 2)) : (x(d.ibus[0]))
          } else return 0
        }
    
        var myABV = function(d, i) {
          if (d.abv) {
            return d.abv[1] ? (y((d.abv[0] + d.abv[1]) / 2)) : (y(d.abv[0]))
          } else return 0
        }
    
    
        // Force Simulation
    
        var simulation = d3.forceSimulation(nodes)
          .force('collide', d3.forceCollide().radius(8))
          .force('x', d3.forceX(myIbus))
          .force('y', d3.forceY(myABV))
          .on('tick', ticked);
    
        var myCircles = g.selectAll('circle')
          .data(nodes)
    
        myCircles = myCircles.enter()
          .append('circle')
          .attr("r", 8)
          .merge(myCircles);
    
        myCircles.exit().remove()
    
    
        function ticked() {
          myCircles.attr("cx", function(d) {
              return d.x
            })
            .attr("cy", function(d) {
              return d.y
            });
        };
      </script>
    </body>

    【讨论】:

    • 太棒了,我完全错过了d3.forceYd3.forceX part,并且不清楚如何迭代函数,现在想清楚了。非常感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-27
    • 2013-02-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多