I'm trying to do a simple plot with d3. It works fine when I plot only one line at time but I would like to make my code more generic.
My data are in this format (note that the 2 set of data may not contain the same amount of point and the time is not perfectly synchronize between my 2 sets of measure):
var data = [{key: "kmm03", value:[{"time":1364108443000,"mesure":"1.6299999952316284"},{"time":1364108503000,"mesure":"1.100000023841858"},{"time":1364108563000,"mesure":"1.159999966621399"}},
{key: "kmm04", value:[{"time":1364108416000,"mesure":"2.690000057220459"},{"time":1364108476000,"mesure":"3.319999933242798"},{"time":1364108536000,"mesure":"3.140000104904175"},{"time":1364108596000,"mesure":"2.9800000190734863"}}]
Now I try to plot it like this but I cannot get the svg lines to dispay:
var margin = {top: 20, right: 40, bottom: 20, left: 40};
var width = 780 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
var time_scale = d3.time.scale()
//.domain(time_extent)
.domain([1364108443000, 1364112559000])
.range([0, width]);
var mesure_scale = d3.scale.linear()
.domain([0,10])
.range([height, 0]);
var vis = d3.select("#box1")
.append("svg:svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var time_axis = d3.svg.axis()
.scale(time_scale)
.orient("bottom");
var mesure_axis = d3.svg.axis()
.scale(mesure_scale)
.orient("left");
var set_axis_t = vis.append("svg:g")
.attr("transform", "translate(0, " + height +")")
.call(time_axis);
var set_axis_m = vis.append("svg:g")
.attr("transform", "translate(0, 0)")
.call(mesure_axis);
var line = d3.svg.line()
.x(function(d){return time_scale(d.time);})
.y(function(d){return mesure_scale(d.mesure);});
var group = vis.selectAll('path')
.data(data)
.enter().append('path')
.attr("d", line);
d3 selection.data() takes an array of elements. In your code the data var is an array of two objects. When you call line on d, d3 looks for time and measure properties in each element. These don't exist so no paths are appended.
The amount and time values you want to render are nested one layer down in the value property of each object.
To draw these, change the .attr('d', line) to .attr("d", function(d) {return line(d.value);});
Here is a working version. To make it work I made a few other changes:
1- closing brackets of the value arrays were missing
2- .selectAll('path') was not working for the data because it was conflicting with the path elements in the axes. To address this, I assigned the data paths the class visline and use .selectAll('.visline') to access them
Related
I'm learning to make charts in d3 from scratch without taking someone else code and modifying it. I can successfully create a x & y axis vertical bar chart. But when it comes to transform the same chart to horizontal bar chart I end up in a mess. Here is my code so far:
var data = [{
name: "China",
value: 1330141295
}, {
name: "India",
value: 1173108018
}, {
name: "Indonesia",
value: 242968342
}, {
name: "Russia",
value: 139390205
}];
//set margins
var margin = {
top: 30,
right: 30,
bottom: 30,
left: 40
};
var width = 960 - margin.left - margin.right;
var height = 600 - margin.top - margin.bottom;
//set scales & ranges
var yScale = d3.scaleBand().range([0, height]).padding(0.1)
var xScale = d3.scaleLinear().range([0, width])
//draw the svg
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 * 2 + "," + margin.top + ")")
//load the data
data.forEach(function(d) {
d.population = +d.population;
});
//set domains
xScale.domain([0, d3.max(data, function(d) {
return d.population
})])
yScale.domain(data.map(d => d.name))
//add x & y Axis
svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(d3.axisBottom(xScale));
svg.append("g")
.call(d3.axisLeft(yScale))
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", d => yScale(d.name))
.attr("height", d => yScale(d.name))
.attr("x", d => width - xScale(d.population))
.attr("width", yScale.bandwidth())
Thank you very much.
You need to change a lot of things in your code.
TL;DR
change value to population in the array
scales are used to convert values to proportional pixel values
height is the vertical size of an element. you should use yScale(d.name)
width is the horizontal size of an element. you should use xScale(d.population)
y is the vertical position of an element. you should use yScale.bandwidth()
x is the vertical position of an element. you should use 0
use selectAll("rect") on a new appended g or the svg element not the same g element that has the axises on it
add fill attribute so that your rects have color
You have the population field labelled value but you're calling population through out the code to use it. So replace value with population in your data objects.
Next you need to change the way you're setting up the rects. use selectAll on the svg element directly or append another g to the svg element and add the rects on that element. Right now your code attempts to add the rects to the same g element as the x axis.
Make sure you are setting the attributes of the rects correctly. Height is the size in pixels of the rect element not the position. y is the position of the rects vertically from the top downwards. this means the height attribute should use yScale(d.name) and width should use xScale(d.population) because they are the width and length of the rectangles, or rect elements. x and y are the coordinate positions of the element from the top left corner of the svg element. Use them to determine the starting position of the top left pixel of your rects. You should have y as yScale.bandwidth() and x as 0.
I took bits from this and this solutions.
I then do the following to show my x axis label ticks:
var x = d3.scaleBand().range([0, width]).padding(.1).domain(d3.range(1993, 2002));
svg.append("g")
.attr('class', 'axis')
.attr('transform', 'translate(-10,' + (height - 20) + ')')
.call(d3.axisBottom(x));
However, I then get the following output where x axis ticks are shifted further to the right each time like if their gap doesn't match the bar's.
The following snippet shows how the rects and axis's ticks line up correctly.
Need to be careful on how d3.range generates arrays, in that d3.range(1993, 2002) will create [1993, 1994, ... , 2001].
Also, I'm not sure why the g element that contains the X axis was shifted left, so that is set to 0
var width = 800
var height = 100
//this makes an array 1993 to 2001. Is that what you want?
var data = d3.range(1993, 2002)
var x = d3.scaleBand()
.range([0, width])
.padding(.1)
.domain(data);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
svg.append("g")
.attr('class', 'axis')
//.attr('transform', 'translate(-10,' + (height - 20) + ')')
.attr('transform', 'translate(0,' + (height - 20) + ')')
.call(d3.axisBottom(x));
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', d => x(d))
.attr('y', 0)
.attr('width', x.bandwidth())
.attr('height', x.bandwidth())
<script src="https://d3js.org/d3.v4.min.js"></script>
I don't have enough S.O to 'comment' but I think it may be helpful to use the xScale:
-in the xAxis (something like...
xAxis = d3.axisBottom()
.scale(xScale)
)
-in the x position of each bar ( something like...
'x', d => xScale(year)
)
-in the width of each bar, seems like you might need to use something like...
('width', x.bandwidth())
looks like the xScale in the (1)axis and (2)bars are not working as expected together.
Maybe this helps?!
I'm new to using d3js to build graphs. My current task is charting no of logs across time. So my Y axis is logs and x axis is time. For some data, the graph comes fine within the grid line, but for some where the data is huge, it flows out of the grids like below. Also even if I am charting 3 or 4 days in the past, I only see 2 days on the axis. Below also the code that I use for setting height. I'm using time scale on x axis and linear scale on y axis.
var margin = {top: 20, right: 80, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var svg = d3.select(".content").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 x = d3.time.scale()
.range([0, width]);
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");
How can I set the height and width to be according to the data?
The following is my draw axis code:
var seasons = ["summer", "winter", "fall", "spring"];
var margin = {top:80, right:30, bottom:30, left:30},
width = 1200 - margin.right - margin.left,
height = 800 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.domain(seasons)
.rangeRoundBands([0, width], 0.9);
xAxis = d3.svg.axis()
.scale(x)
.tickSize(4, 6)
.tickPadding(6)
.orient("bottom");
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);
</script>
However, the tickPadding function does now introduce a space between the ordinal axis categories.
More specifically, I want that each of the summer, winter, fall and spring parts of the axis are separate from each other, sort of like dashed line. How can I get this?
I don't know of any way built into the d3 axis to accomplish this, but you can remove the path it draws and replace it with a dashed line, like so:
// Draw the axis, as you currently are
var axisElem = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Remove the line
axisElem.selectAll("path.domain").remove();
// Figure out how big each dash should be
var gapFraction = 0.1; // The portion of the line that should be a gap
var total = x(seasons[1]) - x(seasons[0]);
var dash = total * (1 - gapFraction);
var gap = total * gapFraction;
// Draw the dashed line
axisElem.append("line")
.classed("domain", true)
.attr("x1", x(seasons[0]) - dash / 2 + gap / 2)
.attr("x2", x(seasons[seasons.length - 1]) + dash / 2 - gap / 2)
.attr("y1", 0)
.attr("y2", 0)
.attr("stroke", "black")
.attr("stroke-dasharray", dash + "," + gap);
I'm trying to start from Mike Bostock's histogram example:
http://bl.ocks.org/mbostock/3048450
Initially I'm just trying to change the data and domain to get an understanding of how it works and get closer to what I need. But in doing that, my script throws an error due to negative widths on the rects.
What is this line doing exactly and why does it generate a negative value?
.attr("width", x(data[0].dx) - 1)
My fiddle is here: http://jsfiddle.net/rolfsf/p96dH/1/
and my script currently is this:
//generate some data with a median of 75
var values = d3.range(1000).map(d3.random.logNormal(Math.log(75), 0.4));
// A formatter for counts.
var formatCount = d3.format(",.0f");
var margin = {top: 10, right: 30, bottom: 30, left: 30},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([60, 95])
.range([0, width]);
// Generate a histogram using twenty uniformly-spaced bins.
var data = d3.layout.histogram()
.bins(x.ticks(7))
(values);
var y = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d.y; })])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
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 bar = svg.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });
bar.append("rect")
.attr("x", 1)
.attr("width", x(data[0].dx) - 1)
.attr("height", function(d) { return height - y(d.y); });
bar.append("text")
.attr("dy", ".75em")
.attr("y", 6)
.attr("x", x(data[0].dx) / 2)
.attr("text-anchor", "middle")
.text(function(d) { return formatCount(d.y); });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
UPDATE: The answer in this question: D3 Histogram with negative values actually gives me the chart layout I wanted, though Lars is correct that my domain function is causing problems.
an updated fiddle is here (note that due to the domain issue, the first and last bars have incorrect heights) http://jsfiddle.net/rolfsf/p96dH/3/
I replaced my width function with the barWidth variable:
var numbins = data.length;
var barWidth = width/numbins - 1;
It's simply the way you've set up your x scale. You're assuming that all values are going to be in the interval (60,95), which they are not. For values smaller than 60, you get negative numbers.
You can fix this easily by getting the actual extent of the data for the domain:
var x = d3.scale.linear()
.domain(d3.extent(data))
.range([0, width]);
You're right, it is due to the domain; the reason it works in Mike's example is that the domain minimum he uses is 0.
A better approach would be to do the following, replace every occurrence of
x(data[0].dx) - 1
with
x(60 + data[0].dx) - 1
More, generally, you can define your x scale with:
var x = d3.scale.linear()
.domain(d3.extent(data))
.range([0, width]);
And then the above snippets (setting bar width), become:
x(d3.min(datasetBars) + data[0].dx) - 1)