Я использую d3 v4 (4.12.0).
У меня есть контейнер SVG, в котором я рисую простую горизонтальную ось (ось X, линейный масштаб), которая реагирует на панорамирование с помощью мыши.
Я хотел бы смоделировать «бесконечную» или «бесконечную» горизонтальную ось.
Под этим я подразумеваю, что хочу загрузить и отобразить только небольшую часть очень большого набора данных и отрисовать только ту часть оси, которая показывает очень маленькое подмножество элементов из этого большого набора.
Скажем, у меня есть горизонтальная ось, которая показывает 10 точек данных из большего массива объектов. У меня есть параметр offset
, который начинается с 0, чтобы показать первые десять точек этого массива.
Моя процедура:
Когда я прокручиваю ось влево достаточно далеко, чтобы показать 11-ю и последующие точки данных, я затем:
Обновите параметр
offset
, чтобы отразить, сколько единиц я перевелОбновите масштаб оси X на основе нового значения смещения
Перерисуйте метки осей с обновленным диапазоном шкалы (
x_scale
)Переместите элемент группы, содержащий ось, на количество пикселей, представляющих одну единицу на оси (
scroller_element_width
)
Моя попытка работает до шага 3. Этот процесс, похоже, терпит неудачу на шаге 4, поскольку окончательное перемещение оси никогда не происходит.
Вся ось смещена влево, и у нее свежие метки, но она не смещается вправо с этими обновленными метками — она фактически падает со страницы.
Я хотел бы спросить здесь экспертов по d3, почему этот шаг терпит неудачу и что я могу сделать, чтобы исправить это.
Вот функция, которая рисует ось и подключает событие масштабирования:
renderScroller() {
console.log("renderScroller called");
if ((this.state.scrollerWidth == 0) || (this.state.scrollerHeight == 0)) return;
const self = this;
const scroller = this.scrollerContainer;
const scroller_content = this.scrollerContent;
const scroller_width = this.state.scrollerWidth;
const scroller_height = this.state.scrollerHeight;
var offset = 0,
limit = 10,
current_index = 10;
var min_translate_x = 0,
max_translate_x;
var scroller_data = Constants.test_data.slice(offset, limit);
var x_extent = d3.extent(scroller_data, function(d) { return d.window; });
var y_extent = [0, d3.max(scroller_data, function(d) { return d.total; })];
var x_scale = d3.scaleLinear();
var y_scale = d3.scaleLinear();
var x_axis_call = d3.axisTop();
x_scale.domain(x_extent).range([0, scroller_width]);
y_scale.domain(y_extent).range([scroller_height, 0]);
x_axis_call.scale(x_scale);
d3.select(scroller_content)
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + [0, scroller_height] + ")")
.call(x_axis_call);
var scroller_element_width = parseFloat(scroller_width / (x_scale.domain()[1] - x_scale.domain()[0]));
var pan = d3.zoom()
.on("zoom", function () {
var t = parseSvg(d3.select(scroller_content).attr("transform"));
var x_offset = parseFloat((t.translateX + d3.event.transform.x) / scroller_element_width);
//
// lock scale and prevent y-axis pan
//
d3.event.transform.y = 0;
if (d3.event.transform.k == 1) {
d3.event.transform.x = (x_offset > 0) ? 0 : d3.event.transform.x;
}
else {
d3.event.transform.k = 1;
d3.event.transform.x = t.translateX;
}
d3.select(scroller_content).attr("transform", d3.event.transform);
t = parseSvg(d3.select(scroller_content).attr("transform"));
x_offset = parseFloat(t.translateX / scroller_element_width);
var test_offset = Math.abs(parseInt(x_offset));
if (test_offset != offset) {
scroller_data = updateScrollerData(test_offset);
x_extent = d3.extent(scroller_data, function(d) { return d.window; });
y_extent = [0, d3.max(scroller_data, function(d) { return d.total; })];
x_scale.domain(x_extent).range([0, scroller_width]);
y_scale.domain(y_extent).range([scroller_height, 0]);
x_axis_call.scale(x_scale);
//
// update axis labels
//
d3.select(scroller_content)
.selectAll(".x.axis")
.call(x_axis_call);
//
// shift the axis backwards to simulate an endless horizontal axis
//
var pre_shift = parseSvg(d3.select(scroller_content).attr("transform"));
console.log("pre_shift", pre_shift.translateX);
console.log("scroller_element_width", scroller_element_width);
var expected_post_shift = pre_shift.translateX + scroller_element_width;
console.log("(expected) post_shift", expected_post_shift);
d3.zoom().translateBy(d3.select(scroller_content), expected_post_shift, 0);
//
// observed and expected translate values do not match!
//
var post_shift = parseSvg(d3.select(scroller_content).attr("transform"));
console.log("(observed) post_shift", post_shift.translateX);
}
});
d3.select(scroller).call(pan);
max_translate_x = this.state.scrollerWidth - x_scale(x_extent[1]);
d3.zoom().translateBy(d3.select(scroller), max_translate_x, 0);
// fetch test data
function updateScrollerData(updated_offset) {
offset = updated_offset;
return Constants.test_data.slice(updated_offset - 1, updated_offset + limit - 1);
}
}
Это функция внутри компонента React. Материал React не так актуален, но вот функция render()
этого компонента, чтобы показать родительские элементы SVG и дочерние группы:
render() {
return (
<svg
className="scroller"
ref={(scroller) => { this.scrollerContainer = scroller; }}
width={this.state.scrollerWidth}
height={this.state.scrollerHeight}>
<g
className="scroller-content"
ref={(scrollerContent) => { this.scrollerContent = scrollerContent; }}
/>
</svg>
);
}
Как показано, ссылка scrollerContainer
— это SVG, содержащий элемент группы scrollerContent
. Это scrollerContent
содержит горизонтальную ось.
При панорамировании или прокрутке оси X преобразования применяются к scrollerContent
.
Для получения параметров преобразования я использую вспомогательный метод parseSvg
из d3-interpolate
, т.е. через ES6:
import * as d3 from 'd3';
import { parseSvg } from "d3-interpolate/src/transform/parse";
Для полноты, вот фрагмент тестовых данных:
export const test_data = [
{
"total": 29.86,
"signal": [
4.842,
1.608,
1.837,
3.052,
1.677,
0.8041,
3.09,
1.813,
2.106,
2.38,
1.773,
0.8128,
2.047,
1.658,
0.3588
],
"window": 0,
"chr": "chr1"
},
{
"total": 35.67,
"signal": [
0.6111,
1.995,
0.5715,
2.51,
3.318,
1.523,
3.94,
2.743,
4.445,
0.759,
4.938,
2.61,
3.379,
1.27,
1.057
],
"window": 1,
"chr": "chr1"
},
{
"total": 39.14,
"signal": [
0.0589,
0.1608,
2.426,
4.673,
3.511,
3.912,
2.809,
4.197,
4.648,
2.069,
2.84,
3.878,
0.2681,
3.622,
0.06911
],
"window": 2,
"chr": "chr1"
},
{
"total": 37.45,
"signal": [
2.688,
1.235,
2.358,
1.994,
1.541,
1.189,
0.8078,
4.872,
2.287,
4.266,
2.24,
3.349,
3.519,
1.896,
3.21
],
"window": 3,
"chr": "chr1"
},
{
"total": 47.17,
"signal": [
3.338,
3.613,
3.872,
1.166,
1.828,
4.24,
1.476,
4.025,
4.144,
4.922,
2.183,
2.701,
3.825,
4.346,
1.494
],
"window": 4,
"chr": "chr1"
},
{
"total": 41.7,
"signal": [
0.2787,
1.74,
0.7557,
4.236,
2.865,
4.542,
4.113,
1.265,
4.826,
3.731,
4.931,
2.392,
2.014,
0.6566,
3.352
],
"window": 5,
"chr": "chr1"
},
{
"total": 31.43,
"signal": [
3.025,
4.399,
1.001,
4.859,
0.9173,
2.851,
2.916,
1.821,
1.228,
1.646,
0.1008,
2.09,
2.502,
0.1476,
1.924
],
"window": 6,
"chr": "chr1"
},
{
"total": 38.23,
"signal": [
1.123,
1.972,
0.5079,
4.808,
0.5669,
4.647,
2.598,
1.874,
0.8699,
4.876,
3.981,
1.503,
4.683,
2.853,
1.366
],
"window": 7,
"chr": "chr1"
},
{
"total": 44.2,
"signal": [
3.895,
0.7457,
2.208,
1.837,
3.219,
3.98,
3.494,
4.225,
3.117,
3.162,
3.171,
2.449,
0.1419,
3.745,
4.807
],
"window": 8,
"chr": "chr1"
},
{
"total": 36.33,
"signal": [
0.3164,
2.753,
4.094,
2.237,
4.748,
2.483,
1.541,
4.113,
0.1874,
3.71,
1.313,
0.221,
2.736,
1.208,
4.671
],
"window": 9,
"chr": "chr1"
},
{
"total": 43.05,
"signal": [
1.924,
0.4136,
3.057,
4.686,
1.263,
0.1333,
0.8786,
4.715,
4.845,
4.282,
2.112,
4.597,
3.822,
1.322,
4.999
],
"window": 10,
"chr": "chr1"
},
{
"total": 31.28,
"signal": [
4.216,
0.6655,
2.078,
1.235,
0.5526,
1.556,
1.005,
3.196,
1.907,
4.932,
0.006601,
1.269,
3.964,
4.608,
0.09109
],
"window": 11,
"chr": "chr1"
},
{
"total": 48.3,
"signal": [
4.469,
1.138,
3.958,
2.801,
3.404,
4.988,
2.649,
3.818,
3.284,
0.9281,
3.982,
0.496,
4.28,
3.258,
4.845
],
"window": 12,
"chr": "chr1"
},
{
"total": 42.1,
"signal": [
1.087,
3.127,
0.493,
3.276,
4.195,
1.561,
2.638,
4.897,
3.675,
4.937,
0.05847,
4.272,
2.33,
1.776,
3.776
],
"window": 13,
"chr": "chr1"
},
{
"total": 40.1,
"signal": [
1.275,
4.574,
2.805,
1.646,
0.8759,
4.948,
3.637,
3.227,
2.259,
2.983,
2.905,
4.134,
3.133,
0.08384,
1.617
],
"window": 14,
"chr": "chr1"
},
{
"total": 50.31,
"signal": [
2.228,
0.7037,
4.977,
1.143,
2.506,
4.348,
4.344,
3.998,
4.213,
2.745,
4.374,
3.411,
4.504,
4.417,
2.396
],
"window": 15,
"chr": "chr1"
},
{
"total": 34.7,
"signal": [
2.729,
3.891,
3.873,
2.973,
0.1487,
1.573,
1.781,
2.788,
2.191,
2.912,
1.355,
2.582,
2.374,
3.164,
0.3641
],
"window": 16,
"chr": "chr1"
},
{
"total": 32.89,
"signal": [
3.619,
2.119,
1.854,
4.083,
0.9916,
0.5065,
0.8343,
4.835,
1.723,
3.926,
2.675,
2.281,
0.1531,
2.239,
1.049
],
"window": 17,
"chr": "chr1"
},
{
"total": 38.94,
"signal": [
1.976,
1.587,
3.808,
0.1173,
3.823,
4.349,
3.652,
1.308,
3.434,
3.855,
1.622,
0.2916,
2.382,
3.091,
3.647
],
"window": 18,
"chr": "chr1"
},
{
"total": 34.18,
"signal": [
0.339,
3.695,
3.108,
3.267,
0.08282,
3.53,
2.316,
1.11,
4.504,
4.111,
0.007636,
0.5581,
2.985,
1.707,
2.857
],
"window": 19,
"chr": "chr1"
},
{
"total": 29.62,
"signal": [
2.695,
0.8477,
4.417,
3.012,
2.454,
2.686,
0.6529,
0.2275,
1.052,
0.2092,
2.968,
3.268,
0.7144,
0.4441,
3.973
],
"window": 20,
"chr": "chr1"
}
];
Надеюсь, это показывает всю работу, необходимую для объяснения проблемы. Спасибо за любой совет или руководство.