D3 triangle positioning to an exact point - d3.js

Good Image
Bad Image
I am trying to create a D3 graph which looks like the Illustrator created design (Good Image 1), but the closest in terms of positioning I have been able to get is the second (Bad Image 2).
I'm really new to D3 and creating SVGs in general, so I may be going about this all wrong. The code below is what I've been able to figure out / find online. It looks like I can't directly adjust positioning of the elements themselves using css positioning? I tried adding classes via the html and also in JQuery with $(.myClass).css..., but everything I do has exactly zero effect. The only thing that seems to work is transform, but it's ugly, as can be seen in the second pic.
var margin = { left:10, right:10, top:10, bottom:10 };
var width = 400 - margin.left - margin.right,
height = 450 - margin.top - margin.bottom;
var g = d3.select("#pyramid-chart-area")
.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 + ")");
d3.json("../data/pyramid_hp.json").then(function(data){
data.forEach(function(d){
d.hp = +d.hp;
});
var x = d3.scaleBand()
.domain(data.map(function(d){ return d.hp; }))
.range([0, width])
.paddingInner(0.3)
.paddingOuter(0.3);
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d){
return d.hp;
})])
.range([height, 0]);
var xAxisCall = d3.axisBottom(x);
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxisCall)
.selectAll("text")
.attr("y", "10")
.attr("x", "-5")
.attr("text-anchor", "end")
.attr("transform", "rotate(-40)");
var yAxisCall = d3.axisLeft(y)
.ticks(3)
.tickFormat(function(d){
return d;
});
g.append("g")
.attr("class", "y-axis")
.call(yAxisCall);
var arc = d3.symbol().type(d3.symbolTriangle)
.size(function(d){ return scale(d.hp); });
var scale = d3.scaleLinear()
.domain([0, 5])
.range([0, width]);
var colors = d3.scaleLinear()
.domain(d3.extent(data, function(d) {return d.hp}))
.range([
'#ffffff',
'#303030'
]);
var group = g.append('g')
.attr('transform','translate('+ 192 +','+ 320 +')')
.attr('class', 'triangle-container');
var line = group.selectAll('path')
.data(data)
.enter()
.append('path')
.attr('d', arc)
// .attr('fill',function(d){ return colorscale(d.hp); })
.attr('fill', d => colors(d.hp))
.attr('stroke','#000')
.attr('stroke-width', 1)
.attr('class', 'triangle')
.attr('transform',function(d,i){ return "translate("+ (i * 20) +","+(i * 10)+")"; });

You can position the symbols, but its tricky - symbol size represents area and as rioV8 notes symbols are positioned by their center. But if you can figure out the properties of the triangle we can place it relatively easily.
In the case of a equilateral triangle, you'll want to know the length of a given side as well as the height of that centroid (which is triangle height/3). So these functions will likely be useful:
// get length of a side/width of upright equilateral triangle from area:
function getWidth(a) {
return Math.sqrt(4 * a / Math.sqrt(3));
}
// get height of the triangle from length of a side
function getHeight(l) {
return Math.sqrt(3)*l/2;
}
Using the height of the centroid we can position the circle where we want with something like:
y = SVGheight - SymbolHeight/3 - marginBottom;
No need for scaling here.
The x values of each symbol do need some scaling to arrange them to your liking. Below I use a linear scale with a range of [width/10,0] arbitrarily, the denominator will change the horizontal skew in this case, there are probably better ways to fine tune this.
With this you can achieve the desired result:
For simplicity's sake, below I'm using data (since you don't show any) that represents pixel area - scaling must be factored into the height and width calculations if scaling areas. I've also included circles on the top of each triangle for possible label use, since we know the dimensions of the triangle this is trivial now
var margin = { left:10, right:10, top:10, bottom:10 };
var width = 400 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var g = 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 + ")")
var data = [
{a: 40000},
{a: 30000},
{a: 20000},
{a: 10000}
];
function getWidth(a) {
return Math.sqrt(4 * a / Math.sqrt(3));
}
function getHeight(l) {
return Math.sqrt(3)*l/2;
}
data.forEach(function(d) {
d.w = getWidth(d.a);
d.h = getHeight(d.w);
})
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d){ return d.w; }) )
.range([width/10,0]);
var arc = d3.symbol().type(d3.symbolTriangle)
.size(function(d){ return d.a; });
var colors = d3.scaleLinear()
.domain(d3.extent(data, function(d) {return d.a}))
.range(['#ffffff','#303030']);
var group = g.append('g')
.attr('transform','translate('+ width/2 +',0)')
.attr('class', 'triangle-container');
var line = group.selectAll('path')
.data(data)
.enter()
.append('path')
.attr('d', arc)
.attr('fill', d => colors(d.a))
.attr('class', 'triangle')
.attr('transform',function(d,i){ return "translate("+ x(d.w) +","+ (height - d.h/3 - margin.bottom ) +")"; });
var circles = group.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return x(d.w); })
.attr("cy", function(d) { return height - d.h - margin.bottom; })
.attr("r", 3);
<script src='https://d3js.org/d3.v5.min.js' type='text/javascript'></script>
axes could present a bit of a challenge

