How to translate svg g elements to cozy up with siblings? - svg

var boxes = [[30, 45], [50, 30], [40, 30]]; // [w, h]
//Should i calculate `translate` value here, adding all heights?
var secs = wrapper.selectAll('g.section')
.data(data)
.enter()
.append('g')
.attr('class', 'section')
.attr("transform", "translate(" + 0 + "," + 'unknown' + ")");
//could be many sub-secs by a lot of data transformation before appending rect to the last one of them.
secs.append('rect')
.datum(function(d){return d;})
.attr('class', 'fragment')
.attr('x', 0)
.attr('y', 0)
.attr('width', function(d){return d[0];})
.attr('height', function(d){return d[1];})
// or here, not from data but from elem's dimensions?
secs.each(function(sec, i){
var prev = this.previousSibling?this.previousSibling.getBBox():'';
var ty = prev?prev.height+ prev.y:0;
d3.select(this).attr("transform", "translate(" + 0 + "," + ty + ")");
});
Is this how you translate g elements to fit their childrens, at any level of depth?
And i'll have to translate them manually when child expands?
i'm new at svg and d3.
Thank you.

It looks like you would need to keep track of the sum of heights and translate by that. That is, for each rectangle that you add add its height to a total which you can then use to offset subsequent elements.

Related

D3 proper text() in tremap

i'm new to d3. My problem is unreadable text.
I suppose it's cuz i added text not to rect but to svg. How do i recode or fix this thing?
https://codepen.io/DeanWinchester88/pen/xxrjLrM
svg
.selectAll("text")
.data(root.leaves())
.enter()
.append("text")
.attr("x", (d) => d.x0 +10)
.attr("y", (d) => d.y0 + 20)
.text( (d) => d.data.name)
.attr("font-size", "12px")
.attr("fill","white")
.attr("overflow", "hidden")
.attr("max-width", (d) => d.x1 - d.x0)
tspan = text.text(null)
.append("tspan")
.attr("x", x)
.attr("y", y)
.attr("dy", dy + "em")
Here's your revised code based on my comment above:
https://codepen.io/mattsrinc/pen/MWozBWQ
svg
.selectAll("text")
.data(root.leaves())
.enter()
.append("text")
.attr('transform', d => 'translate(' + d.x0 + ',' + d.y0 + ')')
.selectAll('tspan')
.data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g))
.enter()
.append('tspan')
.attr('font-size', '0.8em')
.attr('fill', 'white')
.attr('x', function(d) { console.log(d); return '0.5em' })
.attr('y', (d, i) => 12 * (i + 1))
.text(d => d);
I've left the console.log command so that you can see how the (game) name is split into parts. And that Pac-Man is right but it's the only one game for the console category (2600) so it translates to thin black rectangle on top left (something you should consider how to solve it visually).
Then also SVG CSS attribute "overflow: hidden" won't work for your data cells with TSPAN. You would have to handle that with calculating this cell width and width of the current word(s) in a line (of TSPAN tag to add). It would be better to follow and expand your other referenced codepen to work with cells that would ease this task.
Other than that I've just changed the color palette used and you can see color ranges here:
https://github.com/d3/d3-scale-chromatic

SVG g hides when parent g is set to a height

