Creating an infinite horizontal axis with d3 v4

I am using d3 v4 (4.12.0).

I have an SVG container in which I draw a simple horizontal axis (X axis, linear scale) that responds to mouse panning.

I would like to simulate an “infinite” or “infinite” horizontal axis.

By this, I mean that I only want to load and display a small part of a very large dataset and only draw an axis that shows a very small subset of the elements from this large set.

Say I have a horizontal axis that shows 10 data points from a larger array of objects. I hold an offset parameter that starts at 0 to show the first ten points of this array.

My procedure:

When I scroll the axis to the left far enough to show the 11th and subsequent data point, I then:

  • Update the offset parameter to display how many units I have converted.

  • Update x-axis scale based on new offset value

  • Redraw axis labels with updated scale range ( x_scale )

  • Translate the group element containing the axis by the number of pixels that represent one unit on the axis ( scroller_element_width )

My attempt works until step 3. This process seems to fail in step 4, since the final axis translation never happens.

The entire axis moves to the left, and it has fresh labels, but it does not move to the right with these updated labels - it basically falls from the page.

I would like to ask d3 experts why this step fails and what I can do to fix it.

Here is a function that draws an axis and attaches a zoom event:

  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); } } 

This is a function inside the React component. React stuff is not that important, but here is the render() function of this component to show the parent elements of the SVG and child group:

  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> ); } 

As shown, scrollerContainer ref is an SVG containing an element of the scrollerContent group. This scrollerContent is what contains the horizontal axis.

When panning or scrolling scrollerContent X axis, transformations are applied to scrollerContent .

To get the conversion parameters, I use the helper method parseSvg from d3-interpolate , i.e. through ES6:

 import * as d3 from 'd3'; import { parseSvg } from "d3-interpolate/src/transform/parse"; 

For completeness, here is a snippet of test data:

 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" } ]; 

Hope this shows all the work needed to explain the problem. Thanks for any tips or advice.

+5
source share
1 answer

I found that your code is difficult to execute without a complete reproducible example. So I coded a simple example of what you are trying to do. Perhaps this will help:

 <!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 ( dx > sx.domain()[0] && dx < sx.domain()[1] && dy > sy.domain()[0] && dy < sy.domain()[1] ) }); var s = g.selectAll(".point") .data(f, function(d) { return di; }); // 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(dx); }) .attr('cy', function(d) { return sy(dy); }); } </script> </body> </html> 
+4
source

Source: https://habr.com/ru/post/1273822/


All Articles