我正在使用d3 v4(4.12.0)。
我有一个SVG容器,我正在绘制一个简单的水平轴(x轴,线性刻度),它响应用鼠标平移。
我想模拟一个“无限”或“无尽”的水平轴。
通过这个,我的意思是我只想加载和渲染一个非常大的数据集的一小部分,并且只绘制足够的轴来显示这个大集合中的一小部分元素。
假设我的水平轴显示来自更大对象数组的10个数据点。我持有offset
参数,该参数从0开始,以显示该数组的前十个点。
我的程序:
当我将轴向左滚动到足以显示第11个及后续数据点时,我接着:
更新offset
参数以反映我翻译的单位数
根据新的偏移值
使用更新的比例范围(x_scale
)重绘轴标签
将包含轴的组元素转换为表示轴上一个单位的像素数(scroller_element_width
)
我的尝试直到第3步。此过程似乎在步骤4失败,因为轴的最终转换从未发生过。
整个轴向左移动,并且它有新标签,但它不会随着那些更新的标签向右移动 - 它基本上会脱离页面。
我想问d3专家,为什么这一步失败了,我可以做些什么来解决这个问题。
这是绘制轴并挂钩zoom事件的函数:
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
ref是包含组元素scrollerContent
的SVG。此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"
}
];
希望这能说明解释问题所需的所有工作。感谢您的任何建议或指导。
答案 0 :(得分:4)
如果没有完整的可重复示例,我发现您的代码难以理解。所以我编写了一个简单的例子来说明你要做的事情。也许它会有所帮助:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<style>
.axis path {
display: none;
}
.axis line {
stroke-opacity: 0.3;
shape-rendering: crispEdges;
}
.view {
fill: url(#gradient);
stroke: #000;
}
button {
position: absolute;
top: 20px;
left: 20px;
}
</style>
</head>
<body>
<svg width="500" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
// 10,000 random data points
var data = d3.range(1, 10000).map(function(d) {
return {
i: d,
x: Math.random() < 0.5 ? Math.random() * 1000 : Math.random() * -1000,
y: Math.random() < 0.5 ? Math.random() * 1000 : Math.random() * -1000,
}
});
var svg = d3.select("svg"),
margin = {
top: 10,
right: 10,
bottom: 10,
left: 10
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// large "endless" zoom
var zoom = d3.zoom()
.scaleExtent([-1e100, 1e100])
.translateExtent([
[-1e100, -1e100],
[1e100, 1e100]
])
.on("zoom", zoomed);
var x = d3.scaleLinear()
.domain([-100, 100])
.range([0, width]);
var y = d3.scaleLinear()
.domain([-100, 100])
.range([height, 0]);
var xAxis = d3.axisBottom(x)
.ticks((width + 2) / (height + 2) * 10)
.tickSize(-height);
var yAxis = d3.axisRight(y)
.ticks(10)
.tickSize(width)
.tickPadding(8 - width);
var gX = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "axis axis--x")
.call(xAxis);
var gY = svg.append("g")
.attr("class", "axis axis--y")
.call(yAxis);
svg.call(zoom);
// plot our data initially
updateData(x, y);
function zoomed() {
var t = d3.event.transform,
sx = t.rescaleX(x), //<-- rescale the scales
sy = t.rescaleY(x);
// swap out axis
gX.call(xAxis.scale(sx));
gY.call(yAxis.scale(sy));
updateData(sx, sy)
}
// classic enter, update, exit pattern
function updateData(sx, sy) {
// filter are data to those points in range
var f = data.filter(function(d) {
return (
d.x > sx.domain()[0] &&
d.x < sx.domain()[1] &&
d.y > sy.domain()[0] &&
d.y < sy.domain()[1]
)
});
var s = g.selectAll(".point")
.data(f, function(d) {
return d.i;
});
// remove those out of range
s.exit().remove();
// add the new ones in range
s = s.enter()
.append('circle')
.attr('class', 'point')
.attr('r', 10)
.style('fill', 'steelblue')
.merge(s);
// update all in range
s.attr('cx', function(d) {
return sx(d.x);
})
.attr('cy', function(d) {
return sy(d.y);
});
}
</script>
</body>
</html>
&#13;