am plotting histogram based on Data ( which is changing dynamically ) , but the height of some bars are not fitting the svg zone ( the exceed the svg area )
this is piece of code which i have doubt on :
private drawBars(data: any[]): void {
let f = Math.min.apply(Math, this.fixedData.map(function (o) {
return o.xAxis;
}));
/** Create the X-axis band scale */
const x = d3.scaleBand()
.range([f, 240])
.domain(data.map(d => d.xAxis))
.padding(0);
/** Create the Y-axis band scale */
const y = d3.scaleLinear()
.domain([0, this.axisMax])
.range([this.height, 0])
.nice();
/** Create and fill the bars */
this.svg.selectAll("*").remove()
this.svg.selectAll("bars")
.data(data, d => d)
.enter()
.append("rect")
.attr("x", d => x(d.xAxis))
.attr("y", d => y(d.yAxis))
.attr("width", x.bandwidth())
.attr("height", (d) => this.height - y(d.yAxis))
.attr("fill", (d) => this.colorPicker(d))
}
and here is a proof of that weird behaviour :
any ideas and i would be thankful !
Your problem is caused by this.axisMax being set way too low, here:
const y = d3.scaleLinear()
.domain([0, this.axisMax])
.range([this.height, 0])
.nice();
You need to recalculate it based on the available data.
The domain of a scale holds the range of possible values it accepts, but, at least for scaleLinear, scaleTime, and some others, being outside that range doesn't throw an error or even return NaN. For these types of scales, the domain is instead used to identify a linear transformation between the input values and output pixels.
For example
Suppose you have a linear scale with domain [0, 10] and range [0, 100]. Then, x = 0 gives pixel value 0, and x = 10 gives 100. However, x = 20 gives pixel value 200, which is more than the given range.
I tried a lot of datasets and I don't know why, I have a issue with data_histo.csv. The x axis seems reverse and then, bars can't be displayed. With data_histo2.csv or data_histo3.csv, all is good.
An explanation could be nice!
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/d3#5.0.0/dist/d3.min.js"></script>
</head>
<body>
<svg class="histogramme" width="960" height="600"></svg>
<script>
//select
let svgHisto = d3.select(".histogramme")
//dimension
let margin = {top: 20, right: 10, bottom: 20, left: 80}
let width = +svgHisto.attr("width") - margin.left - margin.right
let height = +svgHisto.attr("height") - margin.top - margin.bottom;
let g1 = svgHisto.append("g")
.attr("transform",`translate(${margin.left}, ${margin.top})`);
//data
d3.csv("data_histo.csv").then(function(data) {
console.log(data);
//define x and y axis
let x = d3.scaleLinear();
let y = d3.scaleBand();
x.domain(d3.extent(data, function(d) { return d.value0; })).nice()
.rangeRound([0, width]);
y.domain(data.map(function(d) { return d.libreg; }))
.rangeRound([0, height]).padding(0.1);
//add x axis
g1.append("g")
.attr("class", "axis x_axis")
.attr("transform",`translate(0,${height})`)
.call(d3.axisBottom(x));
//add y axis
g1.append("g")
.attr("class", "axis y_axis")
.call(d3.axisLeft(y));
//bar chart
g1.selectAll(".bar1")
.data(data)
.enter().append("rect")
.attr("class", "bar bar1")
.attr("x", function(d) {return x(Math.min(0,d.value0)); })
.attr("y", function(d) { return y(d.libreg) + 10; })
.attr("width", 0)
.attr("height", y.bandwidth() - 20);
//animate
g1.selectAll(".bar1")
.transition()
.duration(1000)
.delay(function(d,i){return i*100})
.attr("width", function(d) { return x(d.value0); });
});
</script>
</body>
</html>
With data_histo.csv
"codreg","libreg","year0","value0","year1","value1"
"03","Guyane",2009,4,2014,4.6
"04","La Réunion",2009,8.2,2014,9.8
"11","Île-de-France",2009,12.6,2014,13.9
"01","Guadeloupe",2009,13.3,2014,15.8
"32","Hauts-de-France",2009,14.7,2014,16.1
"02","Martinique",2009,14.7,2014,17.6
"44","Grand Est",2009,16.5,2014,18
"84","Auvergne-Rhône-Alpes",2009,16.8,2014,18.3
"52","Pays de la Loire",2009,17.1,2014,18.6
"28","Normandie",2009,17.2,2014,19
"53","Bretagne",2009,18.5,2014,20.2
"24","Centre-Val de Loire",2009,18.7,2014,20.4
"27","Bourgogne-Franche-Comté",2009,18.8,2014,20.7
"76","Occitanie",2009,19.3,2014,20.8
"93","Provence-Alpes-Côte d''Azur",2009,19.5,2014,21.3
"94","Corse",2009,20.2,2014,21.5
"75","Nouvelle-Aquitaine",2009,20.2,2014,21.8
With data_histo2.csv
"codreg","libreg","year0","value0","year1","value1"
"84","Auvergne-Rhône-Alpes",2013,39.1,2017,41.7
"27","Bourgogne-Franche-Comté",2013,42.3,2017,44.4
"53","Bretagne",2013,39.6,2017,44.7
"24","Centre-Val de Loire",2013,40.5,2017,46.8
"94","Corse",2013,24.2,2017,30.8
"44","Grand Est",2013,41.3,2017,45.4
"01","Guadeloupe",2013,55.5,2017,56.5
"03","Guyane",2013,33.1,2017,33.2
"32","Hauts-de-France",2013,45.8,2017,47.3
"11","Île-de-France",2013,40.1,2017,42.6
"02","Martinique",2013,52.5,2017,50.2
"28","Normandie",2013,42.6,2017,46.2
"75","Nouvelle-Aquitaine",2013,40,2017,44.4
"76","Occitanie",2013,40.3,2017,43.7
"52","Pays de la Loire",2013,40.6,2017,45.8
"93","Provence-Alpes-Côte d''Azur",2013,38.5,2017,42.6
"04","La Réunion",2013,54.2,2017,54.6
"06","Mayotte",2013,,2017,
Here is my code : https://plnkr.co/edit/B8qEQ4dlUdRHhkQvzjZx?p=preview
There are two issues with your code:
D3 parses csv/tsv/dsv entries as text. So when you load your csv, you get rows that look like this:
{
"codreg": "03",
"libreg": "Guyane",
"year0": "2009",
"value0": "4",
"year1": "2014",
"value1": "4.6"
}
When you set your scale with extent, you aren't using the numerical extent. You could coerce your data to a number like so:
data.forEach(function(d) {
d.value0 = +d.value0;
})
Secondly, if you do this you'll notice some peculiar behavior in the placement of the bars:
You can see that the bars start to the left of the plot area. The reason is that you are using a linear scale, and plot the start of the bars like so:
.attr("x", function(d) {return x(Math.min(0,d.value0)); })
You want your bars to start at x(4) - which is where the x value that marks the interception with the y axis. Instead you are using x(0), which will naturally be to the left of where you want. This works in your second example, because x(0) also happens to be the x value that intercepts the y axis. Instead, you can simply use:
.attr("x",0)
This marks the left edge of the plot area, which is likely where you want all your bars to be anchored to.
Here's a forked plunkr.
One thing to note, is that your shortest bar will always be non-visible: the start and end points will be the same. This is because the extent of the scale goes from the smallest value to the largest value - and the smallest value, marking the left boundary of the plot area, is the value of the shortest bar. To change this you can modify the scale's domain, perhaps using 0 as the first value and then using d3.max to find the uppermost value.
I have a couple of issues with a my D3 punchcard (plunk is here: https://plnkr.co/edit/vIejaTBrGrV07B8UWxOb):
It is looking crowded, I have zooming set up but I can't figure out how to set the initial view to a more "readable" scale, with the dots more spaced out. I plan to add in tooltips and varied radii for the dots.
//Create scale functions
var xScale = d3.scale.linear()
.domain([1900, 2020])
.range([0, w]);
var yScale = d3.scale.linear()
.domain([0, (deptlist.length)])
.range([h, 0]);
//console.log(deptlist.length);
//var rScale = d3.scale.linear()
//.domain([0, d3.max(dataset, function(d) { return d[1]; })])
//.range([2, 5]);
//Define X axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickFormat(function(d) {
//console.log(d);
return d;
});
//Define Y axis
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.tickFormat(function(d) {
//console.log(deptlist[d]);
return deptlist[d];
})
.ticks(deptlist.length - 1);
I can't get my deptlist[] to sort alphabetically.
It is also quite slow...
1. Better distinguish circles
You'll be better able to distinguish the dots if you make them initially smaller, and then make them bigger when the zoom level increases.
d3.selectAll('.dot')
// ...
.attr('r', 2)
Then when you zoom in, you can make the r value a product of the difference between the values in the xScale.domain. For example...
function zoomed() {
svg.selectAll(".dot")
.attr("cx", function(d) { return xScale(+d.year); })
.attr("cy", function(d) { return yScale(deptlist.indexOf(d.dept)); })
.attr('r', function(d) {
return 120 / (xScale.domain()[1] - xScale.domain()[0]);
});
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
}
There are many different ways you can scale your dots, this is just one way of going about it. Your xScale.domain() returns the domain of your xScale, e.g., [1900, 2020]. By subtracting the first value from the second, we get a value that can be used as a reference point from which can scale the dots.
I simply took the largest possible difference, 120, and divided it by the current value of the scale (which is changed on zoom). This creates a larger value the more that the zoom is increased.
2. Sort Y axis alphabetically
Using some of D3's array methods you can make your code much more declarative.
Instead of doing this:
var deptlist = [];
dataset.forEach(function(d) {
if(deptlist.indexOf(d.dept) == -1) deptlist.push(d.dept);
});
You can use d3.map, array.keys(), and d3.descending to (a) return only d.dept to your array, (b) get only the unique values from the array, and (c) use the JS native sort array method in combination with d3.ascending to sort them alphabetically.
var deptlistUnsorted = d3.map(dataset, function(d) {
return d.dept;
}).keys();
var deptlist = deptlistUnsorted.sort(d3.descending);
As a final note, your code is running slowly because you have a console.log statement inside of a forEach loop of an array of several thousand objects. This puts a lot of strain on the browser and is generally something to avoid when dealing with arrays of that size.
I updated your plunkr to reflect the code above.
I have a bar chart where I want to make the gap more pronounced between the 6th and the bar in my chart and the 12th and 13th bar in my chart. Right now I'm using .rangeRoundBands which results in even padding and there doesn't seem to be a way to override that for specific rectangles (I tried appending padding and margins to that particular rectangle with no success).
Here's a jsfiddle of the graph
And my code for generating the bands and the bars themselves:
var yScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([padding, h- padding], 0.05);
svg.selectAll("rect.bars")
.data(dataset)
.enter()
.append("rect")
.attr("class", "bars")
.attr("x", 0 + padding)
.attr("y", function(d, i){
return yScale(i);
})
.attr("width", function(d) {
return xScale(d.values[0]);
})
.attr("height", yScale.rangeBand())
You can provide a function to calculate the height based on data and index. That is, you could use something like
.attr("height", function(d,i) {
if(i == 5) {
return 5;
}
return yScale.rangeBand();
})
to make the 6th bar 5 pixels high. You can of course base this value on yScale.rangeBand(), i.e. subtract a certain number to make the gap wider.
Here's a function for D3 v6 that takes a band scale and returns a scale with gaps.
// Create a new scale from a band scale, with gaps between groups of items
//
// Parameters:
// scale: a band scale
// where: how many items should be before each gap?
// gapSize: gap size as a fraction of scale.size()
function scaleWithGaps(scale, where, gapSize) {
scale = scale.copy();
var offsets = {};
var i = 0;
var offset = -(scale.step() * gapSize * where.length) / 2;
scale.domain().forEach((d, j) => {
if (j == where[i]) {
offset += scale.step() * gapSize;
++i;
}
offsets[d] = offset;
});
var newScale = value => scale(value) + offsets[value];
// Give the new scale the methods of the original scale
for (var key in scale) {
newScale[key] = scale[key];
}
newScale.copy = function() {
return scaleWithGaps(scale, where, gapSize);
};
return newScale;
}
To use this, first create a band scale...
let y_ = d3
.scaleBand()
.domain(data.map(d => d.name))
.range([margin.left, width - margin.right])
.paddingInner(0.1)
.paddingOuter(0.5)
... then call scaleWithGaps() on it:
y = scaleWithGaps(y_, [1, 5], .5)
You can create a bar chart in the normal way with this scale.
Here is an example on Observable.
I am attempting to add labels/axis/titles/etc to a D3 bar graph. I can get something close to the right size, however I end up clipping off part of the last bar (so the last bar is skinnier than the others).
Here is the pertinent code:
var x = d3.scale.linear()
.domain([0, object.data.length])
.range([0, object.width]);
var y = d3.scale.linear()
.domain([object.min-(object.max-object.min)*.15, object.max (object.max-object.min)*.15])
.rangeRound([ object.height - 30, 0]);
var vis = d3.select(object.ele)
.append("svg:svg")
.attr("width", object.width)
.attr("height", object.height)
.append("svg:g");
vis.selectAll("g")
.data(object.data)
.enter().append("rect")
.attr("x", function(d, i) { return x(i); })
.attr("y", function(d) { return object.height - y(d.value); })
.attr("width", object.width/object.data.length - 1)
.attr("height", function(d) { return y(d.value); })
.attr("transform", "translate(30,-30)");
At the moment everything (labels, axis, and so on) is 30px. How do I correctly alter the graph to make room for whatever else I need?
You are cutting off your last bar because you use translate the x coordinate but your the range of your x is only to the width without the extra 30 pixels.
Also it may be easier to simplify your y domain to use .domain([object.min, object.max]) then have the "y" and "height" functions reversed. This way you start the rect at the y(d.value) and make it's height object.height - y(d.value).
I would create three groups initially, one for your y axis, one for x axis, and then another for the bars. Draw your bars inside the last group and then translate the whole group itself instead of each individual bar. Increase the size of your object.width and object.height to match the total space you want.