Почему d3.drag() работает по разному в зависимости от представления данных, привязываемых через .data?
Задача простая - сделать drag кружочков, но в осях. Столкнулся с проблемой. Ниже код, который реализует базовый функционал drag. Данные представлены массивом объектов. При выполнении кода кружки совершают ненужные прыжки.
var x = d3.scaleLinear()
.domain([0, 400])
.range([0, 200])
var y = d3.scaleLinear()
.domain([0, 400])
.range([200, 0])
d3.select('svg')
.append('g')
.call(d3.axisLeft(y))
d3.select('svg')
.append('g')
.attr('transform', 'translate(0, 200)')
.call(d3.axisBottom(x))
var data = [{
x: 100,
y: 200
},
{
x: 300,
y: 400
}
];
var dragHandler = d3.drag()
.on('drag', dragged)
var circles = d3.select('svg')
.selectAll('circles')
.data(data)
.enter()
.append('circle')
.attr('cx', function(d) {
return x(d.x)
})
.attr('cy', function(d) {
return y(d.y)
})
.attr('r', 10)
.call(dragHandler)
function dragged(d) {
d.x = x.invert(d3.event.x);
d.y = y.invert(d3.event.y);
d3.select(this)
.attr('cx', x(d.x))
.attr('cy', y(d.y))
};
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<svg width="300" height="300" viewbox="-50 -50 300 300"></svg>
</body>
</html>
А если заменить массив объектов на массив массивов, то прыжки исчезают! Но перестает сохраняться начальное смещение кружка относительно курсора (при начале drag кружок центром прыгает на курсор).
var x = d3.scaleLinear()
.domain([0, 400])
.range([0, 200])
var y = d3.scaleLinear()
.domain([0, 400])
.range([200, 0])
d3.select('svg')
.append('g')
.call(d3.axisLeft(y))
d3.select('svg')
.append('g')
.attr('transform', 'translate(0, 200)')
.call(d3.axisBottom(x))
var data = [
[100, 200],
[300, 400]
];
var dragHandler = d3.drag()
.on('drag', dragged)
var circles = d3.select('svg')
.selectAll('circles')
.data(data)
.enter()
.append('circle')
.attr('cx', function(d) {
return x(d[0])
})
.attr('cy', function(d) {
return y(d[1])
})
.attr('r', 10)
.call(dragHandler)
function dragged(d) {
d[0] = x.invert(d3.event.x);
d[1] = y.invert(d3.event.y);
d3.select(this)
.attr('cx', x(d[0]))
.attr('cy', y(d[1]))
};
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<svg width="300" height="300" viewbox="-50 -50 300 300"></svg>
</body>
</html>
Почему так происходит? Если сделать масштаб 1:1 то проблема не уйдет. А вот если вообще убрать масштабы и работать "в пикселях" то в первом варианте все ок.
Ответы (2 шт):
Обработчик drag должен знать как найти необходимые ему данные, по умолчанию он ищет их в массиве, поведение это можно переопределить вот так:
var dragHandler = d3.drag()
.subject(d => [d.x, d.y])
.on('drag', dragged)
var x = d3.scaleLinear()
.domain([0, 400])
.range([0, 200])
var y = d3.scaleLinear()
.domain([0, 400])
.range([200, 0])
d3.select('svg')
.append('g')
.call(d3.axisLeft(y))
d3.select('svg')
.append('g')
.attr('transform', 'translate(0, 200)')
.call(d3.axisBottom(x))
var data = [{
x: 100,
y: 200
},
{
x: 300,
y: 400
}
];
var dragHandler = d3.drag()
/// я добавил это
.subject(d => [d.x, d.y])
.on('drag', dragged)
var circles = d3.select('svg')
.selectAll('circles')
.data(data)
.enter()
.append('circle')
.attr('cx', function(d) {
return x(d.x)
})
.attr('cy', function(d) {
return y(d.y)
})
.attr('r', 10)
.call(dragHandler)
function dragged(d) {
d.x = x.invert(d3.event.x);
d.y = y.invert(d3.event.y);
d3.select(this)
.attr('cx', x(d.x))
.attr('cy', y(d.y))
};
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<svg width="300" height="300" viewbox="-50 -50 300 300"></svg>
</body>
</html>
На ответ натолкнул вот этот вопрос: https://stackoverflow.com/questions/42844949/d3-drag-event-behaviour-dragevent-x-and-dragevent-y-values-source
Как я понял свойства .x, .y прям "забиты" в библиотеке и только с ними будет сохраняться начальное смещение относительно курсора и центра круга.
В итоге добавил subject где указал данные d.x, d.y с учетом реальных масштабов по осям.
var dragHandler = d3.drag()
.subject(function(d) {
return {
x: x(d.x),
y: y(d.y)
};
})
.on('drag', dragged)
var x = d3.scaleLinear()
.domain([0, 400])
.range([0, 200])
var y = d3.scaleLinear()
.domain([0, 400])
.range([200, 0])
d3.select('svg')
.append('g')
.call(d3.axisLeft(y))
d3.select('svg')
.append('g')
.attr('transform', 'translate(0, 200)')
.call(d3.axisBottom(x))
var data = [{
x: 100,
y: 200
},
{
x: 300,
y: 400
}
];
//изменил только dragHandler
var dragHandler = d3.drag()
.subject(function(d) {
return {
x: x(d.x),
y: y(d.y)
};
})
.on('drag', dragged)
var circles = d3.select('svg')
.selectAll('circles')
.data(data)
.enter()
.append('circle')
.attr('cx', function(d) {
return x(d.x)
})
.attr('cy', function(d) {
return y(d.y)
})
.attr('r', 10)
.call(dragHandler)
function dragged(d) {
d.x = x.invert(d3.event.x);
d.y = y.invert(d3.event.y);
d3.select(this)
.attr('cx', x(d.x))
.attr('cy', y(d.y))
};
<html>
<head>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<svg width="300" height="300" viewbox="-50 -50 300 300"></svg>
</body>
</html>