I'm working with D3 for the first time and I'm trying to make a parallel coordinate graph. I basically am using this demo. The only real changes I've had is changing the data and changing the far right axis so it has strings instead of numbers as the labels. I do this by using the following:
if(d === "Dog Breed") {
y[d] = d3.scale.ordinal()
.domain(dogData.map(function(p) { return p[d]; }))
.rangePoints([h, 0]); // quantitative color scale
}
Unfortunately, if the dog's breed is too long, the text gets cut off, making it hard to read the label (one has to move the axis in its entirety to read it, but when they let go of it, it goes right back to where it was initially).
My other change were the following:
var m = [30, 10, 10, 10],
w = screen.width - 150, // Make it 150px less than the screen's width.
h = 500 - m[0] - m[2];
The axis label code remains the same at:
// Add an axis and title.
g.append("svg:g")
.attr("class", "axis")
.each(function(d) { d3.select(this).call(axis.scale(y[d])); })
.append("svg:text")
.attr("text-anchor", "middle")
.attr("y", -9)
.text(String);
Is there any way to avoid the name-being-clipped-thing? Even shifting the graph itself over in its block about 20px would help, but I don't know where the code for that would be...
The fix was to manipulate var m to have more on the lefthand side in m[3].
Related
I am trying to create a simple line graph using d3 which segments the curve and paint each segment with a different colour. Currently I am only able to colour the whole area under the curve.
Current:
Attempting to achieve (Pardon me for the terrible colouring. In a rush for time):
This is bits of relevant code. Please help!
var x = d3.scaleLinear();
var y = d3.scaleLinear();
//Set the range of the data
x.domain([0, Math.max(endGrowth, endPlateau) ,maxX]).range([0, width*0.8, width]);
y.domain([0, maxY]).range([height, 0]);
//Define the area under the Graph
var area1 = d3.area()
.x0(function(d){
return x(d.Rank);
})
.y1(function(d){ return y(d.Elements);})
.y0(height);
//Add the colored regions
svg.append("path")
.data([data])
.attr("class", "areaUnderGraph")
.attr("fill", "blue")
.attr("transform", "translate(" + leftMarginLabel + ",0)")
.attr("d", area1);
Right now, the area under the curve is one path, so it can only be colored one color. The simplest way to color different portions under the curve different colors is to split them up in data. It's not clear where data is coming from, but you'd take sub-sections of the array, like
var segments = [data.slice(0, 2), data.slice(2)];
svg.selectAll("path")
.data(segments)
.enter()
.append("path")
.attr("fill", function(d) { /* use d to choose a color */ })
That's the gist: you'd have multiple slices of the data, and instead of one path, you'd create multiple paths that you can color as you wish.
I want to draw a curve with interpolation for some given points. Here, the points represent how much solar energy can be generated by solar panels, so there is one point per hour of the day with sun. The number of points may vary depending on the month of the year (for example, 10 points in December and 16 points in June, because there are respectively 10 and 16 hours of sun a day in those months).
Until here everything fine, but now we want to add a sun image at the hour of the day you're seeing the graphics. For this, I have created 2 lines : one before the current hour and one after, and put the sun image in the current hour position. It looks like this in June with 16 points at 1PM :
This looks fine. The problem is when there are less points, the space between the point before and after the current hour is bigger, and becomes graphically too big. This is for January at 9AM with 10 points (wrong graphical rendering) :
(in both images, the ending / beginning time at the bottom are static)
I want the blank space that is left for the sun to be always the same.
I have tried various things :
adding some points "closer to the sun" in the data : doesn't work because it messes up the scale, and even with a scale updated after adding the points, the top part of the curve is not centered anymore
putting a background on the sun image : the graph must be integrated in a transparent container
using stroke-dasharray : i couldn't manage to understand the percentage / pixels values enough to calculate it. For example, with a distance to dash of 100%, it would dash before the end of the line. For the pixels unit, I haven't found any way to calculate the number of pixels generated by the curve drawing so it isn't possible to calculate the exact position of the dash
using a linearGradiant : I can't get to scale a proper percentage positioning. Anyway, the render is ugly because it cuts the line color vertically, which is not nice graphically
If anyone has an idea of how to properly accomplish this, it would be great. Also I may probably have missed something obvious or think a wrong way for this problem, but after 3 days of thinking about it I'm a bit overloading haha. Thank you for reading
Sounds like you have your answer but I'll propose a different approach. This sounds very solvable using stroke-dasharray. Here a quick demo:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var line = d3.line()
.curve(d3.curveCardinal)
.x(function(d) {
return d[0];
})
.y(function(d) {
return d[1];
});
var data = [[10,450], [250, 50], [490, 450]];
var p = svg.append("path")
.datum(data)
.attr("d", line)
.style("stroke", "orange")
.style("stroke-width", "5px")
.style("fill", "none");
var l = p.node().getTotalLength(),
sunSpace = l / 12;
function createSpace(){
var sunPos = Math.random() * l;
p.attr("stroke-dasharray", (sunPos - sunSpace/2) + "," + sunSpace + "," + l);
}
createSpace();
setInterval(createSpace, 1000);
</script>
</body>
</html>
EDITS FOR COMMENTS
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<svg width="500" height="500"></svg>
<script>
var svg = d3.select("svg"),
margin = {
top: 20,
right: 20,
bottom: 30,
left: 50
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var y = d3.scaleLinear()
.rangeRound([height, 0])
.domain([0, 10]);
var x = d3.scaleLinear()
.rangeRound([0, width])
.domain([0, 10]);
var line = d3.line()
.curve(d3.curveCardinal)
.x(function(d) {
return x(d[0]);
})
.y(function(d) {
return y(d[1]);
});
var data = [
[1, 1],
[5, 9],
[9, 1]
];
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y))
var p = g.append("path")
.datum(data)
.attr("d", line)
.style("stroke", "orange")
.style("stroke-width", "5px")
.style("fill", "none");
var pathLength = p.node().getTotalLength(),
sunSpace = pathLength / 12;
function createSpace() {
var sunPos = x(3);
var beginning = 0,
end = pathLength,
target;
while (true) {
target = Math.floor((beginning + end) / 2);
pos = p.node().getPointAtLength(target);
if ((target === end || target === beginning) && pos.x !== sunPos) {
break;
}
if (pos.x > sunPos) end = target;
else if (pos.x < sunPos) beginning = target;
else break; //position found
}
p.attr("stroke-dasharray", (target - sunSpace/2) + "," + sunSpace + "," + pathLength);
}
createSpace();
</script>
</body>
</html>
One solution to this problem was to use a svg mask.
Like explained here : "SVG clipPath to clip the *outer content out", you can create masks that will create a zone of "non-display" of the element that you apply it to. In other terms, I created a mask with a circle that is at the sun position all the time, which hides the part of the curve that is inside the circle.
You may be overthinking it. Have you considered drawing the full line and including a white circle around your sun icon? As long as you draw the icon after the line, it would leave just the right amount of space.
You should be able to separate your data from your rendering model. You didn't include code, so your specific solution will vary. But the general idea is that you convert the actual data into something that better suits your rendering needs. For example:
// If this is your actual data ...
var numberOfHours = 10;
var showSunAt = 3;
// ... and this is your desired "resolution" for the path ...
var resolution = 16;
// ... map your data to work with this resolution.
var mapped = showSunAt / numberOfHours * resolution;
// Define the "space" to leave between segments.
var spaceBetweenSegments = 1;
// Then, dynamically set the start/end points for each segment.
var firstSegmentEndsAt = mapped - (spaceBetweenSegmenets / 2);
var secondSegmenetStartsAt = mapped + (spaceBetweenSegmenets / 2);
You now have the exact points along the path where the first segment should end, the icon should be rendered, and the second segment should begin.
Hi i am new to d3js so i am trying out simple example codes
so here's the code
d3.selectAll("div")
.data([100,180,200,400,450])
.transition()
.duration(2000)
.style("width", function(d) { return d +"px"; });
http://jsfiddle.net/YjED4/
what i want to achieve is set the size of the chart dynamically to the length of the max data that i am passing i.e charts max length should be on a max scale of 0-500 given the max data i have passed is 450
the problem is when i pass data like
.data([1,18,20,40,4])
i get a chart like this
http://jsfiddle.net/NnLU9/
which beats the purpose of the chart.
So any pointers on how to proceed on this will be helpful
D3 is an amazing technology and I hope you stick with it.
The problem with your code is that it does not have a concept of an x-axis or y-axis, your code just has data that change the width by pixels. Without an x or y axis your chart will not scale properly.
You need something like this in your code:
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, 420]);
The code snippet states that the values are from 0 to the max of the data set, and the pixel range is from 0 pixels to 420 pixels. So if your max is 40, then 40 will map to 420 pixels.
Then the following will scale your chart:
d3.select(".chart")
.selectAll("div")
.data(data)
.enter().append("div")
.style("width", function(d) { return x(d) + "px"; })
.text(function(d) { return d; });
notice the code that says return x(d) + 'px', what is does is when you pass a value let's say '20' into the function x(d) it will scale it to whatever you had scaled your x-axis to. So '20' will be 210 pixels in the function above, '0' would be 0 pixels, and '40' would be '420' pixels.
This is the example I used to get started in d3, I think Mike Bostock can explain it better than anyone: http://bost.ocks.org/mike/bar/
Hope this helps.
I am trying to get brushing to work similar to this example, but with a grouped bar chart: http://bl.ocks.org/mbostock/1667367
I don't really have a good understanding of how brushing works (I haven't been able to find any good tutorials), so I'm a bit at a loss as to what is going wrong. I will try to include the relevant bits of code below. The chart is tracking the time to fix broken builds by day and then grouped by portfolio. So far the brush is created and the user can move and drag it, but the bars in the main chart are re-drawn oddly and the x axis is not updated at all. Any help you can give would be greatly appreciated. Thank you.
// x0 is the time scale on the X axis
var main_x0 = d3.scale.ordinal().rangeRoundBands([0, main_width-275], 0.2);
var mini_x0 = d3.scale.ordinal().rangeRoundBands([0, main_width-275], 0.2);
// x1 is the portfolio scale on the X axis
var main_x1 = d3.scale.ordinal();
var mini_x1 = d3.scale.ordinal();
// Define the X axis
var main_xAxis = d3.svg.axis()
.scale(main_x0)
.tickFormat(dateFormat)
.orient("bottom");
var mini_xAxis = d3.svg.axis()
.scale(mini_x0)
.tickFormat(dateFormat)
.orient("bottom");
After binding the data...
// define the axis domains
main_x0.domain(data.result.map( function(d) { return d.date; } )
.sort(d3.ascending));
mini_x0.domain(data.result.map( function(d) { return d.date; } )
.sort(d3.ascending));
main_x1.domain(data.result.map( function(d) { return d.portfolio; } )
.sort(d3.ascending))
.rangeRoundBands([0, main_x0.rangeBand() ], 0);
mini_x1.domain(data.result.map( function(d) { return d.portfolio; } )
.sort(d3.ascending))
.rangeRoundBands([0, main_x0.rangeBand() ], 0);
// Create brush for mini graph
var brush = d3.svg.brush()
.x(mini_x0)
.on("brush", brushed);
After adding the axis's, etc.
// Create the bars
var bar = main.selectAll(".bars")
.data(nested)
.enter().append("g")
.attr("class", function(d) { return d.key + "-group bar"; })
.attr("fill", function(d) { return color(d.key); } );
bar.selectAll("rect").append("rect")
.data(function(d) { return d.values; })
.enter().append("rect")
.attr("class", function(d) { return d.portfolio; })
.attr("transform", function(d) { return "translate(" + main_x0(d.date) + ",0)"; })
.attr("width", function(d) { return main_x1.rangeBand(); })
.attr("x", function(d) { return main_x1(d.portfolio); })
.attr("y", function(d) { return main_y(d.buildFixTime); })
.attr("height", function(d) { return main_height - main_y(d.buildFixTime); });
Here is the brush function (trying several different options)...
function brushed() {
main_x1.domain(brush.empty() ? mini_x1.domain() : brush.extent());
//main.select("rect")
//.attr("x", function(d) { return d.values; })
//.attr("width", function(d) { return d.values; });
bar.select("rect")
.attr("width", function(d) { return main_x1.rangeBand(); })
.attr("x", function(d) { return main_x1(d.portfolio); });
//.attr("y", function(d) { console.log(d); return main_y(d.buildFixTime); })
//.attr("height", function(d) { return main_height - main_y(d.buildFixTime); });
main.select(".x.axis").call(main_xAxis);
}
The problem comes from trying to use the brush to set the x-scale domain, when your x-scale is an ordinal scale. In other words, the expected domain of your x-axis is a list of categories, not a max-min numerical extent. So the problem is right at the top of the brushing function:
function brushed() {
main_x0.domain(brush.empty() ? mini_x0.domain() : brush.extent());
The domain set by brush.extent() is an array of two numbers, which then completely throws off your ordinal scale.
According to the wiki, if one of the scales attached to a brush function is an ordinal scale, the values returned by brush.extent() are values in the output range, not in the input domain. Ordinal scales don't have an invert() method to convert range values into domain values.
So, you have a few options on how to proceed:
You could re-do the whole graph using a linear time scale for your main x-axes instead of an ordinal scale. But then you have to write your own function to figure out the width of each day on that axis instead of being able to use .rangeBand().
You can create your own "invert" function to figure out which categorical values (dates on the mini_x0.domain) are included in the range returned by brush.extent(). Then you would have to both reset the main_x0.domain to only include those dates on the axis, and filter out your rectangles to only draw those rectangles.
Or you can leave the domain of main_x0. be, and change the range instead. By making the range of the graph larger, you space out the bars greater. In combination with a clipping path to cut off bars outside the plotting area, this has the effect of only showing a certain subset of bars, which is what you want anyway.
But what should the new range be? The range returned by brush.extent() is the beginning and end positions of the brushing rectangle. If you used these values as the range on the main graph, your entire graph would be squished down to just that width. That's the opposite of what you want. What you want is for the area of the graph that originally filled that width to be stretched to fill the entire plotting area.
So, if your original x range is from [0,100], and the brush covers the area [20,60], then you need a new range that satisfies these conditions:
the 20% mark of the new range width is at 0;
the 60% mark of the new range width is at 100.
Therefore,
the total width of the new range is ( (100-0) / (60-20) )*(100-0) = 250;
the start of the new range is at (0 - (20/100)*250) = -50;
the end of the new range is at (-50) + 250 = 200.
Now you could do all the algebra for figuring out this conversion yourself. But this is really just another type of scaling equation, so why not create a new scale function to convert between the old range and the zoomed-in range.
Specifically, we need a linear scale, with its output range set to be the actual range of the plotting area. Then set the domain according to the range of the brushed area that we want to stretch to cover the plotting area. Finally, we figure out the range of the ordinal scale by using the linear scale to figure out how far off the screen the original max and min values of the range would be. And from there, we-can resize the other ordinal scale and reposition all the rectangles.
In code:
//Initialization:
var main_xZoom = d3.scale.linear()
.range([0, main_width - 275])
.domain([0, main_width - 275]);
//Brushing function:
function brushed() {
var originalRange = main_xZoom.range();
main_xZoom.domain(brush.empty() ?
originalRange:
brush.extent() );
main_x0.rangeRoundBands( [
main_xZoom(originalRange[0]),
main_xZoom(originalRange[1])
], 0.2);
main_x1.rangeRoundBands([0, main_x0.rangeBand()], 0);
bar.selectAll("rect")
.attr("transform", function (d) {
return "translate(" + main_x0(d.date) + ",0)";
})
.attr("width", function (d) {
return main_x1.rangeBand();
})
.attr("x", function (d) {
return main_x1(d.portfolio);
});
main.select("g.x.axis").call(main_xAxis);
}
Working fiddle based on your simplified code (Note: you still need to set a clipping rectangle on the main plot):
http://fiddle.jshell.net/CjaD3/1/
I have a D3 ordinal scale that I'm using in a bar chart:
var y = d3.scale.ordinal().rangeRoundBands([0, height], .1);
I call it like this to position a number of g elements, one for each bar in my bar chart:
var barEnter = bar.enter()
.insert("g", ".axis")
.attr("class", "bar")
.attr("transform", function(d, i) {
return "translate(0," + (y(d.State) + height) + ")";
});
It works really well when there are many bars, as they are spread evenly.
However, it works but doesn't look so good when there are just a couple of bars:
Is there a way I can modify either the y function, or the transform attribute, to set transform more sensibly when there are just a few bars? I don't completely understand what the ordinal scale is doing behind the scenes.
For the benefit of others, this is how I solved it.
I realised that the problem occurred when the y scale's domain only had a couple of elements in it. So I ended up padding the scale's domain, as follows:
if (ydomain.length < 12) {
for (var i = ydomain.length; i < 12; i++) {
ydomain.push('y_' + i);
}
}
y.domain(ydomain);
I doubt this is the most elegant way of doing things, but it worked.