In one bar chart I am working on I have created parallel ticks to x Axis in order to give references to the viewer.
The problem is in text I have added. The text is ordered inversely as it should and I have no idea how to keep correct order.
I understand the problem is here:
chart.selectAll("line")
.data(y.ticks(4))
.enter().append("line")
.attr("x1", 60)
.attr("x2", (barWidth + 50) * data.length)
.attr("y1", y)
.attr("y2", y)
.style("stroke", "#ccc");
chart.selectAll(".rule")
.data(y.ticks(4))
.enter().append("text")
.attr("class", "rule")
.attr("x", 30)
.attr("y", y)
.attr("text-anchor", "middle")
.text(String);
But I don't know how to fix it.
Here is the code in jsFiddle.
Thanks in advance.
The problem is that the y coordinates are counted from the top and not the bottom, so you need to reverse all the values. The updated jsfiddle is here; the relevant changes are below.
var y = d3.scale.linear()
.domain([0, d3.max(data, function(datum) { return datum.value; })])
.rangeRound([height-20, 0]);
Reversed the values in rangeRound such that the lowest value is mapped to the top.
chart.selectAll("rect")
.data(data)
.enter()
.append("svg:rect")
.attr("x", function(datum, index) { return x(index); })
.attr("y", function(datum) { return y(datum.value); })
.attr("height", function(datum) { return height - y(datum.value); })
...
Changed how y and height are computed accordingly.
Related
I am creating a horizontal bar chart using d3. And I am using an animation to "grow" the chart at startup. Here is the code.
// Create the svg element
d3.select("#chart-area")
.append("svg")
.attr("height", 800)
.attr("width", 800);
.data(dataValues) // This data is previously prepared
.enter().append("rect")
.style("fill", "blue")
.attr("x", function () { return xScale(0); }) // xScale is defined earlier
.attr("y", function (d) { return yScale(d); }) // yScale is defined earlier
.attr("height", yScale.bandwidth()) // yScale is defined earlier
// Initial value of "width" (before animation)
.attr("width", 0)
// Start of animation transition
.transition()
.duration(5000) // 5 seconds
.ease (d3.easeLinear);
// Final value of "width" (after animation)
.attr("width", function(d) { return Math.abs(xScale(d) - xScale(0)); })
The above code would work without any problem, and the lines would grow as intended, from 0 to whichever width, within 5 seconds.
Now, if we change the easing line to the following
// This line changed
.ease (d3.easeElasticIn);
Then, the ease would try to take the width to a negative value before going to a final positive value. As you can see here, d3.easeElasticIn returns negative values as time goes by, then back to positive, resulting in width being negative at certain points in the animation. So the bars do not render properly (because SVG specs state that if width is negative, then use 0)
I tried every solution to allow the bars to grow negatively then back out. But could not find any. How can I fix this problem?
Thanks.
As you already know, the use of d3.easeElasticIn in your specific code will create negative values for the rectangles' width, which is not allowed.
This basic demo reproduces the issue, the console (your browser's console, not the snippet's console) is populated with error messages, like this:
Error: Invalid negative value for attribute width="-85.90933910798789"
Have a look:
const svg = d3.select("svg");
const margin = 50;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([margin, 300]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attr("width", function(d) {
return xScale(d.x) - margin
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
So, what's the solution?
One of them is catching those negative values as they are generated and, then, moving the rectangle to the left (using the x attribute) and converting those negative numbers to positive ones.
For that to work, we'll have to use attrTween instead of attr in the transition selection.
Like this:
.attrTween("width", function(d) {
return function(t){
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t){
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
In the snippet above, margin is just a margin that I created so you can see the bars going to the left of the axis.
And here is the demo:
const svg = d3.select("svg");
const margin = 100;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([0, 300 - margin]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attrTween("width", function(d) {
return function(t) {
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t) {
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
The annotation should read “min y-value: ” where is the minimum y. I wrote the following codes which labels the same smallest y value to all points but I want the label only displaying on the one scatter point who has the smallest y value
Please help!
var min = d3.min(dataset, function(d) { return d3.min(d); });
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {return "min y-value"+min;})
.attr("x", function(d) {return xScale(d[0]);})
.attr("y", function(d) {return yScale(d[1]);})
You could add a filter in order to only show the text on certain data elements.
// If you really want the min of y-values - is it
// possible that you meant d[1] in the following min function?
var min = d3.min(dataset, function(d) { return d[1]; });
svg.selectAll("text")
.data(dataset)
.enter()
.filter(function(d) { return d3.min(d) > min })
.append("text")
.text(function(d) {return "min y-value"+min;})
.attr("x", function(d) {return xScale(d[0]);})
.attr("y", function(d) {return yScale(d[1]);})
I want to use D3 for generating charts from JSON files. How do I combine/relate bar chart and bubble chart so when you click on either it should provide you details of both charts in a legend.
The each bubble must be below and center of the each bar. It must share x-axis of bar charts.
There are two different data sources for them.
No. of bars = No. of bubbles
I created the xscale
var xScale = d3.scale.ordinal() .domain( d3.range(dataset.length))
.rangePoints([0, w-50]);
This same scale is shared while drawing bar as well as circles.
I have changed your codepen code
Below is the complete code after change
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d,i) {
//return i * (w / dataset.length);
return xScale(i) ;
})
.attr("y", function(d) {
return h - (d * 10);
})
.attr("width", barWidth)
.attr("height", function(d) {
return d * 10;
})
.attr("fill","#d38e71");
var circles = svg.selectAll("circle")
//.data(dataset)
.data(num)
.enter()
.append("a")
.attr("xlink:href", "http://google.com")
.attr("xlink:title",function(d){
return "No. of campagins sent - "+d;
})
.attr("target","_blank")
.append("circle");
circles.attr("cx", function(d,i) {
/* return i* (wB / dataset.length)+(wB / dataset.length)/2-2;*/
return (xScale(i) + barWidth/2);
})
.attr("cy", function(d) {
return hB-35;
})
.attr("r", function(d) {
return rScale(d);
})
Note the following code for bar
.attr("x", function(d,i) {
//return i * (w / dataset.length);
return xScale(i) ;
})
Note the following code for circles.
circles.attr("cx", function(d,i) {
/* return i* (wB / dataset.length)+(wB / dataset.length)/2-2;*/
return (xScale(i) + barWidth/2);
})
One suggestion
Width of the bar is set via this statement (w / dataset.length - barPadding) This can be stored in a variable. So it will not calculate for each member in the chart.
Well the rendering of bar chart works fine with default given data. The problem occurs on the button click which should also cause the get of new data set. Updating the x-axis y-axis works well but the rendering data causes problems.
First Ill try to remove all the previously added rects and then add the new data set. But all the new rect elements gets added into wrong place, because there is no reference to old rects.
Here is the code and the redraw is in the end of code.
http://jsfiddle.net/staar2/wBNWK/9/
var data = JSON.parse('[{"hour":0,"time":147},{"hour":1,"time":0},{"hour":2,"time":74},{"hour":3,"time":141},{"hour":4,"time":137},{"hour":5,"time":210},{"hour":6,"time":71},{"hour":7,"time":73},{"hour":8,"time":0},{"hour":9,"time":68},{"hour":10,"time":70},{"hour":11,"time":0},{"hour":12,"time":147},{"hour":13,"time":0},{"hour":14,"time":0},{"hour":15,"time":69},{"hour":16,"time":67},{"hour":17,"time":67},{"hour":18,"time":66},{"hour":19,"time":0},{"hour":20,"time":0},{"hour":21,"time":66},{"hour":22,"time":210},{"hour":23,"time":0}] ');
var w = 15,
h = 80;
var xScale = d3.scale.linear()
.domain([0, 1])
.range([0, w]);
var yScale = d3.scale.linear()
.domain([0, d3.max(data, function (d) {
return d.time;
})])
.rangeRound([5, h]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(5);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
var chart = d3.select("#viz")
.append("svg")
.attr("class", "chart")
.attr("width", w * data.length - 1)
.attr("height", h);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(d, i) {
return xScale(i) - 0.5;
})
.attr("y", function(d) {
return h - yScale(d.time) - 0.5;
})
.attr("width", w)
.attr("height", function(d) {
return yScale(d.time);
});
chart.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d) {
if (d.time > 10) {
return Math.round(d.time);
}
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "#FFF")
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i) + w / 2;
})
.attr("y", function(d) {
return h - yScale(d.time) - 0.5 + 10;
});
chart.append("g")
.attr("transform", "translate(0," + (h) + ")")
.call(xAxis);
function redraw() {
// This the part where the incoming data set also changes, which means the update to x-axis y-axis, labels
yScale.domain([0, d3.max(data, function (d) {
return d.time;
})]);
var bars = d3.selectAll("rect")
.data(data, function (d) {
return d.hour;
});
bars
.transition()
.duration(500)
.attr("x", w) // <-- Exit stage left
.remove();
d3.selectAll("rect") // This is actually empty
.data(data, function (d) {
return d.hour;
})
.enter()
.append("rect")
.attr("x", function(d, i) {
console.log(d, d.day, xScale(d.day));
return xScale(d.day);
})
.attr("y", function(d) {
return yScale(d.time);
})
.attr("width", w)
.attr("height", function (d) {
return h - yScale(d.time);
});
}
d3.select("button").on("click", function() {
console.log('Clicked');
redraw();
});
Agree with Sam (although there were a few more issues, like using remove() without exit(), etc.) and I am putting this out because I was playing with it as I was cleaning the code and applying the update pattern. Here is the FIDDLE with changes in code I made. I only changed the first few data points but this should get you going.
var data2 = JSON.parse('[{"hour":0,"time":153},{"hour":1,"time":10},{"hour":2,"time":35},{"hour":3,"time":150},
UPDATE: per request, adding logic to consider an update with new data. UPDATED FIDDLE.
Since you're binding the same data to bars, the enter selection is empty. Once you remove the existing bars, you append a new bar for each data point in the enter selection - which again is empty. If you had different data, the bars should append.
If you haven't read through it already, the general update pattern is a great resource for understanding this sort of thing.
I'm working with D3's bullet chart and am trying to figure out how to display the actual measures number just to the right of the measures rectangle. Since I want to do this for every bullet chart, I figure it'd be best to do it right in the bullet.js code. I'm rather new to D3 so any help would be much appreciated! Here is the link to Mike Bostock's bullet chart example with the bullet.js included at the bottom.
It looks like the measures code is handled in this snippet:
// Update the measure rects.
var measure = g.selectAll("rect.measure")
.data(measurez);
measure.enter().append("rect")
.attr("class", function (d, i) { return "measure s" + i; })
.attr("width", w0)
.attr("height", height / 3)
.attr("x", reverse ? x0 : 0)
.attr("y", height / 3)
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", reverse ? x1 : 0);
measure.transition()
.duration(duration)
.attr("width", w1)
.attr("height", height / 3)
.attr("x", reverse ? x1 : 0)
.attr("y", height / 3);
I thought I could just add something like this after the rect is appended but I've had no such luck.
measure.enter().append("text")
.attr("dy", "1em")
.text(function (d) { return d.measurez; })
.attr("x", reverse ? x0 : 0)
.attr("y", height / 3)
.transition()
.duration(duration)
.attr("width", w1)
.attr("x", reverse ? x1 : 0);
Thank you in advance for your consideration!
You almost got it -- there're just two small things to consider. First, you can't call .enter() twice. Once the enter selection has been operated on, it's merged into the update selection and your second selection will be empty. This is fixed easily by saving the selection in a variable, but in this case I would recommend making a separate selection for the text labels.
var measureLabel = g.selectAll("text.measure")
.data(measurez);
measureLabel.enter()....;
Second, to position the text to the right of the rect, you need to take not only the position, but also the width into account when computing the position of the text element. Also, you can omit a few elements that are not relevant to text elements.
measureLabel.enter()
.append("text")
.attr("class", function(d, i) { return "measure s" + i; })
.attr("dy", "1em")
.attr("dx", "1em")
.text(String)
.attr("x", reverse ? function(d) { return w0(d) + x0(d); } : w0)
.attr("y", height / 3);
measureLabel.transition()
.duration(duration)
.attr("x", reverse ? function(d) { return w1(d) + x1(d); } : w1);
Complete example here.