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

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

Related

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

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

D3.js spawn new circles at fixed time intervals with forceSimulation

Hi I am trying to spawn new circles in a set time interval (e.g., double the amount of existing circles every ten seconds) with D3.forceSimulation. I am using forceSimulation to make sure the circles do not overlap. My goal is to also have the new circles spawn in a position near the existing circles.
My initial approach is to append new {} elements into the nodes array using a setInterval function. Basically check the length of the nodes array every ten seconds and append new {} elements so that the new length of the array is double the previous length.
However, I don't think I am understanding/using nodes and d3.forceSimulation correctly. In the code below I see five circles appearing and moving away from each other. But I didn't pass any x or y positions to the circle elements that are joined to the nodes data? Are default/random positions being assigned to the circles?
I know if I add .force('center', d3.forceCenter(width / 2, height / 2)) the five circles will appear near the center of the screen before moving away. But I'm not sure how d3.forceSimulation is setting the initial positions of the five circles when they spawn in.
var nodes = [{}, {}, {}, {}, {}]
var simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody())
.on('tick', ticked);
function ticked() {
var u = d3.select('svg')
.selectAll('circle')
.data(nodes)
u.enter()
.append('circle')
.attr('r', 5)
.merge(u)
.attr('cx', function(d) {
return d.x
})
.attr('cy', function(d) {
return d.y
})
u.exit().remove()
}
Yes, there are default position which are assigned to the circles.
Here is a quote from d3js docs:
The position ⟨x,y⟩ and velocity ⟨vx,vy⟩ may be subsequently modified
by forces and by the simulation. If either vx or vy is NaN, the
velocity is initialized to ⟨0,0⟩. If either x or y is NaN, the
position is initialized in a phyllotaxis arrangement, so chosen to
ensure a deterministic, uniform distribution around the origin.

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

Adding a drag icon to Focus+Context via Brushing Chart

I am trying to adapt Mike Bostock's Focus+Context via Brushing chart at: bl.ocks.org/mbostock/1667367 ‎ to include a drag icon on both vertical lines of the brush rectangle. These should appear once a selection is made and act as a visual cue to shrink or expand the selected/brushed area. I see the placement of the images being dynamic i.e. moving fluidly with the brushed area as opposed to an update after the brushed area is reset. What seems most reasonable to me would be to add an svg image to the context rectangle like so:
//original code
context.append("g")
.attr("class", "x brush")
.call(brush)
.selectAll("rect")
.attr("y", -6)
.attr("height", height2 + 7)
//additional code
.append("svg:image")
.attr("xlink:href", "icon.png")
.attr("width", 10)
.attr("height", 10)
.style("opacity",1)
I've tried playing around with the x and y positioning of both images with no luck getting them to appear, but i conceptually see it working as
y axis: height of context chart divided by 2
x axis: each image respectively corresponding to the min and max x values of the brushed area
Any help would be appreciated.
Lars, thanks for the pointer which generally led me in the right direction although I ended up directly adapting from an example at https://engineering.emcien.com/2013/05/7-d3-advanced-brush-styling. That example is a bit more complex so i borrowed from a subset relevant to my purposes.
Steps i followed include
1.) Creating two images appended to the context g element and initializing their position somewhere that doesn't give the impression that the chart is brushed on loading {i put them halfway (vertically) and close together around the end of the context(horizontally)}.
var leftHandle = context.append("image")
.attr("xlink:href", "icon.gif")
.attr("width", 11)
.attr("height", 27)
.attr("x",x2(data[data.length-6].date))
.attr("y", (height2/2)-15);
var rightHandle = context.append("image")
.attr("xlink:href", "icon.gif")
.attr("width", 11)
.attr("height", 27)
.attr("x",x2(data[data.length-1].date))
.attr("y", (height2/2)-15);
2.) Within the brush function, i created a variable to hold brush.extent(), then tied the x attributes of the images to its min and max x values.
var ext = brush.extent();
leftHandle.attr("x",x2(ext[0]));
rightHandle.attr("x",x2(ext[1]));
One things i'm not completely satisfied with is that when i initially created the images after the brush rectangle, they sat on top of it, preventing me from being able to brush if i hovered over the images (which is the intuitive reaction desired). I ended up placing the images behind the rectangle which has a low opacity. Not the 100% accurate visual representation sought, but it works just fine.

