d3 path gradient stroke - javascript

Hello I am working with d3 diagonal diagram and would like to add a gradient to path which links my circles...
I am generating my tree with:
var width = 800,
height = 700;
element.html('');
var color = d3.interpolateLab("#008000", "#c83a22");
var scale = d3.scale.linear().domain([0, 100]).range(["red", "green"]);
var cluster = d3.layout.cluster()
.size([height, width - 160]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select('#tab-manageAccess').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(40,0)');
/*svg.append("linearGradient")
.attr("id", "line-gradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0).attr("y1", y(0))
.attr("x2", 0).attr("y2", y(1000))
.selectAll("stop")
.data([
{offset: "0%", color: "red"},
{offset: "40%", color: "red"},
{offset: "40%", color: "black"},
{offset: "62%", color: "black"},
{offset: "62%", color: "lawngreen"},
{offset: "100%", color: "lawngreen"}
])
.enter().append("stop")
.attr("offset", function(d) { return d.offset; })
.attr("stop-color", function(d) { return d.color; });*/
var nodes = cluster.nodes(scope.accessTree),
links = cluster.links(nodes);
var link = svg.selectAll('.link')
.data(links)
.enter().append('path')
.attr('class', 'link')
.attr('d', diagonal);
var node = svg.selectAll('.node')
.data(nodes)
.enter().append('g')
.attr('class', 'node')
.attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; });
node.append('circle')
.attr('r', 4.5);
node.append('text')
.attr('dx', function(d) { return d.children ? -8 : 8; })
.attr('dy', 3)
.style('text-anchor', function(d) { return d.children ? 'end' : 'start'; })
.style('font-weight', 'bold')
.attr('fill', function (d) {
var color = '#4D7B88';
if (d.depth === 0) {
color = '#7F3762';
} else if(d.depth === 1) {
color = '#83913D';
}
return color;
})
.text(function(d) { return d.name; });
d3.select(self.frameElement).style('height', height + 'px');
I found this example: https://gist.github.com/mbostock/4163057 co I created variable color with d3.interpolateLab("#008000", "#c83a22"); and then added .style("fill", function(d) { return color(d.t); })
.style("stroke", function(d) { return color(d.t); }) to path element but it doesn't work :( can anyone help me?

The aspect of Mike Bostock's code that you're missing is where he divides the path up into hundreds of different sub-paths and sets the color on each one separately. Go to the live version at http://bl.ocks.org/mbostock/4163057 and check the DOM to see what's really going on.
Why does he do that? Because, while you can set the stroke of an SVG line or path to a gradient, you can't tell it to make the gradient follow the slope or curve of that line. The angle of the gradient is defined when the gradient is created, based on either:
the rectangular bounding box for the element that uses it
(if gradientUnits is set to ObjectBoundingBox), or
the user coordinate system where the object is drawn
(if gradientUnits is set to userSpaceOnUse).
The way you have it set up (in your commented out code) basically creates a hidden gradient background over the entire image, and then lets it show through wherever you draw your lines. Clearly not what you wanted.
Hence, Mike's complex function and the hundreds of sub-paths it creates. Probably not what you want, either, especially if you want the graph to be interactive.
For simple lines, there is another way to get gradients to line up correctly from start to finish of your line.
I've got a very simple example with plain SVG (no D3) up here: http://codepen.io/AmeliaBR/pen/rFtGs
In short, you have to define your line to go in the direction that matches up with the gradient, and then use transforms (scale/rotate/translate) to actually position the line where you want it.
How tricky that would be to implement in D3 depends on how complex your layout is. If you were just using simple lines, I think this would work:
calculate the length of the line and its slope using simple geometry from the (x1,y1) and (x2,y2) values,
draw the line from (0,0) to (0,length) (assuming a vertical gradient),
add a transform attribute of translate(x1,y1) rotate(slope)
With paths, you'd need to know what type of path you're dealing with and use regular expressions to parse and edit the path's d attribute. Very messy.
Maybe just try line markers for start and end?

Related

how to obtain a complete clockwise rotation in D3