I am relatively new to SVG. I am plotting a chart which will dynamically plot temperature or wind value on x,y axis respectively. I am good with x-axis positioning.
But when it comes to Y-Axis, position come right but if the value is higher than parent chart height, it get cut off as you can see in the picture.
Need help on how to force visible the circle/element even it exceeds the parent element width or height?
Here is how mark up gets generated
I am using d3.js for this.
Here is the code for generating circle with text
let svg = container
.append("svg")
.attr("width", width)
.attr("height", height+(height*0.7));
const chart = svg
.append("g")
.attr('class','chartwind')
.attr("transform", "translate(" + padding.left + "," + (padding.top) + ")")
chart.append("path")
.data(datapoints)
.attr("class", "line wind-line")
.style("stroke", colorScale("wind"))
.style('stroke-width', '2px')
.attr("d", windLine)
let windPoint = chart.selectAll('g.windpoint').data(datapoints).enter().append('g').attr('class', 'windpoint').attr('transform', function (d) {
return 'translate(' + (Math.round(xScale(d.parsedUtcDateTimeNow)) - 10) + ',' + yScale(d.windSpeed) + ')';
})
chart.selectAll('.windcircle')
.data(Infos)
.enter().append('circle')
.attr("class", "windcircle")
.style("stroke", colorScale("wind"))
.style('stroke-width', '2px')
.attr("cx", (d) => xScale(d.parsedUtcDateTimeNow))
.attr("cy", (d) => {
return yScale(d.windSpeed);
})
.attr('r', 3)
.style("fill", colorScale("wind"))
windPoint
.append('circle')
.attr("cx", 0)
.attr("cy", 0)
.attr("r", function (d) {
return 8;
})
.attr("fill", "green")
.attr("transform", "translate(0,-23)")
windPoint
.append('text')
.attr("fill", "white")
.attr("transform", "translate(0,-19)")
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "600")
.text((d) => {
return d.windGust;
});
windPoint
.append('text')
.attr('text-anchor', 'middle')
//.attr("transform","translate(0,5)")
.text((d) => {
return d.windSpeed;
});
Update
This is how I am constructing SVG height and width
function getSizesById(id) {
const container = document.querySelector(`#${id}`)
if (!container)
return null
return {
height: container.clientHeight,
width: container.clientWidth,
}
}
const { width, height } = (() => {
return getSizesById(containerID)
})()
const { width, height } = (() => {
return getSizesById(containerID)
})(),
padding = {
...size.padding,
top: 20,
bottom: 20
},
chartHeight = height - padding.bottom - padding.top,
chartWidth = width - padding.left - padding.right
const container = d3.select(`#${containerID}`)
let svg = container
.append("svg")
.attr("width", width)
.attr("height", height);
Note: SVG height (or chart) should look with in the DIV and only when the data points go over the scale, then we need data to be visible over the div height.
<g> elements do not have any inherent size. They are only a logical wrapper for a group of markup tags, and a place to give them some common properties. What restricts the visible parts of your chart is the <svg> element.
Your code shows that each datapoint is represented by grafic elements that span a bounding box of (-8 -31 16 35) (left - top - width - height). This is how much space you need to show all of it.
Remove the transform attribute from your .chartwind group. The space you need to show the complete graph is
left: lower boundary of your xScale range minus 8
top: lower boundary of your yScale range minus 31 (in the downward coordinate system)
width: extent of your xScale range plus 16
height: extent of your yScale range plus 35
Add some padding if you like.
Add a viewBox attribute with these four numbers to the <svg> element: viewbox="<left> <top> <width> <height>". The area described like this will then be fitted inside the available space of the <svg> element, without you having to do any further figuring out of transformations.

.style of child nodes in d3js