draw a grid or rectangles using a scale

I'm building my first line graph in d3:
http://jsfiddle.net/j94RZ/
I want to know how to utilize either the scale or axis allow me to draw a grid (of, presumably rectangles) where I can set a different background colour for each of the section of the grid...so I can alternate colours for each cell of the grid. I want the grid to be drawn and be constrained by the axes of my graph and then also adapt if the spacing of the axes ticks change (i.e. the axes changes like this: http://bl.ocks.org/mbostock/1667367). So if my graph has an x axis with 4 ticks and a y axis of 7 ticks then my graph will have a background grid that's 7 blocks high and 4 blocks wide.
I've been playing with the idea of using a range which starts at zero and ends at the full width of the graph but I don't know what value I can use for the step. Is there any way to sort of query the axis and return how many ticks there are?
var gridRange = d3.range(0, width, step?);
A better approach than your current solution would be to use scale.ticks() explicitly to get the tick values. The advantage of that is that it will still work if you change the number of ticks for some reason.
To get an alternating grid pattern instead of a single fill, you can use something like this code.
.attr("fill", function(d, i) {
return (i % 2) == 1 ? "green" : "blue";
})
Finally, to get the full grid pattern, you can either use an explicit loop as you've suggested, or nested selections. The idea here is to first pass in the y ticks, create a g element for each and then pass the x ticks to each one of these groups. In code, this looks something like this.
svg.selectAll("g.grid")
.data(y.ticks()).enter().append("g").attr("class", "grid")
.selectAll("rect")
.data(x.ticks()).enter().append("rect");
To set the position, you can access the indices within the top and bottom level data arrays like this.
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d, i, j) {
return yScale(j);
})
To set the x position, you need the index of the inner array (passed to the set of g elements), which can be accessed through the second argument of your callback. For the outer array, simply add another argument (j here).
And that's really all there is to it. Complete jsfiddle here. To update this grid dynamically, you would simply pass in the new tick values (gotten from scale.ticks()), match with the existing data, and handle the enter/update/exit selections in the usual manner.
If you want to do without the auxiliary scales (i.e. without .rangeBand()), you can calculate the width/height of the rectangles by taking the extent of the range of a scale and dividing it by the number of ticks minus 1. Altogether, this makes the code a bit uglier (mostly because you need one fewer rectangle than ticks and therefore need to subtract/remove), but a bit more general. A jsfiddle that takes this approach is here.
So after a few helpful comments above I've got close to a solution. Using Ordinal rangebands get me close to where I want to go.
I've created the range bands by using the number of ticks on my axis as a basis for the range of the input domain:
var xScale = d3.scale.ordinal()
.domain(d3.range(10))
.rangeRoundBands([0, width],0);
var yScale = d3.scale.ordinal()
.domain(d3.range(4))
.rangeRoundBands([0, height],0);
I've then tried drawing the rectangles out like so:
svg.selectAll("rect")
.data(p)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d,i) {
0
})
.attr("width", xScale.rangeBand())
.attr("height", yScale.rangeBand())
.attr("fill", "green").
attr('stroke','red');
This gets me the desired effect but for only one row deep:
http://jsfiddle.net/Ny2FJ/2/
I want,somehow to draw the green blocks for the whole table (and also without having to hard code the amount of ticks in the ordinal scales domain). I tried to then apply the range bands to the y axis like so (knowing that this wouldn't really work though) http://jsfiddle.net/Ny2FJ/3/
svg.selectAll("rect")
.data(p)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d,i) {
return yScale(i);
})
.attr("width", xScale.rangeBand())
.attr("height", yScale.rangeBand())
.attr("fill", "green").
attr('stroke','red');
The only way I can think to do this is to introduce a for loop to run the block of code in this fiddle http://jsfiddle.net/Ny2FJ/2/ for each tick of the y axis.

Resources