I want a square to complete a full clockwise rotation on itself, after a pause on the half of the rotation.
The following code makes it doing an half rotation clockwise, and the other half counter-clockwise, contrary to what I expect.
var svg = d3.select('svg');
var s = svg.append("rect")
.attr("width", 50)
.attr("height", 50)
.attr("x", -25)
.attr("y", -25)
.attr("fill", "red")
.attr("transform", "translate(100,100)");
s
.transition()
.duration(1000)
.attr("transform", "translate(100,100) rotate(180)")
.transition()
.delay(1000)
.duration(1000)
.attr("transform", "translate(100,100) rotate(360)");
* {
margin: 0;
padding: 0;
border: 0;
}
body {
background: #ffd;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
I can hack such a code splitting the second half rotation in two quarter clockwise rotations, but I wish to know if there is a more elegant solution.
The culprit here is D3 itself, not any SVG spec.
The problem is that your transition uses d3.interpolateTransform, as we can see here:
var fullname = namespace(name), i = fullname === "transform" ? interpolateTransform : interpolate;
This is v4 source code, not v3, but the principle is the same, as you can see in the actual v3 code:
var interpolate = nameNS == "transform" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS);
Then, if we look in the source code for interpolateTransform (again, v4, but v3 is almost the same), we'll see that it uses a function called parseSvg that calculates the matrix for the new transform:
function parseSvg(value) {
if (value == null) return identity;
if (!svgNode) svgNode = document.createElementNS("http://www.w3.org/2000/svg", "g");
svgNode.setAttribute("transform", value);
if (!(value = svgNode.transform.baseVal.consolidate())) return identity;
value = value.matrix;
return decompose(value.a, value.b, value.c, value.d, value.e, value.f);
}
That function is generating 0 as the final value in the matrix when you pass rotate(360) to it (the actual value is -2.4492935982947064e-16, which is practically zero).
Solution
There are several possible solutions here, the easiest one is using interpolateString instead of interpolateTransform.
Also, since your code uses D3 v3, you can take advantage of d3.transform(), which was removed in v4/v5:
d3.interpolateString(d3.transform(d3.select(this).attr("transform")), "translate(100,100) rotate(360)")
Here is your code with that change:
var svg = d3.select('svg');
var s = svg.append("rect")
.attr("width", 50)
.attr("height", 50)
.attr("x", -25)
.attr("y", -25)
.attr("fill", "red")
.attr("transform", "translate(100,100)");
s.transition()
.duration(1000)
.attr("transform", "translate(100,100) rotate(180)")
.transition()
.delay(1000)
.duration(1000)
.attrTween("transform", function() {
return d3.interpolateString(d3.transform(d3.select(this).attr("transform")), "translate(100,100) rotate(360)")
});
<svg></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
If I use D3v3 it does not rotate to 180 degrees, if I switch to D3v4 it rotates to 180.
You can interpolate to 359.99. It does not follow the string interpolator from the docs, because you also get scale() in the transform.
translate(100, 100) rotate(359.989990234375) scale(0.999,0.999)
This does not happen if you write your own interpolator.
var svg = d3.select('svg');
var s=svg.append("rect")
.attr("width",50)
.attr("height",50)
.attr("x",-25)
.attr("y",-25)
.attr("fill","red")
.attr("transform","translate(100,100) rotate(0)");
s
.transition()
.duration(3000)
.ease(d3.easeLinear)
.attr("transform","translate(100,100) rotate(180)")
.transition()
.delay(2000)
.duration(3000)
.ease(d3.easeLinear)
.attrTween("transform", () => d3.interpolate("translate(100,100) rotate(180)", "translate(100,100) rotate(360)") );
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

geographic sort function for d3's pack layout

