D3.js v7: interpolate translate/scale with new position? - d3.js

I have a graph of several nodes and edges. I used a layout algorithm (elkjs) to calculate the position of the nodes and edges. The graph is fairly large, with 268 nodes and 276 edges, such that:
svg width: 800, height: 600; graph width: 1844, height: 3007
As such, I had to do the requisite math to calculate pan offsets and scaling so it would fully fit, centered in the viewport:
translate(225.228, 15) scale(0.1896)
I programmatically transition it into place over 2500ms - it works fine and fits nicely.
But then I wish to switch to a subgraph, picking a node to see only it and its descendants. This particular example subgraph is near the bottom of the graph, so the starting position of its y-values are relatively large. After I call the layout algorithm again to get the new positions, the repositioned graph is smaller:
svg width: 800, height: 600; graph width: 874, height: 459
I then do the join/enter/update/exit thing to update the positions over a duration (and remove the other elements not in the subgraph), and I also do the math again for pan offsets and scaling:
translate(20, 100.4348) scale(0.8696)
Here's the problem: while it does end up in the right place, it pans off-screen before panning back on-screen:
I think I see why: the re-positioning transition has the same duration (2500ms) as the panning/scaling, and something about that all combined has that undesirable effect. I'm able to keep the subgraph on-screen by making the positioning happen faster:
rects
.transition().duration(1500) // instead of 2500
.attr('x', (d) => d.x ?? 0)
.attr('y', (d) => d.y ?? 0)
but that's of course not a universal solution, and it kind of moves around haphazardly. I'd rather have a way to interpolate the re-positioning with the translate so it smoothly grows and pans to be centered without going off-screen. Is this possible? I'm aware of d3.interpolate and scaleExtent/translateExtent but am stumped on how to use them - so far I haven't figured out how to account for both repositioning and translate/scale at the same time.
Other relevant bits of code:
<svg
style={{ borderStyle: "Solid", borderWidth: "1px" }}
width="800"
height="600"
onClick={onClickHandler} <!-- this just switches to the subgraph -->
>
<g />
</svg>
const gr = d3.select('g');
const rects = gr.selectAll<SVGSVGElement, ElkNode>('rect').data(elkGraph.children, (d) => d.id);
// update, enter, exit logic omitted; see above for position update
// offset and scale logic omitted
gr.transition().duration(2500)
.attr('transform', `translate(${xCenterOffset}, ${yCenterOffset}) scale(${scaleFactor})`)

Related

dc.js heatmap - make the top row rects to begin at y="0"

