Varying the gaps between specific bars in a D3 bar chart - javascript

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.

Related

Histogram with D3 , bars are not fitting hight of svg

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.

Using squares to represent counts in a map

This might be a simple question, but I have a map in d3 and I'd like to represent event-counts as squares.
Here's an example png of what I'm going for:
They're not aligned perfectly in the picture, but let's say I have a JSON:
[
{city:'New York', count:3},
{city:'Washington, D.C.', count:1},
{city:'Austin', count:5},
{city:'Havana', count:8}
]
of counts that I'd like to represent as squares, preferably clustered in an orderly way.
I'm scratching my head on this — I think maybe a force-directed graph will do the trick? I've also seen this: http://bl.ocks.org/XavierGimenez/8070956 and this: http://bl.ocks.org/mbostock/4063269 that might get me close.
For context and set-up (I don't need help making the map, but just to share), here's the repo I'm using for the project: https://github.com/alex2awesome/custom-map, which shows the old way I was representing counts (by radius of a circle centered on each city).
does someone at least know what this might be called?
The technical name of this in dataviz is pictogram.
Here is a general code for plotting the rectangles, you'll have to change some parts according to your needs. The most important part is the calculation for the rectangles x and y position, using the modulo operator.
First, let's set the initial position and the size of each rectangle. You'll have to set this according to your coordinates.
var positionX = 5;
var positionY = 5;
var size = 5;
Then, let's set how many rectangles you want (this, in your code, will be d.count):
var count = 15;
var gridSize = Math.ceil(Math.sqrt(count));
var data = d3.range(count);
Based on the count, we set the gridSize (just a square root) and the data.
Now we plot the rectangles:
var rects = svg.selectAll(".rects")
.data(data)
.enter()
.append("rect");
rects.attr("width", size)
.attr("height", size)
.attr("x", function(d,i){ return positionX + (i%gridSize)*(size*1.1)})
.attr("y", function(d,i){ return positionY + (Math.floor((i/gridSize)%gridSize))*(size*1.1) })
.attr("fill", "red");
Here is a working snippet, using 15 as count (4, 9, 16, 25 etc will give you a perfect square). Change count to see how it adapts:
var svg = d3.select("body")
.append("svg")
.attr("width", 50)
.attr("height", 50);
var count = 15;
var size = 5;
var positionX = 5;
var positionY = 5;
var gridSize = Math.ceil(Math.sqrt(count));
var data = d3.range(count);
var rects = svg.selectAll(".rects")
.data(data)
.enter()
.append("rect");
rects.attr("width", size)
.attr("height", size)
.attr("x", function(d,i){ return positionX + (i%gridSize)*(size*1.2)})
.attr("y", function(d,i){ return positionY + (Math.floor((i/gridSize)%gridSize))*(size*1.2) })
.attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Adding Legend on a basic chart

I am brand new to d3.js and stackoverflow so please pardon if I ask something very basic. I have a basic donut chart however it is a modification of a normal donut chart since you can see there is one on top of another. I was wondering if it is possible to add a label right on the chart. I am able to add legend and label outside the chart but i want to be able to add a label right on the chart itself.
This is the code for chart
var dataset = {
data1: [53245, 28479, 19697, 24037, 40245],
data2: [53245, 28479, 19697, 24037, 40245]
};
var width = 460,
height = 300,
cwidth = 45;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) { return arc.innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1))(d); });
This is the FIDDLE. I would highly be pleased with any suggestions to be able to add label and legend. I would want label to be on top of both the charts in the fiddle.
Is this what you're looking for? Fiddle.
Essentially what this does is append a text element which is positioned using the angle of the path element.
var angle = d.endAngle - d.startAngle;
var loc = d.startAngle - (Math.PI/2) + (angle/2);
return Math.sin(loc) * radius;
If you want the labels on the left side to not overlap onto the chart you could use something similar to the following
var textLength = [];
gEnter.selectAll("text").each(function () {
textLength.push(this.getComputedTextLength());
});
To get the lengths of the text objects, then you can modify either .attr("x", ...) or .attr("transform", "translate(x,0)") when d.startAngle is greater than PI (meaning that it is on the left side of the chart), subtracting the text length from the x element to position it further left.

D3.js attrTween Returning Null (Unexpected Behavior)

