Разбросанный участок с силовой разметкой - PullRequest
1 голос
/ 02 мая 2019

Я пытаюсь создать рассеянный график с имитацией силы в 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-горечью), и где данные находятся в диапазонах, Я просто использую их среднее значение.

Заранее спасибо!

1 Ответ

1 голос
/ 02 мая 2019

Самая важная вещь в использовании имитации силы, чтобы избежать наложения точек на точечной диаграмме, - это использование методов d3.forceX и d3.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. Возможно, я ошибаюсь, но ваши методы myIbus и myABV, похоже, имеют неправильные шкалы (просто меняйте их местами).
  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>
...