I have a D3 stacked bar chart that is working great except that the last bar overhangs the right side of X axis and the left side of the axis is not touching the Y axis. This is because I had to move the bars so that they would be centered over the tick lines. I thought that if I padded the time scale by a date on either end that would take care of it, but instead the bars just spread out to take up the available space.
Here is a JSFiddle: http://jsfiddle.net/goodspeedj/cvjQq/
Here is the X scale:
var main_x = d3.time.scale().range([0, main_width-axis_offset - 5]);
This is my attempt to pad the X axis by a date on either end:
main_x.domain([
d3.min(data.result, function(d) {
return d.date.setDate(d.date.getDate() - 1);
}),
d3.max(data.result, function(d) {
return d.date.setDate(d.date.getDate() + 1);
}
)]);
The calculation for the x attribute on the bars is:
.attr("x", function(d) { return main_x(d.date) - (main_width/len)/2; })
This is a minor thing, but it is really annoying me. Thanks!
The main problem was that you had a clip path in your SVG that clipped parts of the plotting region. Once that is removed, you can see the full graph, but the first and last bars will still "hang" over the ends of the axis.
To change that, you have to extend the axis, e.g. by adding a day or two either way when computing the minimum and maximum for the scale.
Related
I have a problem with my horizontal stacked bar.
The problem: sometimes I got really small values, so one of bands' segment (sub-band ?) has very small width. (Please look below on the picture, rect of each color I call segment):
In some cases I even can't see this segment on the chart. In future I want to show text on each segment (percentage values). But since width of segment can be too small, I need a solution to show text.
Possible solutions: First I thought to set minimal segment width. But it seems the chart will not look OK after this. Also I tried to play around xScale:
const maxX = 1.4; // this value selected experimentally
const xScale = d3.scaleLinear().range([ 0, width ]).domain([ 0, maxX ]);
But for some cases segment still too small (please look at third segment, I marked it with blue color).
So for now I choose solution to zoom stacked bar by selected segment. For example, I want to zoom red segment, all red segments on the chart should be stretched, rest of segments should be shrinked. And total width of bands should be the same (as initial). So width of the chart should be same.
This is my code example: https://jsfiddle.net/8d1te7cb/2/
The problem here that I can't correctly zoom only selected segment. I tried to detect currect group of segments:
const currentNode = d3.select(event.sourceEvent.target).node();
const currentGroup = d3.select(currentNode.parentNode).node();
and then I tried to rescale only segments related to currentGroup:
group.selectAll("rect.segment")
// .attr("transform", event.transform.toString())
.attr("x", (d, i, n) => {
if (n[i].parentNode === currentGroup) {
return xScale(d[0]) + PADDING_TO_SHOW_TEXT;
}
return xScale2(d[0]) + PADDING_TO_SHOW_TEXT;
})
.attr("width", (d, i, n) => {
if (n[i].parentNode === currentGroup) {
return xScale(d[1]) - xScale(d[0])
}
return xScale2(d[1]) - xScale2(d[0])
});
But actually all segments not from currentGroup keep their width and selected group stretched too much, so it moved outside of the axis (and actually after that I got width of the chart changed).
The question: how to fix zoom to allow only selected group stretch and rest of group shrink (and keep initial width of the chart) ?
Extra question: does it exist any another way to show segments proportionally even if for some of them value is too small?
UPDATED: my initial project on typescript, so I forgot to remove some ts hints, this is a bit updated example: https://jsfiddle.net/8d1te7cb/3/ (removed ts from commented code)
Well, finally I implemented what I want.
Result: https://jsfiddle.net/2yqbvrwp/
Details: First of all, I decided to remove second scale, bc I need it only in runtime. So, my zoom function is changed to:
d3.zoom().scaleExtent([ 1, 10 ])
// I am not sure if I need the line below since code works same without it
// I think below is default value
//.translateExtent([ [ 0, 0 ], [ width, height ] ])
.on("zoom", (event) => {
const transform = event.transform;
// the new scale I use for runtime
// the important part here is clamp method. It prevents from moving
// segments outside of axis
const newScaleX = transform.rescaleX(xScale).clamp(true);
// so I just applied new scale to current axis
xAxis.scale(newScaleX)
svg.select("g.axis-x").call(xAxis);
svg.selectAll("rect.segment")
.attr("x", (d) => newScaleX(d[0]))
.attr("width", (d) => newScaleX(d[1]) - newScaleX(d[0]));
})
svg.call(zoom);
Also I removed rect transform from css and added margin left in code. But I think I will return it back since native css is faster.
I have the following Line Graph (CodePen) and as you can see, the Y-Axis isn't very smart. It begins from 0 when it should begin from somewhere near 2200.
My guess was that this would be to do with scales:
// set the ranges
var x = d3.scalePoint().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
But I tried all sorts to get this to behave the "smart" and "flexible" way and couldn't.
How can I make the axis smarter? So if tomorrow I feed the chart a different set of data that starts at 150, the code is smart enough to draw relevant axis values.
If you don't want the Y axis starting at 0, you shouldn't set the lower value in the domain to zero, as you did:
y.domain([0, d3.max(result, function(d) {
return d.consumption;
})]);
If you want the Y axis to start next to the minimum value, you should use:
y.domain([d3.min(result, function(d) {
return d.consumption; })*0.975,
d3.max(result, function(d) {
return d.consumption; })*1.025
]);
Here, the numbers 0.975 and 1.025 are just values to make the axis going a little bit below and after the minimum and maximum values, respectively. You can change them or remove them.
Here is your updated CodePen: http://codepen.io/anon/pen/EgOBvO?editors=0010
PS: while in a bar chart it's highly recommended to always use a zero baseline for the Y axis, there is no problem using a non-zero baseline for the Y axis in a time series (line chart).
I'm using d3.js v4. I'm currently implementing programmatic zoom. This Panning and Zooming Tutorial has helped tremendously. My zoom works with scrolling wheel, but I want to create buttons to zoom. I know what necessary for zooming and panning is a translation [tx, ty] and a scale factor k. I'm using timescale for my x-Axis. I've managed to get tx and scale factor of k, by getting the pixel value of p1 (point 1) and p2(point 2) on the x-axis and then using those values to get a k (Scale factor). Like such:
var k = 500 / (xScale(p2) - xScale(p1)); //500 is desired pixel diff. between p1 and p2, and xScale is my d3.scaleTime() accessor function.
// for this zoom i just want the first value and last value to be at scale difference of the entire width.
Then I calculate tx by this:
var tx = 0 - k * p1;
Then feeding it into a d3.zoomIdentity() and rescaling my xdomain. I created a button to zoom back out. The issue is when I zoom in and then try to use the button to zoom out, it zooms out, but shrinks the x-axis. I can't seem to findout why its shrinking the x-axis instead of zooming back out correctly.
My JSFiddle
https://jsfiddle.net/codekhalifah/Lmdfrho7/2/
What I've Tried
Read zoom Documentation
Read through chapter on zoom in D3.js in action
My Code
After wheel zoom is applied I run this function:
function zoomed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type === "brush") return; // ignore zoom-by-brush
var t = d3.event.transform;
console.log(t);
console.log(xScale.domain());
xScale.domain(t.rescaleX(x2).domain());
usageAreaPath.attr("d", area);
usageLinePath.attr('d',line);
weatherAreaPath.attr('d',weatherChart.area);
focus.select(".axis--x").call(xAxis);
focus.selectAll('.circle')
.attr('cx', function(d) { return xScale(getDate(d)); })
.attr('cy', function(d) { return yScale(d.kWh); })
.attr("transform", "translate(0," + 80 + ")");
... other non related items
}
The zoom works properly, but after zooming in and then manually attempting to zoom back normal position I want.
My manual zoom button function
function programmaticZoom(){
var currDataSet = usageLinePath.data()[0], //current data set
currDataSetLength = currDataSet.length,//current data set length
x1 = currDataSet[0].usageTime, //getting first data item
x2 = currDataSet[currDataSetLength-1].usageTime, //2nd data item
x1px = xScale(moment(x1)), //Get current point 1
x2px = xScale(moment(x2)); // Get current point 2
// calculate scale factor
var k = width / (x2px - x1px); // get scale factor
var tx = 0 - k * x1px; // get tx
var t = d3.zoomIdentity.translate(tx).scale(k); //create zoom identity
xScale.domain(t.rescaleX(window.x2).domain());
usageAreaPath.attr("d", area);
usageLinePath.attr('d',line);
weatherAreaPath.attr('d',weatherChart.area);
focus.select(".axis--x").call(xAxis);
focus.selectAll('.circle')
.attr('cx', function(d) { return xScale(getDate(d)); })
.attr('cy', function(d) { return yScale(d.kWh); })
.attr("transform", "translate(0," + 80 + ")");
}
I've been looking at this for a little while now, and I got it sort of working, there's one thing I can't figure out but you might be able to since you seem more familiar with d3 than I am. In your programmaticZoom() function, I noticed that tx is the offset for where the start of the graph is, and k is the scale. Using this, I changed the block:
var k = width / (x2px - x1px); // get scale
var tx = 0 - k * x1px; // get tx
var t = d3.zoomIdentity.translate(tx).scale(k);
to
var k = 1;
var t = d3.zoomIdentity.scale(k);
First, when k = 1.0, the graph will fit in the window perfectly. The reason I believe setting k to its former value was wrong is that, when you increase the value of k so that k > 1.0, it stretches the width of the graph past the screen. The content on the graph has to be readjusted so that it can take up as much space on the graph as possible, while still being within the bounds of the screen. With k < 1, which is what happens with width / (x2px - x1px);, the graph shrinks to be less than the size of the screen. Readjusting like this will only make the graph's content fit to take up the maximum that it can within the graph, but since the graph is smaller than the screen, it will be readjusted to fit the shrunken graph and appear smaller than the screen.
I got rid of tx entirely because it offsets where the graph starts at. When the graph is zoomed in, it's width is stretched past the screen. It makes sense to offset here because you need to have your offset equal to where you want to begin viewing the graph, and allow the parts you don't need to remain off of the screen. In the case where you're zooming out all the way, the graph is the size of the screen, so offsetting is going to cause your graph to not start at the beginning of the screen and instead push part of it off of the screen. In your case, with k already shrinking the graph, the offset causes the shrunken graph to appear in the middle of the screen. However if the graph was not shrunken, the offset would push part of the graph off of the screen.
By changing that, the zoom out button appears to work, however there is still a problem. In the zoomed() function, you set var t = d3.event.transform;. The d3.event.transform contains values of k, x, and y that need to be 1, 0, and 0 respectively when completely zoomed out. I cannot change these values in the programmaticZoom() function though, because d3.event.transform only exists after an event was fired, specifically the mouse wheel. If you are able to get these values to be k = 1, x = 0, y = 0 only when the zoom button is clicked, the issue should be completely fixed.
Hopefully that helped some, I'm not very familiar with d3 but this should solve most of your problem and hopefully give you an idea of what was going wrong.
I haven't found any examples of this so far, but I was hoping the readers at Stack have run into this situation before.
We have a Y axis where the range from 0-200 isn't very interesting. We'd like to "squish" this area of the y axis and have the gap become larger as the Y axis values grow so that the top end of the spectrum shows a lot more movement in the line graph.
This would mean that the ticks on the Y axis would grow as they went up in range and allow the graph to show larger differences near the top end of the spectrum.
I can see that Y doesnt take a function, but has anyone done something like this and if so does anyone have any working examples?
If its not possible with D3 has anyone done something similar with highcharts?
NOTE
Please make sure that the ideas in the graph are still clear if you use such a scale. Be aware that the lines are only built based on start and end point and are not "kinked" when traversing the break in the axis.
When writing the answer I wasn't aware that you have a line graph and values below 200.
ANSWER
Assuming that you want to use a linear scale, you could e.g. try the following:
newScale = d3.scale.linear().domain([0,200,1000]).range([0,100,1000])
An axis could e.g. look like this:
var xAxis = d3.svg.axis()
.scale(newScale)
.orient('bottom');
d3.select("#someSVG").append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + 123 + ')')
.call(xAxis);
Consequently, you'd get:
Looking at your codepen, you might want to try:
var y = d3.scale.linear().range([height,height * 9 / 10, 0]);
and then use as domain:
y.domain([0, 200, d3.max(data, function (d) {
return d.close;
})]);
so in this example, you allocate 1/10th of the height to the first 200.
I am trying to build a line chart using D3 and I am using this example.
The problem that I am facing is that when I am passing a dataset that contains the same value in the linear.domain() fof the y then the line of the line chart gets drawn on top of the horizontal axis and as a result is not visible. Also no numbers are printed on the y axis.
Here is a JSFiddle with my code.
The metrics variable contains 3 objects, three different timestamps with the same value. On the result panel the line is get drawn on top of x axis. If you change one of the three values from 5 to any other number then the line chart gets drawn as normal.
Any ideas on how can I draw the line in the middle of the y axis when the metrics variable contains values of the same value?
Thanks
Instead of line
y.domain(d3.extent(metrics, function(d) { return d.close; }));
use
y.domain([d3.max(metrics, function(d) { return d.close; }) + 0.01,
d3.min(metrics, function(d) { return d.close; }) - 0.01]);
The thing is, if y scale is just segment of length 0, like [5.0, 5.0] (or any other number other that 5.0), d3 is totally confused, and refuses to display anything on y axis, and places 5.0 right at origin.
With new code: the following is obtained for the same case:
]
Here is also link to jsfiddle.