Related

d3js v4 can't remove old data chart after update

I've modified nice AlainRo’s Block for my needs (unfortunately can't link to it, because have not enough reputation), and I can't remove old data chart after entering new data. There is my codepen. In another example I've added merge(), and the chart is well aligned but the old one is still visible and text values are missed.
I spent a lot of time on it, and I run out of ideas.
There's code
barData = [
{ index: _.uniqueId(), value: _.random(1, 20) },
{ index: _.uniqueId(), value: _.random(1, 20) },
{ index: _.uniqueId(), value: _.random(1, 20) }
];
var margin = {top: 20, right: 20, bottom: 50, left: 70},
width = 400 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom,
delim = 4;
var scale = d3.scaleLinear()
.domain([0, 21])
.rangeRound([height, 0]);
var x = d3.scaleLinear()
.domain([0, barData.length])
.rangeRound([0, width]);
var y = d3.scaleLinear()
.domain([0, 21])
.rangeRound([height, 0]);
var svg = d3.select('#chart')
.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("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
function draw() {
x.domain([0, barData.length]);
var brush = d3.brushY()
.extent(function (d, i) {
return [[x(i)+ delim/2, 0],
[x(i) + x(1) - delim/2, height]];})
.on("brush", brushmove);
var svgbrush = svg.selectAll('.brush')
.data(barData)
.enter()
.append('g')
.attr('class', 'brush')
.append('g')
.call(brush)
.call(brush.move, function (d){return [d.value, 0].map(scale);});
svgbrush
.append('text')
.attr('y', function (d){return scale(d.value) + 25;})
.attr('x', function (d, i){return x(i) + x(0.5);})
.attr('dx', '-.60em')
.attr('dy', -5)
.style('fill', 'white')
.text(function (d) {return d3.format('.2')(d.value);});
svgbrush
.exit()
.append('g')
.attr('class', 'brush')
.remove();
function brushmove() {
if (!d3.event.sourceEvent) return; // Only transition after input.
if (!d3.event.selection) return; // Ignore empty selections.
if (d3.event.sourceEvent.type === "brush") return;
var d0 = d3.event.selection.map(scale.invert);
var d = d3.select(this).select('.selection');;
var d1 =[d0[0], 0];
d.datum().value = d0[0]; // Change the value of the original data
d3.select(this).call(d3.event.target.move, d1.map(scale));
svgbrush
.selectAll('text')
.attr('y', function (d){return scale(d.value) + 25;})
.text(function (d) {return d3.format('.2')(d.value);});
}
}
draw();
function upadateChartData() {
var newBarsToAdd = document.getElementById('charBarsCount').value;
var newBarData = function() {
return { index: _.uniqueId(), value: _.random(1, 20) }
};
newBarData = _.times(newBarsToAdd, newBarData);
barData = _.concat(barData, newBarData)
draw();
};
Is it also possible to remove cross pointer and leave only resize, when I'm dragging top bar border?
You're appending g elements twice. This:
svgbrush.enter()
.append('g')
.attr('class', 'brush')
.merge(svgbrush)
.append('g')
.call(brush)
.call(brush.move, function (d){return [d.value, 0].map(scale);});
Should be:
svgbrush.enter()
.append('g')
.attr('class', 'brush')
.merge(svgbrush)
.call(brush)
.call(brush.move, function (d){return [d.value, 0].map(scale);});
Here is your updated Pen: http://codepen.io/anon/pen/VmavyX
PS: I also made other changes, declaring some new variables, just to organize your enter and update selections and solving the texts problem.

d3 v4 scaleBand ticks

I have data like the following
date,values
2016-10-01,10
2016-10-02,20
2016-10-03,30
2016-10-04,5
2016-10-05,50
2016-10-06,2
2016-10-07,7
2016-10-08,17
and am generating a bar chart using the following code
var margin = {top: 20, right: 20, bottom: 70, left: 40},
width = 800 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var parseDate = d3.timeParse("%Y-%m-%d");
var x = d3.scaleBand().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>Month of " + d.date + ":</strong> <span style='color:red'>" + d.value + " sales</span>";
})
var svg = d3.select("#barg").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.call(tip);
data = d3.csvParse(d3.select("pre#data2").text());
data.forEach(function(d) {
d.date = parseDate(d.date);
d.value = +d.value;
});
x.domain(data.map(function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", "-.55em")
.attr("transform", "rotate(-90)" )
svg.append("g")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Value ($)");
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.date); })
.attr("width", x.bandwidth() - 5)
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
So the problem I am having is that I have ordinal data, but for large cardinality (for instance, 120 data points) The x axis has way too many ticks. I have tried a few things like tickValues, but when I use this, my x axis tick points all show up on top of each other. Ideally I would like 10 tick points or so, when the cardinality is high. Any ideas?
This can be done using tickValues indeed. For instance, in this demo, we have 200 values, so the axis is absolutely crowded:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 100);
var data = d3.range(200);
var xScale = d3.scaleBand()
.domain(data.map(function(d){ return d}))
.range([10, 490]);
var xAxis = d3.axisBottom(xScale);
var gX = svg.append("g").call(xAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
Now, the same code using tickValues:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 100);
var data = d3.range(200);
var xScale = d3.scaleBand()
.domain(data.map(function(d){ return d}))
.range([10, 490]);
var xAxis = d3.axisBottom(xScale)
.tickValues(xScale.domain().filter(function(d,i){ return !(i%10)}));
var gX = svg.append("g").call(xAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
In this last snippet, tickValues uses the remainder operator to show only 1 in every 10 ticks:
.tickValues(xScale.domain().filter(function(d,i){
return !(i%10)
}));
Here is a general solution to this problem using tickFormat(...). We can define a minimum acceptable width for our ticks, then skip every nth tick based on this minimum.
d3
.axisBottom(xScale)
.tickFormat((t, i) => {
const MIN_WIDTH = 30;
let skip = Math.round(MIN_WIDTH * data.length / chartWidth);
skip = Math.max(1, skip);
return (i % skip === 0) ? t : null;
});
let skip = ... is a rearrangement of the inequality ChartWidth / (NumTicks / N) > MinWidth. Here N represents the tick "step size", so we are asserting that the width of every nth tick is greater than the minimum acceptable width. If we rearrange the inequality to solve for N, we can determine how many ticks to skip to achieve our desired width.

trouble with scales and hebin in d3js

I am trying to use the hexbin layout with data that is normally distributed around 0 - all the examples use data centered around the center of the screen, so the scales are the same as the screen scales (except for y inversion)
I've tried to modify the scale functions to account for possible negative values. It works for the y-scale, but the x-scale gives NaNs, and the hexagons are plotted off the screen upper left. That is not the only problem - I would like to programmatically determine the bin size for the hexbin function - in my data series, all of the values are 'binned' into only one to three hexagons, and I need them spread out over the available domain.. here is my code
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.hexbin.v0.min.js?5c6e4f0"></script>
<script>
minMultArray =function(arr,index){
var min = arr[0][index];
for (i=0;i<arr.length;i++) {
min = (min>arr[i][index]?arr[i][index]:min);
}
return min;
};
maxMultArray =function(arr,index){
var max = arr[0][index];
for (i=0;i<arr.length;i++) {
max = (max< arr[i][index]?arr[i][index]:max);
}
return max;
};
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var randomX = d3.random.normal(0, 5),
randomY = d3.random.normal(0, 6),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
var minX = minMultArray(points,0);
var minY = minMultArray(points,1);
//var minZ = minMultArray(points,2);
var maxX = maxMultArray(points,0);
var maxY = maxMultArray(points,1);
//var maxZ = maxMultArray(points,2);
var color = d3.scale.linear()
.domain([0, 20])
.range(["white", "steelblue"])
.interpolate(d3.interpolateLab);
var hexbin = d3.hexbin()
.size([width, height])
.radius(20);
alert('minX='+minX +' maxX='+maxX);
var x = d3.scale.linear()
.domain([minX, maxX])
.range(0,width);
alert('xScale(3)='+x(3));
var y = d3.scale.linear()
.domain([minY, maxY])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(6, -height);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickSize(6, -width);
console.log('hex = ' +hexbin(points));
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("clipPath")
.attr("id", "clip")
.append("rect")
.attr("class", "mesh")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll(".hexagon")
.data(hexbin(points))
.enter().append("path")
.attr("class", "hexagon")
.attr("d", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + (d.x) + "," + (d.y) + ")"; })
.style("fill", function(d) { return color(d.length); });
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
</script>
After more debugging the hexbin functions, they are not compatible with negative and/or fractional domains- so I solved this by mapping my original data by linear scales up to the height and width of the hexagon plots. Then bin size is controlled by radius. I also modified the hexbin binning function to handle three element arrays, and can compute stats on the third element, using color or size to show mean/median/stddev/max/min. If interested, I can post the code on github...

