I try to make a bar chart on the basis of 2D data array (I din`t want to use 2D array initially, so there is a function "mergingAr", which merges them) using d3.js. Here is the code:
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
var arr1 = [399200,100000, 352108, 600150, 39000, 17005, 4278];
var arr2 = [839, 149, 146, 200, 200, 121, 63];
function mergingAr (array1, array2)
{
var i, out = [];//literal new array
for(i=0;i<array1.length;i++)
{
out.push([array1[i],array2[i]]);
}
return out;
}
var data = mergingAr(arr1, arr2);
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, d3.max(data, function(d) {
return d[0]; })])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[1]; })])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d[0]); })
//.attr("width", x.rangeBand())
.attr("width", width/a1.length)
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return height - y(d[1]); });
Te problem is - the bars cover each other, there are no distance between them, even if I used rangeRoundBands.
There are 2 issues in your code.
The first one is that the data array is not sorted. In order to sort it you can do:
out = out.sort(function(a,b) { return d3.ascending(a[0],b[0]) })
before returning out in your mergeAt function. Sorting the array makes sure that you process bars in the right order.
The second issue is that your intervals are not equal. To remediate to this, I made the width of a block equal to the distance to the next one (but you might want to do something different):
.attr("width", function(d,i){
if(i!=(data.length-1)) {
return x(data[i+1][0])-x(data[i][0])
} else {
return 10; // the last block is of width 10. a cleaner way is to add a
// marker at the end of the array to know where to finish
// the axis
}
})
JsFiddle: http://jsfiddle.net/chrisJamesC/6WJPA/
Edit
In order to have the same interval between each bar and the same width, you have to change the scale to an ordinal one:
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1)
.domain(data.map(function(d){return d[0]}))
Then, you need to change the way you compute the width to:
.attr("width", x.rangeBand())
JsFiddle: http://jsfiddle.net/chrisJamesC/6WJPA/2/
Related
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.
I have a simple d3 area chart, with two areas plotted using the following data:
var data = [
[{'year':0,'amount':2},{'year':1,'amount':3},{'year':2,'amount':9},{'year':3,'amount':5},{'year':4,'amount':6},{'year':5,'amount':7},{'year':6,'amount':8},{'year':7,'amount':9},{'year':8,'amount':10},{'year':9,'amount':11},{'year':10,'amount':12}],
[{'year':0,'amount':1},{'year':1,'amount':2},{'year':2,'amount':8},{'year':3,'amount':4},{'year':4,'amount':5},{'year':5,'amount':6},{'year':6,'amount':7},{'year':7,'amount':8},{'year':8,'amount':9},{'year':9,'amount':10},{'year':10,'amount':11}]
];
The two separate arrays of objects allow me to plot two areas on one chart using the code below:
var colors = [
'steelblue',
'lightblue',
];
var margin = {top: 20, right: 30, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain(d3.extent(data.map(function(d,i) { console.log(d); return d[i].year; })))
.range([0, width]);
var y = d3.scale.linear()
.domain([-1, 16])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(-height)
.tickPadding(10)
.tickSubdivide(true)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.tickPadding(10)
.tickSize(-width)
.tickSubdivide(true)
.orient("left");
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("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "y axis")
.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("y", (-margin.left) + 10)
.attr("x", -height/2)
.text('Axis Label');
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
//************************************************************
// Create D3 line object and draw data on our SVG object
//************************************************************
var line = d3.svg.area()
.interpolate("cardinal")
.x(function(d) { return x(d.year); })
.y0(height)
.y1(function(d) { return y(d.amount); });
svg.selectAll('.line')
.data(data)
.enter()
.append("path")
.attr("class", "area")
.attr('fill', function(d,i){
return colors[i%colors.length];
})
.attr("d", line);
Thing is I need to set the domains based on the data. I've tried doing:
.domain(d3.extent(data.map(function(d) { return d.amount; })))
...when creating my linear scale but obviously this doesn't work as the array map in the call above just maps out the nested arrays instead of the objects inside.
How do I set the domain using data in this format? Or is there a better way to structure my data whilst still allowing for multiple areas to be drawn?
To get the overall extent of all the amount values contained in both arrays you need to somehow merge these arrays into one. There are several ways this could be done:
d3.merge() to merge both arrays into one:
var allValues = d3.merge(data);
The main advantage of this approach over the following ones is the fact, that this will work with any number of nested arrays in data without any changes to the code.
Built-in method Array.prototype.concat():
var allValues = data[0].concat(data[1])
If you want to show off and don't need to be compatible with older version of JavaScript, you can apply the spread operator new to ES6:
var allValues = [...data[0], ...data[1]];
Having this flattened array containing all values you can pass it to d3.extent() to calculate the overall extent.
var extent = d3.extent(allValues, function(d) { return d.amount; });
var data = [
[{'year':0,'amount':2},{'year':1,'amount':3},{'year':2,'amount':9},{'year':3,'amount':5},{'year':4,'amount':6},{'year':5,'amount':7},{'year':6,'amount':8},{'year':7,'amount':9},{'year':8,'amount':10},{'year':9,'amount':11},{'year':10,'amount':12}],
[{'year':0,'amount':1},{'year':1,'amount':2},{'year':2,'amount':8},{'year':3,'amount':4},{'year':4,'amount':5},{'year':5,'amount':6},{'year':6,'amount':7},{'year':7,'amount':8},{'year':8,'amount':9},{'year':9,'amount':10},{'year':10,'amount':11}]
];
console.log(d3.extent(d3.merge(data), function(d) { return d.amount; })); // using d3.merge()
console.log(d3.extent(data[0].concat(data[1]), function(d) { return d.amount; })); // using Array.prototype.concat()
// This will only work in compatible browsers which support the new ES6 spread operator
console.log(d3.extent([...data[0], ...data[1]], function(d) { return d.amount; })); // using ES6 spread operator
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have developed bar graph using d3.js. The developed bar graph is included in fiddle. I am new to d3.js .So I am in difficulty for formatting graph. I desire to format graph more than the graph shown below.
The main problem I have experienced is ,the graph do not show -ve integer next to the -ve to be plotted in the y axis. ex) The value plotted is -490 , my current graph do not show -500 in y -axis. This issue is also exiting +ve values
The code is given below
var mainApp = angular.module('mainApp', []);
mainApp.controller('FIItradingController',
['$scope', function($scope) {
var data = [
{name: "01-12-2014", value: 4984.6},
{name: "02-12-2014", value: -109.45},
{name: "03-12-2014", value: 474},
{name: "04-12-2014", value: 391.07},
{name: "05-12-2014", value: 106.82},
{name: "06-12-2014", value: -12.36},
{name: "07-12-2014", value: 10},
{name: "08-12-2014", value: 20}
];
var data1 = [4, 8, 15, 16, 23, 42];
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
/*var x = d3.scale.linear()
.range([0, width]);*/
/*var chart = d3.select(".chart")
.attr("width", width);*/
/*var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, width]);*/
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
$scope.render = function(data) {
/*var chart = d3.select(".chart")
.attr("width", width)
.attr("height", barHeight * data.length);*/
/*bar.append("rect")
.attr("width", x)
.attr("height", barHeight - 1);
bar.append("rect")
.attr("width", function(d) { return x(d.value); })
.attr("height", barHeight - 1);
bar.append("text")
.attr("x", function(d) { return x(d.value) - 3; })
.attr("y", barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.value; });
x.domain([0, d3.max(data, function(d) { return d.value; })]);
chart.attr("height", barHeight * data.length);
*/
x.domain(data.map(function(d) { return d.name; }));
//y.domain([0, d3.max(data, function(d) { return d.value; })]);
y.domain([d3.min(data,function(d){return d.value}), d3.max(data, function(d) { return d.value; })]);
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height) + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
/*chart.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", x.rangeBand());*/
chart.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", function(d) { return d.value < 0 ? "bar negative" : "bar positive"; })
.attr("y", function(d) {return y(Math.max(0, d.value)); })
.attr("x", function(d) { return x(d.name); })
.attr("height", function(d) {return Math.abs(y(d.value) - y(0)); })
.attr("width", x.rangeBand());
}
function type(d) {
d.value = +d.value; // coerce to number
return d;
}
$scope.render(data);
}]);
If any one know please help me. The fiddle is given below
http://jsfiddle.net/HB7LU/8960/
So, your plot is showing the negative value—it's just that it looks like it drops off below the chart area because the lowest point on the y-axis is where your chart ends.
There are a several ways you get around this (like multiplying the output of d3.min() by 1.1 to give a little extra room), but probably the easiest and most elegant is just to add .nice() to your y-scale, like so:
y.domain([d3.min(data,function(d){return d.value}), d3.max(data, function(d) { return d.value; })])
.nice();
You might also consider using d3.extent() instead of d3.min() and d3.max(). It returns a two-value array of the minimum and maximum values in an array. And I'd also put chain .domain() and .nice() onto y right after its definition; nothing necessitates it being declared 40 lines later. Now we have this:
var y = d3.scale.linear()
.range([height, 0])
.domain(d3.extent(data, function(d) { return d.value; }))
.nice();
Forked fiddle.
I am trying to use the margin conventions described in http://bl.ocks.org/mbostock/3019563
when plotting bar charts. However, the bars do not align with the x-axis as you can see
in this basic example: http://bl.ocks.org/kyrre/bbd29f225173825797e3. What am I doing wrong?
var data = [
{x: "Differential Geometry", y: 10},
{x: "Statistical Physics", y: 5},
{x: "Music", y: 3}
]
var margin = {top: 20, right: 10, bottom: 20, left: 50};
var width = 500 - margin.left - margin.right;
var height = 320 - margin.top - margin.bottom;
var y = d3.scale.linear()
.domain([0, d3.max(data, function(x) {
return x.y;
})])
.range([0, height]);
var x = d3.scale.ordinal()
.domain(_.map(data, function(d) { return d.x;}))
.rangeRoundBands([0, width], 0.10);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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 + ")");
var rect = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", function(d) {
return x(d.x);
})
.attr("y", 0)
.attr("height", function(d) {
return y(d.y);
})
.attr("fill", function(d) { return "blue";})
.attr("width", 20);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(yAxis);
The y coordinates are counted from the top (i.e. 0 is at the top of the image). It should work if you set y to the total minus height.
.attr("y", function(d) { return (height - y(d.y)); })
Given this simple chart I created:
var data = [["2013-01-24 06:38:02.235191", 52], ["2013-01-23 06:38:02.235310", 54], ["2013-01-22 06:38:02.235330", 45], ["2013-01-21 06:38:02.235346", 53]],
maxValue = d3.max(data, function (d) { return d[1]; }),
margin = {top: 20, right: 20, bottom: 50, left: 50},
width = 500 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom,
svg, x, y, xAxis, yAxis, line;
$.each(data, function(index, val) {
val[0] = new Date(val[0]);
});
x = d3.time.scale()
.range([0, width])
y = d3.scale.linear()
.domain([0, maxValue])
.range([height, 0]);
xAxis = d3.svg.axis()
.scale(x)
.tickSize(4, 2, 0)
.ticks(d3.time.days, 1)
.tickFormat(d3.time.format("%m/%d"))
.orient("bottom");
yAxis = d3.svg.axis()
.scale(y)
// .ticks(5)
// .tickValues([0, maxValue * 0.25, maxValue * 0.5, maxValue * 0.75, maxValue])
.tickSize(4, 2, 0)
.orient("left");
line = d3.svg.line()
.x(function(d) { return x(d[0]); })
.y(function(d) { return y(d[1]); });
svg = d3.select("#chart-holder").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 + ")");
x.domain(d3.extent(data, function(d) { return d[0]; }));
y.domain(d3.extent(data, function(d) { return d[1]; }));
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.attr("transform", function(d) {
return "rotate(-60)translate(" + -this.getBBox().height * 1.7 + "," +
-this.getBBox().width/4 + ")";
});
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
svg
.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("fill", "#0b8da0")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d[0]); })
.attr("cy", function(d) { return y(d[1]); });
How can I add a space between axis and line, so it won't touch the axis.
Also is there a way to force yAxis ticks to always start from 0, no matter what is the smallest value in the data set?
Working example can be found here: http://jsfiddle.net/n7Vmr/
You can just change the domain of the scale used to draw the y-axis, look for the line
y.domain(d3.extent(data, function(d) { return d[1]; }));
needs to be changed, if you want it one less than the smallest value in your dataset use
y.domain([d3.min(data, function (d) { return d[1]; })-1, maxValue]);
or if you want it to start from 0, regardless of the data
y.domain([0, maxValue]);