I'm trying to make a simple graph with nodes and links. I have "g" elements containing a circle and its text, and links on their own. I have, for example, this bit on code called on a mouseover event:
//check if circle is connected to current "viewed" (mouseover-ed)
//circle via a link referenced by "that" and paint it green if so
circles.filter(function() {
return d3.select(this).attr("index") == d3.select(that).attr("src");
}).attr("viewed",1).style("stroke", "green");
});
This was really a long shot as nodes is the 'g' element container and I wasn't sure what calling .style would do, but to my surprise it did change the color - but only for the text!
Is there a way to make it change the stroke style of the circle as well?
The declaration code:
var circles = svg.append("g")
.attr("class","nodes")
.selectAll("circle")
.data(graph.nodes)
.enter()
.append("g")
.attr("transform",function(d,i){d.x = getX(i);d.y=getY(i);return "translate(" + d.x + "," + d.y + ")";})
.attr("name", function(d){return d.name;})
.attr("viewed", 0)
.attr("focused", 0)
.attr("index", function(d, i) {return i;});
circles.append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", node_radius_wo_pad)
.on("mouseover", function(){...};
circles.append("text")
.attr("text-anchor","middle")
.text(function(d){return d.name});
The reason this is working is that you haven't explicitly declared a stroke colour for the text and so it inherits what you set for the parent g element. To make this work for the circles, you have to select them explicitly:
var toChange = circles.filter(function() {
return d3.select(this).attr("index") == d3.select(that).attr("src");
});
toChange.attr("viewed", 1);
toChange.selectAll("circle").style("stroke", "green");
toChange.selectAll("text").style("stroke", "green");

Get arrowheads to point at outer edge of node in D3

I'm new to D3 and I'm trying to create an interactive network visualization. I've copied large parts of this example, but I have changed the curved lines to straight ones by using SVG "lines" rather than "paths", and I've also scaled the nodes according to the data they represent. The problem is that my arrowheads (created with SVG markers) are at the ends of the lines. Since some of the nodes are large, the arrows get hidden behind them. I'd like my arrowheads to show up right at the outside edge of the node they point to.
Here is how I'm creating the markers and links:
svg.append("svg:defs").selectAll("marker")
.data(["prereq", "coreq"])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link")
.attr("marker-end", function(d) { return "url(#" + d.type + ")"; });
I noticed that the "refX" attribute specifies how far from the end of the line the arrowhead should show up. How can I make this dependent on the radius of the node it's pointing to? If I can't do that, could I instead change the endpoints of the lines themselves? I'm guessing I would do that in this function, which resets the endpoints of the lines as everything moves:
function tick() {
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
circle.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
text.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
Which approach makes more sense, and how would I implement it?
Thanks Lars Kotthoff, I got this to work following the advice from the other question! First I switched from using lines to paths. I don't think I actually had to do that, but it made it easier to follow the other examples I was looking at because they used paths.
Then, I added a "radius" field to my nodes. I just did this when I set the radius attribute, by adding it as an actual field rather than returning the value immediately:
var circle = svg.append("svg:g").selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", function(d) {
if (d.logic != null) {
d.radius = 5;
} else {
d.radius = node_scale(d.classSize);
}
return d.radius;
I then edited my tick() function to take this radius into account. This required a bit of simple geometry...
function tick(e) {
path.attr("d", function(d) {
// Total difference in x and y from source to target
diffX = d.target.x - d.source.x;
diffY = d.target.y - d.source.y;
// Length of path from center of source node to center of target node
pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY));
// x and y distances from center to outside edge of target node
offsetX = (diffX * d.target.radius) / pathLength;
offsetY = (diffY * d.target.radius) / pathLength;
return "M" + d.source.x + "," + d.source.y + "L" + (d.target.x - offsetX) + "," + (d.target.y - offsetY);
});
Basically, the triangle formed by the path, it's total x change (diffX), and it's total y change (diffY) is a similar triangle to that formed by the segment of the path inside the target node (i.e. the node radius), the x change inside the target node (offsetX), and the y change inside the target node (offsetY). This means that the ratio of the target node radius to the total path length is equal to the ratio of offsetX to diffX and to the ratio of offsetY to diffY.
I also changed the refX value to 10 for the arrows. I'm not sure why that was necessary but now it seems to work!
I answered the same question over here. The answer uses vector math, it's quite useful for other calculations as well.

How do I adjust my SVG transform based on the viewport?

I'm working with the d3 library and have had success working with the chloropleth example, as well as getting a click action to zoom in to a particular state (see this question for details). In particular, here is the code I'm using for my click to zoom event on a state:
// Since height is smaller than width,
var baseWidth = 564;
var baseHeight = 400;
d3.selectAll('#states path')
.on('click', function(d) {
// getBBox() is a native SVG element method
var bbox = this.getBBox(),
centroid = [bbox.x + bbox.width/2, bbox.y + bbox.height/2],
// since height is smaller than width, I scale based off of it.
zoomScaleFactor = baseHeight / bbox.height,
zoomX = -centroid[0],
zoomY = -centroid[1];
// set a transform on the parent group element
d3.select('#states')
.attr("transform", "scale(" + scaleFactor + ")" +
"translate(" + zoomX + "," + zoomY + ")");
});
However, when I click to view on the state, my transform is not in the center of my viewport, but off to the top left, and it might not have the proper scale to it as well. If I make minor adjustments manually to the scaleFactor or zoomX/zoomY parameters, I lose the item altogether. I'm familiar with the concept that doing a scale and transform together can have significantly different results, so I'm not sure how to adjust.
The only other thing I can think of is that the original chloropleth image is set for a 960 x 500 image. To accomodate for this. I create an albersUSA projection and use my d3.geo.path with this projection and continue to add my paths accordingly.
Is my transform being affected by the projection? How would I accomodate for it if it was?
The scale transform needs to be handled like a rotate transform (without the optional cx,cy parameters), that is, the object you want to transform must first be moved to the origin.
d3.select('#states')
.attr("transform",
"translate(" + (-zoomX) + "," + (-zoomY) + ")" +
"scale(" + scaleFactor + ")" +
"translate(" + zoomX + "," + zoomY + ")");
For futher reference,
I found this article where you should find how to use the matrix transformation to achieve zoom and pan effects very simple.
Excerption:
<script type="text/ecmascript">
<![CDATA[
var transMatrix = [1,0,0,1,0,0];
function init(evt)
{
if ( window.svgDocument == null )
{
svgDoc = evt.target.ownerDocument;
}
mapMatrix = svgDoc.getElementById("map-matrix");
width = evt.target.getAttributeNS(null, "width");
height = evt.target.getAttributeNS(null, "height");
}
]]>
</script>
function pan(dx, dy)
{
transMatrix[4] += dx;
transMatrix[5] += dy;
newMatrix = "matrix(" + transMatrix.join(' ') + ")";
mapMatrix.setAttributeNS(null, "transform", newMatrix);
}
function zoom(scale)
{
for (var i=0; i<transMatrix.length; i++)
{
transMatrix[i] *= scale;
}
transMatrix[4] += (1-scale)*width/2;
transMatrix[5] += (1-scale)*height/2;
newMatrix = "matrix(" + transMatrix.join(' ') + ")";
mapMatrix.setAttributeNS(null, "transform", newMatrix);
}

Resources