Reading tsv file with a filter

I am trying to plot a simple bar graph from a tsv file and would appreciate some help.
First of all, here is a sample input file:
ind count
0 1228
1 0
2 238
3 9
I need to plot number of records (y-axis) vs. "count" (x-axis).
The user has the option via the HTML page to decide at what "count" value to
suppress the data. For example, suppress all records where count=0. Changing the number of records and with that the range of data affects the scales of both axes.
I have no problem getting all the data and setting up the scales for the axes. I do that as the first step right after reading in the data from the file (d3.tsv...). But how do I do that when I only want to use partial data of the input file? I use
d3.max(data, function(d){ return +d.indx}
to get the number of records in my input file.
I did play with the filter() method a bit but (besides being unsuccessful so far) I still think that the right place to filter data out would be right away - when reading the input file.
Chances are this is so obvious that the answer will make me blush but
my brain has given up. With that, thank you for responding!
And now, here is some of my code:
function drawBarGraph( minCountNum){
d3.tsv("../../data/test.tsv", function( error, data){ //get the number of rows and the largest number of the "total" column for scaling purposes
//y-axis: use number of lines in input file
var bar_num = d3.max(data, function(d) { return +d.indx;} ); //use plus sign (+) in front of a variable to ensure conversion to a numeric value.
//x-axis: use largest value of "total reads" from input file;
var data_max = d3.max(data, function(d) { return +d.total;} );
//set up canvas size
var margin = { top: 30, right: 20, bottom: 40, left: 50},
width = 800 - margin.left - margin.right,
height = 2 * bar_num + 2; //each bar is 1px high and spaced by 1px; add 2px to the bottom to allow a little space between the xAxis and the first data point
var bar_height = 1; //yes, draw the bars 1px high
//set up the x and y scales
var xScale = d3.scale.linear()
.domain([0, data_max]) //define original range with min and max
.range([0, width]); //define what resulting range should be
var yScale = d3.scale.linear()
.domain([0, bar_num*2])
.range([0, height]);
//set up the x and y axes
var xAxis = d3.svg.axis()
.scale( xScale)
.orient("bottom")
//.ticks( 5); //let d3 decide on the number of ticks used or calculate based on longest x-value
var yAxis = d3.svg.axis()
.scale( yScale)
.orient("left")
.ticks( 0); //no ticks or make them the index or node id's
var canvas = 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 + ")");
var div = d3.select("body").append("div")
.attr("class", "tooltip") //add the CSS tooltip
.style("opacity", 0);
canvas.selectAll("rect")
.data(data)
.enter()
//.filter(function(d) { return +d.total > 0; }) //this returns 'undefined'
.append("svg:a")
.attr("xlink:show", "new")
.attr("xlink:href", function(d){return ncbi_url + d.taxon_id;})
.append("rect")
.attr("width", function(d) { return xScale(d.total);})
.attr("height", bar_height)
.attr("y", function(d,i){ return i * (bar_height + 1);})
.attr("fill", function(d) { return d.color;})
.on("mouseover", function(d){
div.transition()
.duration(200)
.style("opacity", .9);
div.html(
"Node ID: " + d.id + "<br/>Tot. Reads: " + d.total)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d){
div.transition()
.duration(500)
.style("opacity", 0);
});
canvas.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
canvas.append("text") //add the x-axis label
.attr("x", (width / 2))
.attr("y", height + margin.bottom)
.style("text-anchor", "middle")
.text("Total Reads");
canvas.append("g")
.call(yAxis);
canvas.append("text") //add the y-axis label
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left + 20)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Node #");
canvas.append("text") //add a title
.attr("x", (width / 2))
.attr("y", 0 - (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "20px")
.style("text-decoration", "underline")
.text("Node Plot");
})
}
Yes, the best way to do this would be right after getting the data to make sure that all subsequent operations consider only the subset that you want. You can use the .filter() function for this.
var threshold = ...;
var newdata = data.filter(function(d) { return +d.total > threshold; });
(I've taken total as the field to filter on like in the code you've posted).