I have a dc.js heatmap working:
But I want to add grid lines to it, like so:
You can see that the lines to not match up with the bottom edges of the rects. Inserting the lines themselves is easy, you just start at zero and add 11 lines based on the height of the rects, which in this case will always be 11 / chart.effectiveHeight().
The reason they do not match up, seems to be that the top rect row does not always start at 0, instead, there seems to be a random(?) y position that the chart starts at, this will change with the height of the chart container, eg this y position starts at 5:
If it was consistent, then I could just start appending lines from that number instead of 0, but it is not. I have tried a couple of hacky work arounds, however I am unsure as to how to get the y position of all the rects after they are available in the DOM.
Interestingly the demo heatmap does not have this issue:
Here is the code for the heatmap:
const heat_map = dc.heatMap('#heatmap');
heat_map
.width(0)
.height(0)
.margins(margins)
.dimension(hm_dim)
.group(hm_group)
.keyAccessor(function(d) { return +d.key[0]; })
.valueAccessor(function(d) { return +d.key[1]; })
.colorAccessor(function(d) { return +d.value; })
.colors(color_scale)
.calculateColorDomain()
.yBorderRadius(0)
.xBorderRadius(0)
heat_map.render();
Is there a way to force the rects to begin at 0? Or get the random y position for the top rows? I did have a look at the source code but got a bit lost. Also I thought about creating a false group that would include each rect in the grid, and the grid lines could then be rect borders, but I thought that was a bit heavy handed.
Outlining the cells using CSS
It's easy to outline the cells using CSS:
rect.heat-box {
stroke-width: 1;
stroke: black;
}
Example fiddle.
However, as you point out, this only works if all the cells have values; crossfilter will not create the empty ones and I agree it would be absurd fill them in using a fake group just for some lines.
So, to answer your original question...
Why is there a gap at the top of the chart?
The heatmap calculates an integer size for the cells, and there may be space left over (since the space doesn't divide perfectly).
It's kind of nasty but the heatmap example avoids having extra space by calculating the width and height for the chart using the count of cells in each dimension:
chart
.width(45 * 20 + 80)
.height(45 * 5 + 40)
The default margins are {top: 10, right: 50, bottom: 30, left: 30} so this allocates 45x45 pixels for each cell and adds on the margins to get the right chart size.
Since the heatmap in this example draws 20 columns by 5 rows, it will calculate the cell width and height as 45.
Alternative Answer for Responsive/Resizable Charts
I am revisiting this question after rewriting my heatmap chart to be responsive - using the "ResizeObserver" method outlined in the dc.js resizing examples and Gordon's answer to this question
While specifying the chart width and height for the heatmap in Gordon's answer still works, it does not combine well with the resizing method because resized charts will have their .width and .height set to 'null'. Which means that this rounding issue will reoccur and the heat boxes will be again be offset by a random integer x or y value of anywhere between 0 and 5 (unless you want to write a custom resizing function for heatmaps).
The alternative answer is relatively simple and can be determined by selecting just one heat-box element in the heatmap.
The vertical offset value for the heat boxes is the remainder value when the heat-box y attribute is divided by the heat-box height attribute.
const heatbox_y = heat_map.select('.heat-box').attr('y);
const heatbox_height = heat_map.select('.heat-box').attr('height')
const vertical_offset = heatbox_y % heatbox_height
The modulus % will return the remainder.
The horizontal offset can be determined in the same way.
Thus you can append lines to the chart at regular intervals determined by the heatbox_height + the vertical_offset values.
This will work if you pick any heat-box in the chart, and so it is suitable for instances like this where you cannot guarantee that there will be a heat-box at each x or y level. And it means that you are free to set your chart height and width to 'null' if needed.

How to get intersection of link-edge and node in d3js?

I want to have many node shapes (circle, square...) Here is my JSfiddle prototype the problem is arrow placings:
They are created like this in js:
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', 'red');
//...
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link");
and css:
.link {
stroke: #7a4e4e;
stroke-width: 3px;
stroke-opacity: 1;
marker-end: url(#end-arrow);
}
Arrows shall be where I drew green marks, yet they are in the centre (red marks). They are oriented correctly, yet misplaced. How to make arrows be on the intersection of link-edge and node in d3js?
You can use the function
route = cola.makeEdgeBetween(source: Rectangle, target: Rectangle, ah: number)
where your source and target are the bounds of the nodes and ah is some offset for the size of the arrow.
this will give you 3 coordinates
* #return An object with three point properties, the intersection with the
* source rectangle (sourceIntersection), the intersection with then
* target rectangle (targetIntersection), and the point an arrow
* head of the specified size would need to start (arrowStart).
and you can subsequently draw a line with the sourceIntersection and arrowStart coordinates
however this nodes are all considered rectangular in webcola, so for your circular nodes they will be stopping at the edge of the rectangle that bounds that circle, if you dont want that you would have to compensate for it by increasing the length of the line by calculating that distance

how do i set the pixel value of radius in bubbleChart?

I want to set the pixel radius of the bubbles on my bubbleChart. All bubbles will be 3px when bubbles represent companies, but 6px when they represent portfolios, and so on for a total of four categories. So the user can aggregate the data behind the bubbles at different levels, and the size of the bubbles should represent that.
Problem is I am struggling to set the radius in pixels.
bubbleChart.width(738)
.height(315)
.margins({left:40,right:30,top:15,bottom:30})
.dimension(idDimBubble)
.group(idGrpBubble)
.clipPadding(10)
.elasticY(true)
.elasticX(true)
//.mouseZoomable(true)
// .elasticRadius(true)
.renderLabel(false)
.keyAccessor(function (p) { return p.value[selectArrays[0][1]] / p.value.name.length; })
.valueAccessor(function (p) { return p.value[selectArrays[0][0]] / p.value.name.length; })
.radiusValueAccessor(function (p) { return (filters[0].indexOf(window.gran)+1); })// this outputs a digit of 1,2, 3 or 4. I want that to correspond to a radius of 3,6,9,12.
//.maxBubbleRelativeSize(0.05)
.x(d3.scale.linear().domain([0, 100]))
.y(d3.scale.linear().domain([0, 100]))
.r(d3.scale.linear().domain([1, 4]).range([1,4]))
.renderHorizontalGridLines(true)
.renderVerticalGridLines(true);
I would think the code above would output a pixel radius of between 1 and 4px. Instead, I have to maxBubbleRelativeSize to suppress the size because all four bubbles are too big. Even the smallest bubble is more like a radius of 10 not 1px.
Does anyone know how I can set the actual pixel size directly? Thanks

d3.geo.path rectangle wrapping the wrong way

This simple geojson rectangle is displayed correctly with some geojson viewers, I get a rectangle as expected. But when I do it with d3, the rectangle seems to wrap around.
var polygonData = {
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ -100, -20 ],
[ -100, 20 ],
[ 100, 20 ],
[ 100, -20 ],
[ -100, -20 ]
]
]
},
"properties": {}
};
var width = 1000;
var height = 500;
var projection = d3.geo.equirectangular()
.scale(100)
.translate([width / 2, height / 2])
.rotate([0, 0])
.center([0, 0])
.precision(0);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr({
width: width,
height: height
});
svg.append('path')
.datum(polygonData)
.attr({
d: path,
fill: 'orange',
opacity: 0.5
});
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<style>
</style>
</head>
<body>
</body>
Here is what I get with a geojson viewer:
But here's what I get with the above d3 code:
Reversing the winding order just fills the opposite shapes, it doesn't fix the problem. I guess its an antimeridian cutting issue. One fix is to add some intermediate points to force the path to not wrap around, but I would need to be able to automate this solution with more complex paths.
Any idea how I can use this geojson with d3 and force it to show it like other geojson viewers, as a simple rectangle across the map?
I don't think, there is anything to blame on D3; in my understanding it's those other GeoJSON viewers which are in error. As a human being living on a more or less planar surface one is easily tricked into believing that a polygon with four corners having carefully chosen coordinates like yours should look like a rectangle. On a sufficiently small scale and given a suitable projection this even holds true for spherical geometry. But as your points are almost half the globe apart, this won't be the case.
To shed some light on this, I used an orthographic projection to display some geographic features along with your polygon:
From this view it becomes apparent that the line along the meridian is the first edge connecting points [-100,-20] and [-100,20]. From that point [-100,20] somewhere in Mexico to the northwest is the great arc, i.e. the shortest connection, to the next point [100,20] half way around the globe. The path is then similarly closed around the southern hemisphere. Thus, the outline of the polygon is the shortest path on the globe's surface connecting all of its points in the given order.
Although your polygon is determined by its coordinates, its look will depend on the projection in use. Here is another view of the same polygon using a mercator projection:

