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.
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 created a d3 plot with linear scale x axis. I placed text label on xaxis using tickFormat().
When I zoom the plot, the tick values extend beyond xaxis. How do I make the ticks (with text labels) beyond xaxis (svg rect) disappear?
var xAxis = d3.svg.axis().scale(x)
.orient("bottom")
.tickValues(idForXAxis)
.tickFormat(function(d,i){ return valuesForXAxis[i] })
Fiddle: https://jsfiddle.net/wicedp/q1b2dsdt/4/
The problem here is not tickFormat. You can easily find the culprit: remove tickValues and the ticks will stay within the range.
The best idea is simply letting D3 generates the ticks. But, if you want to show all the values in the valuesForXAxis array in the original view and still have a zoom behaviour formatting the ticks, there is a workaround. Set the tick number as the number of values inside valuesForXAxis array:
xAxis.ticks(idForXAxis.length)
And then, inside your zoomed function:
xAxis.ticks(idForXAxis.length/d3.event.scale);
Here is the updated fiddle: https://jsfiddle.net/pfrdvLtx/
I was able to fix the problem by adding this piece of code -
var clipX = svg.append("clipPath")
.attr('id', 'clip-x-axis')
.append('rect')
.attr('x', -10)
.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)
.selectAll(".tick text")
.call(wrap, 0);
This way I think xaxis was getting correctly translated and clipped. Here is the fiddle - https://jsfiddle.net/wicedp/q1b2dsdt/12/
I'm trying to customize my x axis on a d3 chart; I want to add two labels, "left" and "right" at both ends of it.
I tried this, but it doesn't work:
var xlabels = ["Left", "Right"]
var xAxis = d3.svg.axis()
.scale(xScale)
.tickValues(xScale.domain())
.orient("bottom")
.ticks(2)
.tickFormat(xlabels)
;
Do you know how to do it?
axis.tickFormat as the name implies defines the label for each of the ticks, if you want to add new labels at both ends you need to add them on your own:
Assuming that you have a reference to the root svg in the var svg
svg.append('text')
.attr('text-anchor', 'start')
.attr('y', function () {
// y position of the left label
// typically a value less than the height of the svg
})
.attr('x', function () {
// x position of the left label
// typically a value near to 0
})
.text('Left')
svg.append('text')
.attr('text-anchor', 'end')
.attr('y', function () {
// y position of the right label
// typically a value less than the height of the svg
})
.attr('x', function () {
// x position of the right label
// typically a value near the width of the svg
})
.text('Right')
Also have a look at http://bl.ocks.org/mbostock/1166403, these lines define a label like the one you need:
svg.append("text")
.attr("x", width - 6)
.attr("y", height - 6)
.style("text-anchor", "end")
.text(values[0].symbol);
I'm playing around with D3 and want tick lines to cut through a linear time graph across the vertical axis. The tick line elements are there, with the correct vectors, but they do not appear. What appears instead is the path element that runs horizontally with the tick labels.
JSFiddle Link
var width = 960;
var height = 200;
var container = d3.select(".timeTable");
var svg = container.append("svg")
.attr("width", width)
.attr("height", height);
var roomID = container.attr("data-room");
var times = [
{"from":"2012-12-27 00:00:00","until":"2012-12-27 12:00:00"},
{"from":"2012-12-27 00:00:00","until":"2012-12-27 23:59:00"},
{"from":"2012-12-27 02:00:00","until":"2012-12-27 04:00:00"},
{"from":"2012-12-27 03:00:00","until":"2012-12-27 21:00:00"},
{"from":"2012-12-27 03:30:00","until":"2012-12-27 04:50:00"},
{"from":"2012-12-27 05:00:00","until":"2012-12-27 12:00:00"},
{"from":"2012-12-27 09:00:00","until":"2012-12-27 15:00:00"},
{"from":"2012-12-27 13:00:00","until":"2012-12-27 23:00:00"},
{"from":"2012-12-27 13:00:00","until":"2012-12-27 23:30:00"},
{"from":"2012-12-27 20:00:00","until":"2012-12-27 23:59:00"},
{"from":"2012-12-27 20:00:00","until":"2012-12-27 22:00:00"},
{"from":"2012-12-27 23:00:00","until":"2012-12-27 23:30:00"},
{"from":"2012-12-28 01:00:00","until":"2012-12-28 13:00:00"}
];
function draw(times) {
// domain
var floor = d3.time.day.floor(d3.min(times, function (d) { return new Date(d.from); }));
var ceil = d3.time.day.ceil(d3.max(times, function (d) { return new Date(d.until); }));
// define linear time scale
var x = d3.time.scale()
.domain([floor, ceil])
.rangeRound([0, width]);
// define x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.ticks(d3.time.hours, 6)
.tickSize(4);
// draw time bars
svg.selectAll("rect")
.data(times)
.enter().append("rect")
.attr("class", "timeRange")
.attr("width", function (d, i) { return x(new Date(d.until)) - x(new Date(d.from)); })
.attr("height", "10px")
.attr("x", function (d, i) { return x(new Date(d.from)); })
.attr("y", function (d, i) { return i * 11; });
// draw x axis
svg.append("g")
.attr("class", "xAxis")
.attr("transform", "translate(0, " + (height - 23) + ")")
.call(xAxis);
}
draw(times);
The path element generated simply overlaps the ticks, but the ticks are not visible even with path removed.
The desired tick effect is shown here Population Pyramid - ticks on the vertical axis have a line that cuts through the rest of the graph.
Is there different behavior I need to be aware of for time scales?
Much appreciated.
Chrome 23, D3 v3
The trick to getting the tick lines into the plot area is to actually make a second axis and hide the labels. So your code plus the grid lines looks something like (fiddle):
// draw x axis
var xAxisLine = svg.append("g")
.attr("class", "xAxis")
.attr("transform", "translate(0, " + (height - 23) + ")")
.call(xAxis);
xAxisLine.selectAll("path.domain").attr("stroke","black").style('fill','none');
xAxisLine.selectAll("line.tick").style('stroke','black');
xAxis.tickSize(height-23);
var xAxisLineOver = svg.append("g")
.attr("class", "xAxis-overlay")
.attr("transform", "translate(0,0)")
.call(xAxis);
xAxisLineOver.selectAll("path.domain").attr("stroke-width",0).style('fill','none');
xAxisLineOver.selectAll("line.tick").style('stroke','red');
xAxisLineOver.selectAll("text").text("");
I'm not sure this is the exact same problem I had. What worked for me was:
.tick line{
stroke: black
}
Chrome is very strict regarding rendering SVG. So keep this in mind:
For axes specify their full RGB value (so #ff0000 instead of just #f00).
Path stroke widths are tricky. If they are less than 1 (px) and you have included
shape-rendering: crispEdges;
in CSS styles than any web browser might not display such path (or when the chart is resized to different size).
In that case comment or delete the "crispEdges" rendering and you should be fine (though in this case you leave the browser the smoothing decision).