Я пытаюсь сделать разбросанный график с симуляцией силы в 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 [JS]](https://i.imgur.com/WsjO6zJb.png)


Самое главное в использовании моделирования силы, чтобы избежать перекрытия точек на точечной диаграмме, — это использовать методы 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 уменьшает перекрытие, но делает визуализацию менее точной.
Кроме того, у вас есть небольшие проблемы:
ticked;myIbus и myABV, кажется, имеют неправильные масштабы (просто поменяйте их местами).Вот ваш обновленный код:
<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.forceYиd3.forceX part, и было неясно, как перебирать функцию, теперь думаю, становится ясно. Большое спасибо!