I'm trying to add a gradient of different colors (meant to symbolize the percentage of different answers recorded in a survey) to a svg shape (in this case, the shape is a country the user has clicked on). I want it to look like this (please ignore the awful choice of colors).
However, this screenshot is taken when the percentage data for each answer is hard coded (visible to the right in the picture), and not fetched from the csv file with the survey data. So, I wrote this snippet of code to generate a gradient based on the data in the survey. It calculates the percentage each answer received, and creates two offset-attributes that defines the boundaries for that specific answer (which is then applied to the gradient svg). Finally, this gradient is applied to the country the user clicked on.
function calculateGrad(csvData, noOfQuestions, chosenCountry){
var total = 0
for (var i = 0; i < noOfQuestions; i++) {
total += parseInt(csvData[i][chosenCountry])
}
var grad = svg.append("defs")
.append("linearGradient")
.attr("id", "grad")
.attr("x1", "0%")
.attr("x2", "0%")
.attr("y1", "100%")
.attr("y2", "0%")
totalPercentFilled = 0
counter = 0
for (var i = 0; i < noOfQuestions; i++) {
var randomColor = "#000000".replace(/0/g,function(){return (~~(Math.random()*16)).toString(16);});
if (i == 0) {
grad.append("stop").attr("offset", "0%").attr("stop-color", randomColor)
var prevColor = randomColor
counter += 1
}
else {
grad.append("stop").attr("offset", totalPercentFilled).attr("stop-color", randomColor)
var prevColor = randomColor
counter += 1
}
grad.append("stop").attr("offset", (totalPercentFilled + (parseInt(csvData[i][chosenCountry])/parseInt(total))*100)).attr("stop-color", prevColor);
totalPercentFilled += (parseInt(csvData[i][chosenCountry])/parseInt(total)*100)
}
console.log(grad)
// console.log("Number of loops made: " + counter)
// console.log("Number of Q:s: " + noOfQuestions)
g.selectAll("#" + country.id).style("fill", "url(#grad)")
console.log("Gradient applied")
While this code seems to generate the exact same gradient (to me), it results in the country getting filled with just one color (the first of the gradient).
I can't figure out why this happens, as the gradients logged in the console in both examples look identical to me. If someone knows what the problem is I'll be eternally grateful for some guidance.
Please also let me know if I need to provide more details.
In the example D3 screenshot you can see that you are not appending the percentage % unit to the gradient stop. The default unit for gradient stops is the objectBoundingBox, and the units run from 0 to 1.
So your gradient is probably there, it's just 100× from what you intended. You're only seeing one band of your (very large) gradient.
Related
After playing around a lot I managed to make the following function change the colour of all 7 of my circles at once. There is a variable colors that is an array of strings of colours.
Here is my function:
function colorSquares(){
for(let i = 0; i<7 ; i++){
let id = '#s' + i ;
d3.select(id).attr('fill',colors[i]);
}
}
I cannot help but think there is a much easier way of doing this! I created the 7 circles like this:
for(let i = 0 ; i< 7; i++){
const circle = d3.select('.canvas')
.append('circle')
.attr('cx', 70 + i* 50)
.attr('cy', 100)
.attr('r', 20)
.attr('stroke' ,'grey')
.attr('stroke-width', 5)
.attr('id' , 'c'+i );
}
As you can see I manually made a string and added it using .attr to each circle and then mess around with strings to call this circle later. Does anyone have a cleaner and better way to do this, effectively I am looking for better ways to create and then select circles such that I don't have to botch strings as ID's
Try
d3.select('.canvas')
.selectAll('circle')
.attr('fill', (d, i) => colors[i]);
I have a D3.js line chart that I want to update on user-input. One path is for ‘total price’, made up of a fixed price + a variable cost. I also show a ’fixed price’ line (not a path).
I have a slider to change the value of the fixed cost and then update the path and line.
The line takes the new inputted slider value and updates as expected. The path, however, starts being plotted with very negative y values and so doesn’t show on the chart.
Am I missing some logic to this?
If I hard-code a new value for ‘fixedCost’ the path updates as expected, but as soon as I substitute it for document.getElementById('fixed').value - it gives me a negative plot. The same problem occurs on first draw if I use the slider value.
I've successfully updated line charts in D3 before but that's usually loading new data set on a change event. I haven't encountered this problem with paths before. I'm using D3 V4. Below is the code for the slider and for the update function. Thanks
...javascript
var slider = d3.select("#chart").append("p").attr('id', 'slider')
.style('position', 'absolute')
.style('top', height + margin.top + 60 + 'px')
.style('left', margin.left + 'px')
.append("input")
.attr("type", "range")
.attr('id', 'fixed')
.attr("value", 408000)
.attr("min", 0)
.attr("max", 1000000)
.style("width", sliderWidth)
.on("input", updateFixed);
// set starting parameters
// var fixedCost = document.getElementById('fixed').value; // doesn't behave as expected when plotting path.
var fixedCost = 408000;
var valuelineTotCost = d3.line()
.x(function(d) { return x(d.subs); })
.y(function(d) { return y(d.variCost + fixedCost); });
function updateFixed() {
var thisValue = document.getElementById('fixed').value;
d3.select('#sliderText')
.text("Fixed Costs: " + format(thisValue) ); // displays as expected
console.log(thisValue); // returns as expected
svg.select('#fixedCostLine')
.attr("x1", 0)
.attr("x2", width)
.attr("y1", y(thisValue))
.attr("y2", y(thisValue)); // this line updates as expected
// var fixedCost = document.getElementById('fixed').value; // tried this instead of using thisValue but still not behaving as expected
// var fixedCost = thisValue; // also not behaving as expected
var fixedCost = 508000; // behaves as expected
// adjust totCost path
var valuelineTotCost = d3.line()
.x(function(d) { return x(d.subs); })
.y(function(d) { return y(d.variCost + fixedCost); });
svg.select("#totalPath")
.style("stroke", "purple")
.attr("d", valuelineTotCost);
};
...
Here are the original path co-ordinates generated by d3.line followed by the negative y plots given when adjusted (even minutely) using the slider value.
Lastly, using a hard-coded value for the update, switching from the starting point of 408000 to 508000 - gave the third set of plots.
The path generator was reading the value from the input as a string.
thisValue = parseInt(thisValue);
solved the problem.
I am building a chart using D3.js, which shows some info about employee's competencies.
screenshot:
As you can see, some text is larger than container element size, because of that, part of the text, is cut. I want to wrap these texts inside container.
I found this example , but I was not able to apply some solution to my chart.
Help would be appreciated...
Here is charts codepen url
and here is full screen view
p.s. I need text to be wrapped by words
In order to wrap the labels, you need to adjust Mike's solution to deal with textPath elements.
For this, we need several things:
1. Get the available width, reaching which the labels should wrap
You could compute the length of the arc itself, but I've done this by computing the segment created by the endpoints of your invisible paths that your labels follow. This will provide us with a little side margin as well, as the segment's length is shorter than the arc's length.
The distance between two points is computed as follows:
d = sqrt((x2 - x1)^2 + (y2 - y1)^2)
2. Wrap the labels when they rich available width and keep the aligned to center
For managing this one, I had to dig into the SVG documentation on the textPath element to see how it can be wrapped and shifted along the y axis.
Initially, I tried setting several textPath elements within one text label, but I couldn't manage to shift them along the y axis. It turns out, that for this you need to add tspan elements within textPath elements. But here another problem arose - I couldn't manage to keep them centrally aligned.
In the end, to achieve shift along y axis and central alignment, you need to use one textPath element (for horizontal alignment) with one tspan element inside (for vertical alignment).
3. Wrap the labels by letters, not by words
This is the point that I have assumed that you'll need namely letter wrapping (at the moment of writing, I didn't get the answer from OP), because on small sizes of your chart, there are words too long to fit into one line.
This was the easiest problem to solve. Just adjust the splitting and joining operations to switch from words to letters:
letters = text.text().split('').reverse(); // instead of .split(/\s+/)
...
tspan.text(line.join("")); // instead of .join(" ")
And here's the whole code that was changed, with relevant comments:
outerSvg.selectAll(".outerCircleText")
.data(pie(behaviorsDatasetOuterCircle))
.enter().append("text")
.attr("class", "outerCircleText")
//Move the labels below the arcs for those slices with an end angle greater than 90 degrees
.attr("dy", function (d, i) {
d.i = i;
return (d.startAngle >= 90 * Math.PI / 180 ? 18 : -11);
})
.text(function(d) { return d.data.name; })
.call(wrap); // Do not add `textPath` elements here. Instead, add them in the `wrap` function
function wrap(text) {
text.each(function() {
var text = d3.select(this),
letters = text.text().split('').reverse(),
letter,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
textPath = text.text(null).append("textPath") // Add a textPath element
.attr("startOffset", '50%')
.style("text-anchor", "middle")
.attr("xlink:href", function(d) { return "#outerArc" + d.i; }),
tspan = textPath.append('tspan'), // Inslide textPath, add a tspan element, for offset feature later.
path = d3.select(text.select('textPath').attr('xlink:href')); // Get the path to compute width of text later.
var startLoc = /M(.*?)A/;
var newStart = path.attr('d').match(startLoc)[1];
var newEnd = path.attr('d').indexOf(' 0 0 0 ') > -1
? path.attr('d').split(' 0 0 0 ')[1]
: path.attr('d').split(' 0 0 1 ')[1] ;
// Compute the start/end coordinate points of the arc that the text will follow.
var x1 = parseFloat(newStart.split(' ')[0]),
y1 = parseFloat(newStart.split(' ')[1]),
x2 = parseFloat(newEnd.split(' ')[0]),
y2 = parseFloat(newEnd.split(' ')[1]);
// Compute the length of the segment between the arc start/end points. This will be the
// width which the labels should wrap when reaching it.
var width = Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2));
// And then we go on (with slight changes) with the example from Mike Bostock
// from here https://bl.ocks.org/mbostock/7555321
while (letter = letters.pop()) {
line.push(letter);
tspan.text(line.join(""));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(""));
line = [letter];
// Instead of adding only a tspan element, add a new textPath so that the wrapped
// letters will be aligned to center. Without it, the letters will start drawing
// from right with part of them invisible, like if the labels are not wrapped.
textPath = text.append("textPath")
.attr("startOffset", '50%')
.style("text-anchor", "middle")
.attr("xlink:href", function(d) { return "#outerArc" + d.i; }),
// Add a tspan element to offset the wrapped letters from the previous line
tspan = textPath.append("tspan")
.attr('dy', '1em')
.attr('text-anchor', 'middle')
.text(letter);
}
}
});
}
In the end, it was an interesting challenge. Here is a fork of your codepen with a working example (the changes are starting with line 749).
The codepen has only the outer labels wrapped. I have left the inner labels for you to implement the approach described here. Good luck with that!
I want to reimplement the following Processing sketch with d3js.
Recursion Thing
This wonderful sketch recursively builds up the graph, using a complex pushMatrix, popMatrix hierarchy.
How could this be implemented in d3.js as we there always work on the DOM immediately when appending a shape or transformation. But in a logic like in this sketch the appending part seems to have to be held back for the respective popMatrix to come. It feels like I have to implement my own transform and shape stack to temporary remember the transformation and shapes to be added until the popMatrix comes but that seems so not d3.js.
Any suggestion highly appreciated
ps:
i dont want to use processing.js as i want to work with svg, not canvas.
Interesting problem! Here's my take on it: http://jsfiddle.net/Y48BL/
This is more a proof of concept; I didn't do all the different colours and such. Nevertheless, it demonstrates the general approach. The general idea is to use g elements instead of the matrices that processing uses. Both are used for local transformations of the coordinate system; in the case of the g elements by setting transform accordingly. New gs (matrices) are created inside the recursive function and then passed on to the next level of the recursion. This would correspond to pushMatrix(). Coming back up, we continue to use the original g, corresponding to popMatrix().
The translation of the drawing of the circles and lines is fairly straightforward -- I find the D3 code much easier to read.
So I came up with this helper "class" to get this done, maybe a bit of a overkill but I will have more use cases for this.
var TransformStack = (function () {
function TransformStack() {
this.stack = [];
}
TransformStack.prototype.getCurrentElement = function () {
return this.stack[this.stack.length - 1];
};
TransformStack.prototype.setCurrentElement = function (element) {
this.stack[this.stack.length - 1] = element;
};
TransformStack.prototype.push = function (transformElement) {
this.stack.push(transformElement);
};
TransformStack.prototype.pushAndTransform = function (transformAttr) {
this.push(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.transform = function (transformAttr) {
this.setCurrentElement(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.pop = function () {
return this.stack.pop();
};
return TransformStack;
})();
Basically a stack to push/pop g elements which replaces the matrices approach in processing as Lars already pointed out. With this the main routine looks something like
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height)
.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + gScale + ")");
var tstack = new TransformStack();
tstack.push(svg);
doIt(nRecursions);
function doIt(n) {
// Circle
tstack.getCurrentElement()
.append("circle")
.attr("r", theSize)
.style("fill", "#fe6b0c")
.style("stroke", "0")
.style("stroke-width", "2")
.style("opacity", 0.3);
if (n != nRecursions) {
for (var i = 0; i < 4; i++) {
tstack.getCurrentElement().append("line")
.style("stroke", "red")
.style("opacity", 0.16)
.attr("x1", Math.random() * 4 - 2)
.attr("x2", Math.random() * 4 - 2)
.attr("y1", theSize / 2.0 + Math.random() * 4 - 2)
.attr("y2", distance - (theSize * theScale / 2.0) - 8.0 + Math.random() * 4 - 2);
}
}
var rot = 0;
tstack.pushAndTransform("scale(" + theScale + ")");
for (var i = 0; i < n; i++) {
if (n > 0) {
tstack.pushAndTransform("translate(0," + distance + ")");
doIt(n - 1);
tstack.pop();
rot = 360 / n;
tstack.transform('rotate(' + rot + ')');
}
}
tstack.pop();
}
}
Just wanted to share this, maybe of some use for some. The main point was given by Lars already.
I am starting with d3.js, and am trying to create a network graph each circle of which contains a label.
What I want is a line break an svg text.
What I am trying to do is to break the text into multiple <tspan>s, each with x="0" and variable "y" to simulate actual lines of text. The code I have written gives some unexpected result.
var text = svg.selectAll("text").data(force.nodes()).enter().append("text");
text
.text(function (d) {
arr = d.name.split(" ");
var arr = d.name.split(" ");
if (arr != undefined) {
for (i = 0; i < arr.length; i++) {
text.append("tspan")
.text(arr[i])
.attr("class", "tspan" + i);
}
}
});
In this code am splitting the text string by white space and appending the each splitted string to tspan. But the text belonging to other circle is also showing in each circle. How to overcome this issue?
Here is a JSFIDDLE http://jsfiddle.net/xhNXS/ with only svg text
Here is a JSFIDDLE http://jsfiddle.net/2NJ25/16/ showing my problem with tspan.
You need to specify the position (or offset) of each tspan element to give the impression of a line break -- they are really just text containers that you can position arbitrarily. This is going to be much easier if you wrap the text elements in g elements because then you can specify "absolute" coordinates (i.e. x and y) for the elements within. This will make moving the tspan elements to the start of the line easier.
The main code to add the elements would look like this.
text.append("text")
.each(function (d) {
var arr = d.name.split(" ");
for (i = 0; i < arr.length; i++) {
d3.select(this).append("tspan")
.text(arr[i])
.attr("dy", i ? "1.2em" : 0)
.attr("x", 0)
.attr("text-anchor", "middle")
.attr("class", "tspan" + i);
}
});
I'm using .each(), which will call the function for each element and not expect a return value instead of the .text() you were using. The dy setting designates the line height and x set to 0 means that every new line will start at the beginning of the block.
Modified jsfiddle here, along with some other minor cleanups.