I am a beginner in D3.js . I need to draw a graph with axis as per below image.Any working fiddle will help me a lot.
I have checked the below fiddle but not sure how to tweak it as per my requirement.
You can create the scale (which is completely independent from the axis) and use its parameters to create the svg elements for your axis; in your case the horizontal line and a circle + text as ticks.
Specifically, you could use the scale range() to get the horizontal line length; and the scale ticks(n) functions to get n evenly spaced points in the domain of the scale. By passing these points to the scale you get the x coordinate to use for your ticks.
xScale = d3.scale.linear().domain([0.0, 1.0]).range([0, width]);
var axis = svg.append('g').attr('class', 'xaxis');
axis.append('line').attr({
'x1': xScale.range()[0],
'y1': height,
'x2': xScale.range()[1],
'y2': height
});
ticks = axis.selectAll('.ticks')
.data(xScale.ticks(10))
.enter()
.append('g')
.attr('class', 'tick');
ticks.append('circle')
.attr({
'cx': function(d) { return xScale(d); },
'cy': height,
'r': 5
});
ticks.append('text')
.attr({
'x': function(d) { return xScale(d); },
'y': height,
'dy': 20 // Move the text a little under the line
})
.text(function(d) { return d; });
Here is a minimal working JSFiddle: http://jsfiddle.net/LeartS/yGesr/
Related
I have constructed a stacked bar chart with approx 700 bars. Everything function as it should but I am getting really frustrated with the stripes that appear when the chart is drawn. Below is a screenshot with the default view and a zoomed view.
zoomed view to the left, default to the right
I suspect that the stripes come from the padding between the bars. I've tampered with the bar width to try and eliminate the padding but the stripes are still there. Currently the bar width code looks like this:
.attr("width",((width-(padding+xPadding))/data.length)+0.01)
The "+0.01" removes the padding and if I increase it further to, say 1, the stripes are gone. However, now the bars are stacked on each other noticably, which I do not want. I suspect there is some quick fix to this(maybe css or something other trivial) but I cannot find it myself. So, how do I solve this?
Thanks in advance.
EDIT 1:
Tried using scalebands as suggested in comments but it had no effect on the stripes.
same behaviour with scalebands
EDIT 2:
Added relevant code used to draw rectangles. Note the code does not run, snippet is just for viewing the code.
d3.csv("vis_temp.csv", function(d, i, columns) {
for (i = 1, t = 0; i < columns.length-1; ++i){ //calculate total values. ignore last column(usecase)
t += d[columns[i]] = +d[columns[i]];
}
d.total = t;
return d;
}, function(error,data){
if(error){
console.log(error);
return;
}
console.log(data);
dataset = data; // save data outside of d3.csv function
header = data.columns.slice(1); //prop1, prop2..... no sample
header.splice(header.length-1,1); //remove usecase from header
stack = d3.stack().keys(header);
maxValue = d3.max(data,function(d){
return d.total;});
samples = data.map(function(d){
return d.sample;});
xScale = d3.scaleLinear()
.domain([1,samples.length+1])
.range([padding+1,width-xPadding]);
/* using scalebands
xScale = d3.scaleBand()
.domain(d3.range(data.length))
.range([padding+1,width-xPadding]);
*/
yScale = d3.scaleLinear()
.domain([0,maxValue])
.range([height-padding,padding]);
zScale = d3.scaleOrdinal()
.domain(header)
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]); // low profile, stylish colors
xAxis = d3.axisBottom()
.scale(xScale)
.ticks(nbrOfXTicks);
yAxis = d3.axisLeft()
.scale(yScale)
.ticks(nbrOfYTicks);
svg.append("text")
.attr("class","chart_item")
.attr("x",(width-padding-xPadding-20)/2)
.attr("y",padding/2)
.text("measurement");
svg.append("text")
.attr("class","chart_item")
.attr("x",padding/3)
.attr("y",height/2)
.attr("transform","rotate(270,"+padding/3+","+height/2+")")
.text("Time [ms]")
svg.append("text")
.attr("class","chart_item")
.attr("x",(width-padding-xPadding)/2)
.attr("y",height-7)
.text("Sample");
svg.append("g")
.attr("class","axis")
.attr("id","x_axis")
.attr("transform","translate(0,"+(height-padding)+")")
.call(xAxis);
svg.append("g")
.attr("class","axis")
.attr("id","y_axis")
.attr("transform","translate("+padding+",0)")
.call(yAxis);
svg.append("g").attr("class","data");
svg.select(".data")
.selectAll("g")
.data(stack(data))
.enter()
.append("g")
.attr("class","data_entry")
.attr("id",function(d){
return d.key;})
.attr("fill",function(d){
return zScale(d.key);})
.selectAll("rect")
.data(function(d,i){
return d;})
.enter()
.append("rect")
.attr("id",function(d){
return "bar_"+d.data.sample;})
.style("opacity",function(d){
return d.data.usecase=="E" ? val1 : val2;})//some bars opacity change
.attr("width",((width-(padding+xPadding))/data.length)+0.01) // +0.01 to remove whitespace between bars
//.attr("width",xScale.bandwidth()) use this with scalebands
.attr("height",function(d){
return (yScale(d[0])-(yScale(d[1])));
})
.attr("x",function(d){
return xScale(d.data.sample);})
.attr("y",function(d){
return yScale(d[1]);})
.on("mouseover",mouseover) //tooltip on mouseover
.on("mouseout", function() {
d3.select("#tooltip").classed("hidden", true);
});
When using ordinal scale for x axis, you can set the bar padding in the range.
For example:
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeBands([0, width], 'padding');
A regular padding value would be around 0.1, but you can set to 0 since you don't want padding.
Now, you can set your width attr like this: .attr("width", xScale.rangeBand())
I am using D3.js v4.
I have a minimum example working with zooming in and out on a single axis, with the following code:
// Create dummy data
var data = [];
for (var i = 0; i < 100; i++) {
data.push([Math.random(), Math.random()]);
}
// Set window parameters
var width = 330
var height = 200
// Append div, svg
d3.select('body').append('div')
.attr('id', 'div1')
d3.select('#div1')
.append("svg").attr("width", width).attr("height",height)
.attr('id','chart')
// Create scaling factors
var x = d3.scaleLinear()
.domain([0,1])
.range([0, (width - 30)])
var y = d3.scaleLinear()
.domain([0,1])
.range([0,height])
// Create group, then append circles
d3.select('#chart').append('g')
.attr('id','circlesplot')
d3.select('#circlesplot')
.selectAll('circles')
.data(data)
.enter().append('circle')
.attr('cx', function(d,i){ return x(d[0]); })
.attr('cy', function(d,i){ return y(d[1]); })
.attr('r', 4)
// Create y axis, append to chart
var yaxis = d3.axisRight(y)
.ticks(10)
var yaxis_g = d3.select('#chart').append('g')
.attr('id', 'yaxis_g')
.attr('transform','translate(' + (width - 30) +',0)')
.call(yaxis)
// Create zoom svg to the right
var svg = d3.select('#div1')
.append('svg')
.attr('width', 30)
.attr('height', height)
.attr('transform', 'translate('+ width + ',0)')
.call(d3.zoom()
.on('zoom', zoom))
function zoom() {
// Rescale axis during zoom
yaxis_g.transition()
.duration(50)
.call(yaxis.scale(d3.event.transform.rescaleY(y)))
// re-draw circles using new y-axis scale
var new_y = d3.event.transform.rescaleY(y);
d3.selectAll('circle').attr('cy', function(d) { return new_y(d[1])})
}
fiddle here: https://jsfiddle.net/v0aw9Ler/#&togetherjs=2wg7s8xfhC
Putting the mouse just to the right of the yaxis and scrolling gives the zooming function on the y axis.
What I'd like to happen is for the y axis maximum (in this case 1.0) to stay fixed, while zooming only in the other direction. You can kind of see what I mean by placing the mouse at the very bottom and just to the right of the y axis, and see the points cluster at the bottom of the graph.
I think it has to do with using zoom.extent(), but I'm just really not sure where to go from here; advice is greatly appreciated.
Source for this min working example:
http://bl.ocks.org/feyderm/03602b83146d69b1b6993e5f98123175
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 was wondering if you could help me with the follwoing D3js Zoom and pan functionality in the following fiddle: http://jsfiddle.net/moosejaw/nUF6X/5/
I hope the code (although not great) is straight forward.
I have a chart that has total chromosome length by total chromosome length. The tick values are the individual lengths (totals) of each chromosome. The ticks are formatted to be the name of the chromosomes (to look nice to the end user).
The problems that I am having are:
The x-axis and y-axis labels are extending outside the graph area. When I do not supply the tick values explicitly, the labels "disappear" as they should. See:
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickValues(tickValues)
.tickFormat(function(d) {
var ret = bpToChrMBP(d);
return ret.chr;
});
How do I prevent the x axis to not pan to the left before the minimum value? Also not pan to the right past the maximum value? This happens whether or not I am zoomed in. (The same for y-axis, except top and bottom).
Is there a way to "center" the axis labels between the tick marks. The tick marks are not evenly spaced. I tried using subdivide for minor tick marks, but that doesn't subdivide between tick marks correctly.
Any help would be greatly appreciated!
Matt
This Fiddle solves most of your problems: http://jsfiddle.net/CtTkP/
The explanations are below:
I am not sure what you meant by extending beyond the graphs area. Should the labels be insde the chart-area? If you mean that on panning, the labels extend beyond the axis, the problem can be solved by using two more clip-paths judiciously, though this does not allow for graceful fading of values which svg.axis translations provide:
var clipX = svg.append("clipPath")
.attr('id', 'clip-x-axis')
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', margin.bottom);
svg.append("g")
.attr("class", "x axis")
.attr('clip-path', 'url(#clip-x-axis)')
.attr("transform", "translate(0, " + height + ")")
.call(xAxis);
// ...
var clipY = svg.append("clipPath")
.attr('id', 'clip-y-axis')
.append('rect')
.attr('x', - margin.left)
.attr('y', 0)
.attr('height', height)
.attr('width', margin.left);
svg.append("g")
.attr("class", "y axis")
.attr('clip-path', 'url(#clip-y-axis)')
.call(yAxis);
To prevent the panning from extending beyond values, you will have to manually restrict the translate for the zoom:
function zoomed() {
var trans = zoom.translate(),
scale = zoom.scale();
tx = Math.min(0, Math.max(width * (1 - scale), trans[0]));
ty = Math.min(0, Math.max(height * (1 - scale), trans[1]));
zoom.translate([tx, ty]);
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
// ...
This will not allow the graph from panning beyond the limits.
As you are explicitly overriding the tickValues, you can tweak the values to center them:
var tickValues2 = [];
tickValues.forEach(function (t, idx) {
if (idx < tickValues.length - 1) {
tickValues2.push((t + tickValues[idx + 1]) / 2);
}
});
Then instead of using tickValues for xAxis and yAxis, use tickValues2.
The problem is that you are setting tickValues manually, instead of letting the x and y scale do it for you. Try commenting it out: // .tickValues(tickValues)
var x = d3.scale.linear().rangeRound([0, width]).domain(d3.extent(tickValues));
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
// .tickValues(tickValues)
.tickFormat(function(d) {
var ret = bpToChrMBP(d);
return ret.chr;
});
A quick and dirty fix to allow setting tickValues explicitly could be to define a clippingPath for each axis.
You also don't need the make_x_axis function (same for y axis). Check out this zoomable scatterplot example: http://bl.ocks.org/ameliagreenhall/raw/d30a9ceb68f5b0fc903c/
To prevent panning left/right past the cutoffs you would have to re-implement d3.behavior.zoom(). Right now there is a function called mousemove that calls translateTo and this function doesn't have a limit:
function translateTo(p, l) {
l = point(l);
translate[0] += p[0] - l[0];
translate[1] += p[1] - l[1];
}
You can try playing with the dx and dy attributes when you define the axes.
I have a simple bar chart drawn in d3, with vertical bars: http://jsfiddle.net/philgyford/LjxaV/2/
However, it's drawing the bars down, with the baseline at the top of the chart.
I've read that to invert this, drawing up from the bottom, I should change the range() on the y-axis. So, change this:
.range([0, chart.style('height')]);
to this:
.range([chart.style('height'), 0]);
However, that looks like it's drawing the inverse of the chart - drawing in the space above each of the bars, and leaving the bars themselves (drawn from the bottom) transparent. What am I doing wrong?
Per the d3 basic bar chart :
http://bl.ocks.org/mbostock/3885304
You are correct in inverting the range.
Additionally, your rectangles should be added like this:
.attr('y', function(d) { return y(d.percent); } )
.attr('height', function(d,i){ return height - y(d.percent); });
The x and y coordinates for svg start in the top left. You want the y to start on the bottom. The code below assumes you're appending to some function along the lines of:
svg.selectAll('rect')
.data(dataset)
.enter()
.append('rect')
To make the bar plot act as you desire, set the y attribute to begin at distance data[i] above the axis:
.attr('y', function(d) { return height - d; })
Then, you must make the distance extend the remaining data[i] to the axis.
.attr('height', function(d) { return d; })
And that's it!
Setting the y attribute seems to work:
.attr('y', function(d){ return (height - parseInt(y(d.percent))); })
jsfiddle here