I am working in a problem domain where I need to display data at millisecond time intervals. I have found that when you zoom in far enough on a graph, the behavior of the zoom/pan functions suddenly change! If you are using a time scale, this happens exactly when your axis starts displaying milliseconds.
Specifically, the behavior change is how d3 zooms around the mouse cursor. For intervals measured in seconds or above, the gridlines zoom with respect to the cursor position. i.e Mousing over 6PM and scrolling will cause 6PM to take up more space. But once you get to milliseconds, the grid seems to be zoomed from the far left, regardless of your cursor position. The panning also stops moving the axis around at all.
I am not very experienced in d3, but to me, this looks like a bug. I see nothing in the zoom documentation about this. Is there a reason why it behaves this way, and if so, can it be overridden somehow?
In the links below, as soon as you zoom far enough that the x-axis labels change from :YY to .YYY (ie. :50 to .653) you should see the problem.
This shows the "broken" behavior: http://jsfiddle.net/JonathanPullano/LuYDY/3/
This shows the "correct" behavior: http://jsfiddle.net/JonathanPullano/LuYDY/2/
EDIT: I have discovered that the problem is not in the zoom behavior, but rather the time.scale() itself. I made another fiddle which uses setTimeout to automatically rescale the axis without using zoom. The problem still persists. Try it here.
http://jsfiddle.net/JonathanPullano/LuYDY/4/
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var x = d3.time.scale()
.domain([-5000, 5000])
.range([0, width]);
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("rect")
.attr("width", width)
.attr("height", height);
var gXAxis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
function chart(selection) {
selection.each(function(data) {
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickSize(-height);
gXAxis.call(xAxis);
});
}
update();
function update() {
var start = new Date(x.domain()[0].getTime() + 10);
var end = new Date(x.domain()[1].getTime() - 10);
if(start.getTime() < end.getTime()) {
x.domain([start,end]);
setTimeout(update,10);
}
svg.call(chart);
}
The zoom and pan is behaving as expected, but the tick marks/ grid lines are being drawn at different values each time, which makes it look erratic at first and at extreme zooms makes it look like the grids aren't moving.
I've modified your fiddle to use a more complete tick format function, and also to draw a circle in the same point (on the scales) each time:
http://jsfiddle.net/LuYDY/5/
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickSize(-height)
.tickFormat(d3.time.format("%H:%M.%L") );
As you can see, the values zoom and pan as expected, but the tick values always get spaced at the same points on the window, rather than at round numbers of milliseconds.
The bug is simply that the time scale's definition of "nice" round datetime value falls apart at this scale. Explicitly telling the scale to be .nice() doesn't work, either.
The time scale methods try to find a suitable time unit for choosing ticks, and that doesn't work when the entire domain is less than 1 second. What would be ideal is if it switched to a linear scale definition of "nice" (i.e., round to multiples of powers of 10).
I.e. if you do:
x = d3.time.scale()
.domain([10, 477])
.nice();
And then query the x domain and convert the values back to integers, you still get exactly [10,477]. If you did that with a linear scale, the domain would be rounded off to [0,500]. Similarly, tick values for that time scale are placed at exact fractions of the domain, but are placed at round numbers for the linear scale.
You could make this an issue request on github. In the meantime, if your data domain is in the millisecond range, maybe just use a linear scale.
Related
I'm trying to update my x axis in a D3js bar chart (is partially working) depending on a user filter, the bars are actually changing but is not doing it well. I don't really know where is the problem and I need some help.
in this part of the code I'm updating the bar chart
function update(selectedGroup) {
svg.selectAll("rect").remove()
var groups = d3.map(dataFilter, function(d){return(d.group)}).keys();
x.domain(groups);
var dataFilter = result.filter(function(d){return d.group==selectedGroup});
console.log(dataFilter);
var rectG=rectangulos(dataFilter)
}
the complete bar chart
how is working now:
the result should be something like this
I have an live example here
There is a relatively straightforward reason you are seeing this behavior.
When the domain of the scale x is all the groups x.bandwidth() is small. But when the domain of x is only one value, x.bandwidth() is large. In both cases, the first band starts in the same location.
Next we have a nested scale here xSubgroup - the range of this is equal to x.bandwidth(). When the domain of x changes, we need to update the range of xSubgroup. If we don't do this, the bars will still be very thin and start at the beginning of the axis (as the bars' bandwidth aren't changing even if the group's bandwidth does). You don't update the sub scale's range, but we need to do that:
x.domain(groups);
xSubgroup.range([0, x.bandwidth()])
With this we get the update we're looking for.
But the axis labels remain unchanged. Updating a scale doesn't update the axis unless we explicitly do so. I'll break up your method chaining and store a reference for the g holding the axis:
var xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
xAxis.selectAll("text")
...
Now we can update the axis, I'm carrying forward the text styling as well. You can simplify the code by using an update function to do all the entering/exiting/updating of axes and data - here we have some duplication in that both the initial set up and the update function have overlap.
To update the axis we use:
// Call the axis again to update
xAxis.call(d3.axisBottom(x))
xAxis.selectAll("text")
.style("text-anchor", "end")
.attr("font-size", "55px")
.attr("y", "-7")
.attr("x", "-7")
.attr("transform", "rotate(-90)");
Which gives the desired behavior if I understand correctly, updated plunkr
I am trying to create something like this geology visual below that maps soil by its composition (of a given three components):
To my knowledge there is only d3.axisBottom() but nothing like d3.axis45Degrees(). So unfortunately my attempts at this visual were cut short virtually right off the bat because I can't even figure out how to set up the axes for a triangular coordinate plane.
Question
Can d3 handle such an axis configuration, or are there any other d3 methods that would be relevant for a task such as this?
This question is borderline "too broad". However, I believe that it's an interesting question, since the documentation may lead someone to believe that only vertical/horizontal axes are possible.
You can always rotate the axis (any axis, be it axisBottom, axisTop, axisRight or axisLeft) and rotate the <text> elements back.
Here is a simple demo (full of magic numbers):
const svg = d3.select("svg");
const scale = d3.scaleLinear([10, 380]);
const axis = d3.axisLeft(scale);
const axis2 = d3.axisRight(scale);
const axis3 = d3.axisBottom(scale);
const axisGroup = svg.append("g")
.attr("transform", "rotate(30, 100, 400)")
.call(axis)
.selectAll("text")
.attr("transform", "rotate(-30, -10, 0)");
const axisGroup2 = svg.append("g")
.attr("transform", "rotate(-30, 108, -378)")
.call(axis2)
.selectAll("text")
.attr("transform", "rotate(30, 10, 0)");
const axisGroup3 = svg.append("g")
.attr("transform", "translate(14,333)")
.call(axis3)
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="450" height="400"></svg>
Change those magic numbers accordingly. By the way, in that triangle you shared in your question the internal angles are 60 degrees (not 45), so here I'm rotating the axes by 30 degrees.
Finally, it's worth mentioning that I just transformed (translate, rotate etc...) those axes. For a real plot, like the one in your image, you'll have to create a whole math just to calculate where in the SVG the values of the 3 coordinates will fall.
I've spent enough time modifying the stacked bar chart sample that it would be wise to get a second pair of eyes to look at what I'm doing wrong. The modified sample is in a js fiddle.
My main changes are these:
1: On line 10 reversed the array for range
y = d3.scale.linear().range([height, 0])
2: On lines 17 and 22 added axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(format);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
3: Line 62 changed the addition of y0 and y (this is the change that might be creating problem)
.attr("y", function (d) { return y(d.y) - y(d.y0); })
As you can see bars are going way above the specified height.
If someone could tell me what I'm doing wrong I'd be very thankful. I spent too much time looking just by myself. I do think I'm very close as chart is displaying other than the height problem.
There is one more change, which you haven't mentioned: that you the chart containing g is now translated to the top-left point instead of the bottom-left point, as was in the original plot.
Changing the following two lines around the line you mentioned will solve the problem:
.attr("y", function (d) { return y(d.y + d.y0); })
.attr("height", function (d) { return y(d.y0) - y(d.y +d.y0); })
Demo
This is how I generally do these calculations with an inverted scale such as y = d3.scale.linear().range([height, 0]):
Here, the point S(y + y0) will be the value of the point closer to the top, or with the lower y value, and the height of the bar will be = S(y0) - S(y + y0) or = abs(S(y0+y) - S(y0)), whichever you find more palatable.
However, to be honest, there is plenty of trial and error involved before I actually draw this down. Then I do more trial and error to find the solution and convince myself that this drawing helped.
So ... your mileage might vary. :)
I am having a bit of a trouble scaling my graph, according to the length on the bars. For example, in the jsfiddle, I can't draw a bar beyond the data point of size 25. I know one way to fix this would be to make the width and height of the body larger. But I was thinking scaling the entire graph would be much more efficient, so that one bar doesn't end up looking abnormally large.
http://jsfiddle.net/NkkDC/
I was thinking, I would have to scale the "y" function here, but I wasn't sure how.
bars.on("click", clickEvent)
.transition().duration(2000).delay(200)
.attr("y", function(d, i) { return i * 20; })
.attr("width", x)
.attr("height", 20);
Thanks in advance :)
The input domain of your xScale can change every time you add a new value (since you could add a new maximum), so we need to recalculate the xScale when we re-render the chart. Moving the declaration of the x-scale inside your render function should do the trick :
var xScale = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, 420]);
http://jsfiddle.net/NkkDC/1/
I created a bar chart (fiddle) using D3.js.
I have one (probably very simple) question for which I would be glad to get some support:
Why is the x-axis too short?
Older question, and my first answer attempt on Stack, but hopefully still useful:
From what I can tell your X domain only goes to XMax, which will be the bottom-left point of the last bar of the graph - it's not coming up "short", it just doesn't know how wide the bar is after XMax.
(It does not account for you adding the barWidth of 20 pixels, and your text placement does account for the additional width, which seems to be why you're seeing the problem only with the Axis.)
In order to extend the axis all the way to the end, you'll have to account for the additional barWidth in your domain, or append the additional barWidth another way. That might be possible by adding another half day or so on to your xMax, or by shifting each bar to the left of its X position (-barWidth) instead of +barWidth.
The x-axis isn't too short (it's exactly tall enough for your max value) but you might have wanted it to be nice.
var y = d3.scale.linear()
.domain([0, yMax])
.range([h - margin.top - margin.bottom, 0])
.nice();