Maximum width for column in bar chart

I'm looking for a way of limiting the column width in a chart, I'm sure this ought to be relatively easy but I cant find a way of doing it.
I'm populating a chart from some dynamic data, where the number of columns can vary quite dramatically - between 1 and 20.
e.g: sample of csv
Location,Col1
"Your house",20
Location,Col1,Col2,Col3,Col4,Col5
"My House",12,5,23,1,5
This is working fine, and the col widths are dynamic, however when there is only one column in the data, I end up with one bar of width 756 (the whole chart), and I dont like the way this looks.
What I'd like to do is only ever have a maximum column of width 100px irrespective of the number of columns of data.
Below is my script for the chart
Many thanks,
<script>
var margin = {
top : 40,
right : 80,
bottom : 80,
left : 40
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear().range([ 0, width ]);
var y = d3.scale.linear().range([ height, 0 ]);
var x0 = d3.scale.ordinal()
.rangeRoundBands([0, width], .05);
var x1 = d3.scale.ordinal();
var y = d3.scale.linear()
.range([height, 0]);
var chart = d3.select("body").append("svg")
.attr("class","chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var legendChart = d3.select("body").append("svg")
.attr("class","chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("/sampledata.csv.txt", function(error, data) {
// Use the first row of csv for header names
var reasonNames = d3.keys(data[0]).filter(function(key) {
return key !== "Location";
});
//console.log(reasonNames);
data.forEach(function(d) {
d.reasons = reasonNames.map(function(name) {
return {
name : name,
value : +d[name]
};
});
//console.log(d.reasons);
});
x0.domain(data.map(function(d) {return d.Location; }));
x1.domain(reasonNames).rangeRoundBands([0, x0.rangeBand()]);
console.log(x0.rangeBand());
y.domain([0, d3.max(data, function(d) { return d3.max(d.reasons, function(d) { return d.value; }); })]);
var maxVal = d3.max(data, function(d) { return d3.max(d.reasons, function(d) { return d.value; }); });
//console.log(maxVal);
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
//.tickFormat(d3.format(".2s"));
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
var location = chart.selectAll(".name")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(d.Location) + ",0)"; });
location.selectAll("rect")
.data(function(d) { return d.reasons; })
.enter().append("rect")
.attr("width", x1.rangeBand()-2)
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d,i) { return "#"+3+"9"+i; /*color(d.name);*/ });
chart.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", function(d) { return x1(d.name)+ x.rangeBand() / 2; })
.attr("y", function(d) { return y(d.value); })
.attr("dx", -3) // padding-right
.attr("dy", ".35em") // vertical-align: middle
.attr("text-anchor", "end") // text-align: right
.text("String");
var legend = legendChart.selectAll(".legend")
.data(reasonNames.slice().reverse())
.enter()
.append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")";
});
legend.append("rect")
//.attr("x", width - 18)
.attr("x", 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {/*console.log(i);*/return "#" + 3 + "9" + i;
});
legend.append("text")
//.attr("x", width - 24)
.attr("x", 48)
.attr("y", 9).attr("dy",".35em")
//.style("text-anchor", "end")
//.text(function(d,i) { return String.fromCharCode((65+i))+i; });
.text(function(d) { return d; });
});
</script>
The easiest way to achieve this is by changing the line
.attr("width", x1.rangeBand()-2)
to
.attr("width", Math.min(x1.rangeBand()-2, 100))
You might also want to adjust the starting position and/or padding.
Code for adjusting starting position if anyone is stuck on it:
.attr("x", function(d, i) { return x1(d.seriesName) + (x1.rangeBand() - 100)/2 ;})
P.S. : referring answer from Lars.
Setting an absolute maximum width for the columns doesn't allow proper rendering for different screen resolutions, div sizes, etc.
In my case, I just wanted the columns not to look so large when the number of columns itself is small
I found it easier and more straight-forward to play with the scale definition, by changing the maximum width (where all columns will fit), their inner and outer paddings.
var w = 600
// var w = parseInt(d3.select(parentID).style('width'), 10) // retrieve the div width dynamically
var inner_padding = 0.1
var outer_padding = 0.8
var xScale = d3.scale.ordinal().rangeRoundBands([0, w], inner_padding, outer_padding)
When rendering the plot, I just ran a switch/if-else statement, which assigns different padding values. The lower the number of columns to plot, the greater the outer_padding (and eventually inner-padding) values I use.
This way, I keep the plots responsive.
I am able to change the width of the bar using the above answer. But unfortunately, my X Axis labels are not aligned when there is a single bar in the chart and it uses the max width set.
var tradeGroup = svg.selectAll("g.trade")
.data(trades)
.enter()
.append("svg:g")
.attr("class", "trade")
.style("fill", function (d, i) {
return self.color(self.color.domain()[i]);
})
.style("stroke", function (d, i) {
return d3.rgb(self.color(self.color.domain()[i])).darker();
});
var aWidth = Math.min.apply(null, [x.rangeBand(), 100]);
// Add a rect for each date.
var rect = tradeGroup.selectAll("rect")
.data(Object)
.enter()
.append("svg:rect")
.attr("x", function (d) {
return x(d.x);
})
.attr("y", function (d) { return y( (d.y || 0) + (d.y0 || 0)); })
.attr("height", function (d) { return y(d.y0 || 0) - y((d.y || 0) + (d.y0 || 0)); })
.attr("width", Math.min.apply(null, [x.rangeBand(), 100]));
For completeness the full answer would look like this:
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", (d) -> x1(d.name) + (x1.rangeBand() - d3.min([x1.rangeBand(), 100]))/2)
.attr("width", d3.min([x1.rangeBand(), 100]))
.attr("y", (d) -> y(d.grade) )
.attr("height", (d)-> height - y(d.value) )
(coffeescript syntax)
Note this include the full answer, the 'width' and the 'x' settings. Also 'x' settings is accounting for a when 100 width is not the min value.
Thought I'd share that I came up with a slightly different answer to this. I didn't want to hard code in a maximum bar width because 1) it wasn't responsive to different screen sizes and 2) it also required playing with the x-coordinate attribute or accepting some irregular spacing.
Instead, I just set a minimum number of bars, based on the point where the bars became too wide (in my case, I found that less than 12 bars made my chart look weird). I then adjusted the scaleBand's range attribute, proportionately, if there were less than that number of bars. So, for example, if the minimum was set to 12 and there were only 5 items in the data, rather than rendering each of them at 1/5th of the full width, I scaled the range down to 5/12ths of the original width.
Basically, something like this:
// x is a scaleBand() that was previously defined, and this would run on update
var minBarSlots = 12;
if (data.length < minBarSlots) {
x.range([0, width*(data.length/minBarSlots)])
}
else {
x.range([0, width])
}`

Resources