How to add a rectangle to specified axis in D3

I have a zoomable area plot done in D3, which works well. Now I am trying to add a rectangle to the specified location along x-axis in the middle of the plot. However, I can't seem to figure out how to do that. "rect" element is specified using absolute (x,y) of the plot and so when using zooms it stays in the same position.
So I was wondering if there is a way to tie "rect" to the axis when plotting, so that it benefits from all the zoom and translate behaviour or do I need to manually edit the x,y,width and length of the rectangle according to translation as well as figuring out where the corresponding x and y coordinates are on the graph? I am trying to use "rect" because it seems the most flexible element to use.
Thanks
Alex
I'm not sure how you are doing the zooming, but I am guessing you are changing the parameters of the scales you use with your axis? You should be able to use the same scales to place your rectangle.
If you are starting with plot coordinates then maybe using the invert function on the scale will help (available at least for quantitive scales), e.g. https://github.com/mbostock/d3/wiki/Quantitative-Scales#wiki-linear_invert
You should be able to take initial plot coordinates and invert them to determine data coordinates that can then move with changes in the scale.
If the scale is linear you can probably invert the length and width too, but you will have to compute offsets if your domain does not include 0. Easiest is to compute the rectangle's end points, something like:
var dataX0 = xScale.invert(rect.x);
var dataX1 = xScale.invert(rect.x + rect.width);
var dataWidth = dataX1 - dataX0;
If you have the data in axes coordinates already you should be able to do something like:
var rectData = [{x: 'April 1, 1999', y: 10000, width: 100, height:100}];
svg.selectAll('rect.boxy')
.data(rectData)
.enter().append('rect').classed('boxy', true)
.style('fill','black');
svg.selectAll('rect.boxy')
.attr('x', function(d) { return x(new Date(d.x));} )
.attr('y', function(d) { return y(d.y);})
.attr('width', function(d) { return d.width;} )
.attr('height', function(d) { return d.height;} );
Based on the example you shared where x and y (as functions) are the scales the axes are based on.

Resources