Using d3's pack layout, I made some bubbles associated with states. Current test script: https://jsfiddle.net/80wjyxp4/4/. They're colored according to region. You'll note Texas is in the "NW", California in the "SE", etc.
Question:
How would you geographically sort the circles in pack-layout?
One hack way might use the default d3.layout.pack.sort(null). This sort starts with the first data point (in this case, AK) and then adds bubbles in a counterclockwise direction. I could hand-sort data to be input in an order that approximates state positions, and add in blank circles to move edge circles around.
I'm interested about better ideas. Would it be better to use a modified force layout, like http://bl.ocks.org/mbostock/1073373? The d3.geom.voronoi() seems useful.
On this http://bl.ocks.org/mbostock/1073373 look at these lines:
states.features.forEach(function(d, i) {
if (d.id === 2 || d.id === 15 || d.id === 72) return; // lower 48
var centroid = path.centroid(d); // <===== polygon center
if (centroid.some(isNaN)) return;
centroid.x = centroid[0]; <==== polygon lat
centroid.y = centroid[1]; <==== polygon lng
centroid.feature = d;
nodes.push(centroid); <== made node array of centroids
});
----------------
force
.gravity(0)
.nodes(nodes) <==== asign array nodes to nodes
.links(links)
.linkDistance(function(d) { return d.distance; })
.start();
Each state acts like a multi-foci layout. Like this one: http://bl.ocks.org/mbostock/1021841
Started with pack layout, to get circle radii
used state centroids from this modified force layout http://bl.ocks.org/mbostock/1073373
Contrived to avoid overlaps using collisions https://bl.ocks.org/mbostock/7881887
The code is here: https://jsfiddle.net/xyn85de1/. It does NOT run because I can't get data for the topojson file, at http://bl.ocks.org/mbostock/raw/4090846/us.json, from the link, and the file is too large to copy-paste. Download to your own server then run.
It has a hover-title text and transitions in, but ends up looking like this:
Spacing and stuff is modifiable with different parameters. The circles are approximately sorted geographically. MO is that big gold one in the middle; AK and HI are dark blue to the left; CA is lower left in pink; TX is at the bottom in light blue. And so on.
Code:
After collecting all data (location data for init position of circles), along with the state name, value, area (for color coding) into the nodes variable:
// circles
var circles = svg.selectAll("g") //g
.data(nodes)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + -d.x + "," + -d.y + ")";
})
.append("circle")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.attr("r", function(d) {return d.r;})
.attr("fill", function(d) { return color(d.area); })
.call(force.drag); // lets you change the orientation
// title text
circles.append("title")
.text(function(d) { return d.state + ": " + format(d.value) + " GWh"; });
// // text // doesn't work :/ porque?
// circles.append("text")
// .attr("dy", ".3em")
// //.style("text-anchor", "middle")
// .text(function(d) { return d.state.substring(0, d.r / 3); });
// circle tick function
function tick(e) {
circles
.each(collide(collision_alpha))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// force
force.nodes(nodes)
.on("tick", tick)
.start();

Why is d3 ignoring my color array?

I'm trying to build out a simple color chart, as an introductory d3 exercise, and I'm already stuck.
I have the following:
var colors = ["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#2c7fb8","#253494"];
var barHeight = 20,
barWidth = 20,
width = (barWidth + 5) * colors.length;
d3.select("body").selectAll("svg")
.data(colors)
.enter().append("rect")
.attr("class", "block")
.attr("width", barWidth)
.attr("height", barHeight - 1)
.text(function(d) { return d; })
.attr("fill", function(d) { return d; });
https://jsfiddle.net/xryamdkf/1/
The text works fine. I see the hex codes, but the height and width are definitely not respected, and I can't seem to set the color.
This works to set the color: .style("background", function(d) { return d; }) but I think that is the text background, not the rect fill.
What am I doing wrong here? How can I make 20x20 rectangles filled with color in d3?
As you are not giving any index and reference of colors array into your function the code will not understand from where to pick colors. try with below code it will help.
d3.select("body").selectAll("svg")
.data(colors).enter().append("rect")
.attr("class", "block")
.attr("width", barWidth)
.attr("height", barHeight - 1)
.text(function(d) {
return d;
})
.attr("fill", function(d,i) { return colors[i]; });
So, a few things. You should call data() on what will be an empty selection of the things you will be adding.
svg.selectAll("rect").data(colors)
.enter().append("rect")
The rect doesn't have a text property. There is an svg text node that shows text and you'll want to add it separately.
I hope this https://jsfiddle.net/xryamdkf/8/ gets you closer.

D3: Add border around clip-path

I'm aiming to add a border around the circular avatars I create in my D3 visualization. I create these circular avatars by using clip-path. When I add a border to my node it is a square border around the whole node, rather than circular like I'm aiming for (and I understand why, because this node is rectangular). Here is what that currently looks like:
I'm struggling in getting this border to instead appear around the circular, clipped, image.
Here is the code where I currently set the (rectangular) border:
var nodeEnter = node.enter().append('svg:g')
.attr('class', 'node')
.attr('cursor', 'pointer')
.attr('style', function(d) {
var color;
if (d.strength > 2) {
color = 'blue';
} else {
color = 'red';
}
return 'outline: thick solid ' + color + ';';
})
.attr('transform', function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(force.drag);
...and this is how I declare my clip-path:
var clipPath = defs.append('clipPath')
.attr('id', 'clip-circle')
.append('circle')
.attr('r', 25);
My full example can be found here:
http://blockbuilder.org/MattDionis/5f966a5230079d9eb9f4
How would I go about setting this as a circular border around the image rather than rectangular around the entire node?
You could just add a circle of slightly larger radius (then your clip-path) into your node:
nodeEnter.append('circle')
.attr('r',30)
.style('fill', function(d) {
return d.strength > 2 ? 'blue' : 'red'
});
var images = nodeEnter.append('svg:image')
.attr('xlink:href', function(d) {
return d.avatarUrl;
})
.attr('x', function(d) {
return -25;
})
.attr('y', function(d) {
return -25;
})
.attr('height', 50)
.attr('width', 50)
.attr('clip-path', 'url(#clip-circle)');
Updated code.

How to draw gradient arc using d3.js?

I have started using d3.js. I have following requirement
Requirement:
What I have tried?
fiddle
Question?
How to achieve gradient as same as above image.
Any suggestion or idea will be grateful.
Note
I am just started d3.js.
Edit - changed data structure and fiddle link to represent unfilled chunk at the beginning.
I would use the pie function in d3 to create a pie chart.
The image above is basically a pie with two different gradient styles applied to the pie chunks.
A red linear gradient and a black/white radial gradient.
I created a fiddle linked below to show you an example.
The key here is that you need to structure your data to also include the percentage that should not have the red-gradient applied.
Using the example above, we have three chunks with red and the rest as unfilled.
Imagine the data set like so:
var data = [{
percent: 10,
pie: 0
}, {
percent: 13,
pie: 1
}, {
percent: 13,
pie: 1
}, {
percent: 6,
pie: 1
}, {
percent: 56,
pie: 0
}];
So we have the percent and we also flag which chunks should be red and which chunk should be the unfilled section using the pie attribute.
You can use whatever data set you wish but I'm just using this as an example.
So next thing is to create your SVG element:
var width = 400;
var height = 400;
var radius = Math.min(width, height) / 2;
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(((radius - 10) / 5) * 4);
var pie = d3.layout.pie()
.sort(null)
.value(function (d) { return d.percent });
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
and after this we will create the two gradients to style the pie chunks.
So first one is the linear red gradient:
// append a defs tag to SVG, This holds all our gradients and can be used
//by any element within the SVG we append it to
var defs = svg.append("svg:defs")
//next we append a linear gradient
var red_gradient = defs.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "0%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");
//first dark red color
red_gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "rgb(221,48,2)")
.attr("stop-opacity", 1);
//second light red color
red_gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "rgb(247, 78, 1)")
.attr("stop-opacity", 1);
Then we append the radial gradient for the unfilled part. This one is a little tricker because we need to move the gradient with a transform to get the right radial center. If you translate it half the width and height I think it should work out.
var radial_gradient = defs.append("radialGradient")
.attr("gradientUnits", "userSpaceOnUse")
.attr("cx", '50%')
.attr("cy", '50%')
.attr("r", "75%")
.attr("fx", '50%')
.attr("fy", '50%')
.attr('gradientTransform', "translate(-200,-200)")
.attr("id", 'gradient2');
radial_gradient.append("stop").attr("offset", "0%").style("stop-color", "black");
radial_gradient.append("stop").attr("offset", "55%").style("stop-color", "white");
radial_gradient.append("stop").attr("offset", "95%").style("stop-color", "black");
Once we have set up the gradients, we can add the pie:
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
// we create a function to append the different chucks of the pie.
// we check the pie attribute from the data and apply the correct gradient.
g.append("path")
.attr("d", arc)
.style("fill", function (d) {
if (d.data.pie === 1) {
console.log('true' + d.data.pie);
return "url(#gradient)"
}
else {
console.log('false' + d.data.pie);
return "url(#gradient2)"
}
})
JSFiddle: http://jsfiddle.net/staceyburnsy/afo292ty/2/

Categories