I have a very basic D3 SVG which essentially consists of a couple arcs.
No matter what I use (attr, attrTween, and call) I cannot seem to get the datum via the first argument of the callback--it is always coming back null (I presume it's some kind of parse error, even though the path renders correctly?)
I might be overlooking something basic as I am relatively new to the library...
var el = $('#graph'),
width = 280,
height = 280,
twoPi = Math.PI * 2,
total = 0;
var arc = d3.svg.arc()
.startAngle(0)
.innerRadius(110)
.outerRadius(130),
svg = d3.select('#graph').append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
meter = svg.append('g').attr('class', 'progress');
/* Add Meter Background */
meter.append('path')
.attr('class', 'background')
.attr('d', arc.endAngle(twoPi))
.attr('transform', 'rotate(180)');
/* Add in Icon */
meter.append('text')
.attr('text-anchor', 'middle')
.attr('class', 'fa fa-user')
.attr('y',30)
.text('')
/* Create Meter Progress */
var percentage = 0.4,
foreground = meter.append('path').attr('class', 'foreground')
.attr('transform', 'rotate(180)')
.attr('d', arc.endAngle(twoPi*percentage)),
setAngle = function(transition, newAngle) {
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
/*transition.attrTween('d', function(d) { console.log(this)
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) { d.endAngle = interpolate(t); return arc(d); };
});*/
};
setTimeout(function() {
percentage = 0.8;
foreground.transition().call(setAngle, percentage*twoPi);
},2000);
It's this block of code that seems to be problematic:
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
Returning:
undefined 0 "M7.959941299845452e-15,-130A130,130 0 0,1 76.4120827980215,105.17220926874317L64.65637775217205,88.99186938124421A110,110 0 0,0 6.735334946023075e-15,-110Z"
I tried using the interpolator to parse the i value as a string since I cannot seem to acquire "d," however that had a parsing error returning a d attribute with multiple NaN.
This all seems very strange seeing as it's a simple path calculated from an arc???
The first argument of basically all callbacks in D3 (d here) is the data element that is bound to the DOM element you're operating on. In your case, no data is bound to anything and therefore d is undefined.
I've updated your jsfiddle here to animate the transition and be more like the pie chart examples. The percentage to show is bound to the path as the datum. Then all you need to do is bind new data and create the tween in the same way as for any of the pie chart examples:
meter.select("path.foreground").datum(percentage)
.transition().delay(2000).duration(750)
.attrTween('d', function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc.endAngle(twoPi*interpolate(t))();
};
});

d3.js - evenly spaced bars on a time scale

I'm building a bar plot in d3.js in which each bar represents total TB cases during a month. The data essentially consists of a date (initially strings in %Y-%m format, but parsed using d3.time.format.parse) and an integer. I'd like the axis labels to be relatively flexible (show just year boundaries, label each month, etc.), but I'd also like the bars to be evenly spaced.
I can get flexible axis labeling when I use a date scale:
var xScaleDate = d3.time.scale()
.domain(d3.extent(thisstat, function(d) { return d.date; }))
.range([0, width - margin.left - margin.right]);
... but the bars aren't evenly spaced due to varying numbers of days in each month (e.g., February and March are noticeably closer together than other months). I can get evenly-spaced bars using a linear scale:
var xScaleLinear = d3.scale.linear()
.domain([0, thisstat.length])
.range([0, width - margin.left - margin.right]);
... but I can't figure out how to then have date-based axis labels. I've tried using both scales simultaneously and only generating an axis from the xScaleDate, just to see what would happen, but the scales naturally don't align quite right.
Is there a straightforward way to achieve this that I'm missing?
You can combine ordinal and time scales:
// Use this to draw x axis
var xScaleDate = d3.time.scale()
.domain(d3.extent(thisstat, function(d) { return d.date; }))
.range([0, width - margin.left - margin.right]);
// Add an ordinal scale
var ordinalXScale = d3.scale.ordinal()
.domain(d3.map(thisstat, function(d) { return d.date; }))
.rangeBands([0, width], 0.4, 0);
// Now you can use both of them to space columns evenly:
columnGroup.enter()
.append("rect")
.attr("class", "column")
.attr("width", ordinalXScale.rangeBand())
.attr("height", function (d) {
return height - yScale(d.value);
})
.attr("x", function (d) {
return xScaleDate(d.date);
})
.attr("y", function (d){
return yScale(d.value);
});
I've created an example a while ago to demonstrate this approach: http://codepen.io/coquin/pen/BNpQoO
I had the same problem, I've ended up accepting that some months are longer than others and adjusting the column bar width so that the gap between bars remains constant. So tweaking the barPath function in the crossfilter home page demo (http://square.github.com/crossfilter/ - uses d3) I got something like this:
var colWidth = Math.floor(x.range()[1] / groups.length) - 1;//9;
if (i < n - 1) {
//If there will be column on the right, end this column one pixel to the left
var nextX = x(groups[i+1].key)
colWidth = nextX - x(d.key) - 1;
}
path.push("M", x(d.key), ",", height, "V", yVal, "h",colWidth,"V", height);
Try d3.scale.ordinal:
var y = d3.scale.ordinal()
.domain(yourDomain)
.rangeRoundBands([0, chartHeight], 0.2);
Tweek 0.2 parameter.

Categories