I am trying to write a transitioning bar graph that uses two CVS files. I know that both of the files are loading properly because it shows in the console that the first one loads with the page and the second one loads when you click the update button.
The only thing that I have really thought of trying was changing the svg select to group instead of selecting all rectangles incase there was something screwed up there.
This block is creating the svg element, bringing in the first CSV file, and appending the rectangles onto the chart. My only thought for what the problem could be is that it is inside a function, but if I take it out of the function how do I bind the data to them?
//Creating SVG Element
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3.select('#chart')
.append('svg')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand().range([chart_pad_x, chart_w -
chart_pad_x]).padding(0.2);
var y_scale = d3.scaleLinear().range([chart_pad_y, chart_h -
chart_pad_y]);
//Data-------------------------------------------------------------------
d3.csv('data.csv').then(function(data){
console.log(data);
generate(data); });
function generate(data){
//Scale domains
x_scale.domain(d3.extent(data, function(d){ return d }));
y_scale.domain([0, d3.max(data, function(d){ return d })]);
//Create Bars
svg.select('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i){
return x_scale(i);
})
.attr('y', function(d){
return y_scale(d);
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d){
return y_scale(d);
})
.attr('transform',
"translate(0,0)")
.attr('fill', '#03658C')
'''
The results I have experienced is a blank window with just the update button. As previously stated I know that the data is being generated because I can see it in the console.
Try using the following:
svg.selectAll('rect')
.data(data)
If you use svg.select this will only make the data binding with the first element found.
d3.select(selector): Selects the first element that matches the specified selector string. If no elements match the selector, returns an empty selection. If multiple elements match the selector, only the first matching element (in document order) will be selected. For example, to select the first anchor element:
This should be clear if you inspect the DOM nodes.
To fix the issue lets change some things in your code:
Lets create a dummy fetch function:
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
You are also using a scaleBand with an incomplete domain by using the extent function:
d3.extent(): Returns the minimum and maximum value in the given iterable using natural order. If the iterable contains no comparable values, returns [undefined, undefined]. An optional accessor function may be specified, which is equivalent to calling Array.from before computing the extent.
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
console.log(x_scale.domain()) // [min, max]
The scaleBand needs the whole domain to be mapped
Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
If we continue using that scale we will be only to get two values for our x scale. Lets fix that with the correct domain:
x_scale.domain(data);
Lastly use the selectAll to create the data bind:
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
This should do the trick.
Complete code:
var chart_w = 1000,
chart_h = 500,
chart_pad_x = 40,
chart_pad_y = 20;
var svg = d3
.select('#chart')
.append('svg')
.style('background', '#f9f9f9')
.style('border', '1px solid #cacaca')
.attr('width', chart_w)
.attr('height', chart_h);
//Defining Scales
var x_scale = d3.scaleBand()
.range([chart_pad_x, chart_w - chart_pad_x])
.padding(0.2);
var y_scale = d3.scaleLinear()
.range([chart_pad_y, chart_h - chart_pad_y]);
//Data-------------------------------------------------------------------
(function simulateCSVFetch() {
const data = [1,2,3,4,5];
generate(data);
})();
function generate(data) {
console.log(d3.extent(data, function(d) { return d }));
//Scale domains
x_scale.domain(d3.extent(data, function(d) { // cant use extent since we are using a scaleBand, we need to pass the whole domain
return d;
}));
// Band scales are typically used for bar charts with an ordinal or categorical dimension. The unknown value of a band scale is effectively undefined: they do not allow implicit domain construction.
x_scale.domain(data);
y_scale.domain([0, d3.max(data, function(d) {
return d
})]);
//Create Bars
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('x', function(d, i) {
return x_scale(d);
})
.attr('y', function(d) {
return chart_h - y_scale(d); // fix y positioning
})
.attr('width', x_scale.bandwidth())
.attr('height', function(d) {
return y_scale(d);
})
.attr('fill', '#03658C');
}
Working jsfiddle here
Related
I have a map already drawed. I would like to add a legend using d3.js. For example when filering by length, the map should show differents colors. Since a week, I couldn't achieve this task. My map color seem to be good but the legend does not match.
Could anybody help me with my draw link function ?
https://jsfiddle.net/aba2s/xbn9euh0/12/)
I think it's the error is about the legend function.
Here is the function that change my map color Roads.eachLayer(function (layer) {layer.setStyle({fillColor: colorscale(layer.feature.properties.length)})});
function drawLinkLegend(dataset, colorscale, min, max) {
// Show label
linkLabel.style.display = 'block'
var legendWidth = 100
legendMargin = 10
legendLength = document.getElementById('legend-links-container').offsetHeight - 2*legendMargin
legendIntervals = Object.keys(colorscale).length
legendScale = legendLength/legendIntervals
// Add legend
var legendSvg = d3.select('#legend-links-svg')
.append('g')
.attr("id", "linkLegendSvg");
var bars = legendSvg.selectAll(".bars")
//.data(d3.range(legendIntervals), function(d) { return d})
.data(dataset)
.enter().append("rect")
.attr("class", "bars")
.attr("x", 0)
.attr("y", function(d, i) { return legendMargin + legendScale * (legendIntervals - i-1); })
.attr("height", legendScale)
.attr("width", legendWidth-50)
.style("fill", function(d) { return colorscale(d) })
// create a scale and axis for the legend
var legendAxis = d3.scaleLinear()
.domain([min, max])
.range([legendLength, 0]);
legendSvg.append("g")
.attr("class", "legend axis")
.attr("transform", "translate(" + (legendWidth - 50) + ", " + legendMargin + ")")
.call(d3.axisRight().scale(legendAxis).ticks(10))
}
D3 expects your data array to represent the elements you are creating. It appears you are passing an array of all your features: but you want your scale to represent intervals. It looks like you have attempted this approach, but you haven't quite got it.
We want to access the minimum and maximum values that will be provided to the scale. To do so we can use scale.domain() which returns an array containing the extent of the domain, the min and max values.
We can then create a dataset that contains values between (and including) these two endpoints.
Lastly, we can calculate their required height based on how high the visual scale is supposed to be by dividing the height of the visual scale by the number of values/intervals.
Then we can supply this information to the enter/update/exit cycle. The enter/update/exit cycle expects one item in the data array for every element in the selection - hence why need to create a new dataset.
Something like the following shold work:
var dif = colorscale.domain()[1] - colorscale.domain()[0];
var intervals = d3.range(20).map(function(d,i) {
return dif * i / 20 + colorscale.domain()[0]
})
intervals.push(colorscale.domain()[1]);
var intervalHeight = legendLength / intervals.length;
var bars = legendSvg.selectAll(".bars")
.data(intervals)
.enter().append("rect")
.attr("class", "bars")
.attr("x", 0)
.attr("y", function(d, i) { return Math.round((intervals.length - 1 - i) * intervalHeight) + legendMargin; })
.attr("height", intervalHeight)
.attr("width", legendWidth-50)
.style("fill", function(d, i) { return colorscale(d) })
In troubleshooting your existing code, you can see you have too many elements in the DOM when representing the scale. Also, Object.keys(colorscale).length won't produce information useful for generating intervals - the keys of the scale are not dependent on the data.
eg
I'm working on a bar chart that updates its data based on the mouseover of another element. When the chart updates, if there are less bars in the new chart, the chart permanently has fewer bars and changing the data back does not add them back in. I've added a gif to show this - when it gets down to 3 bars, they never come back.
Here's my code:
var scatter_versus_dataset; // the main set
var scatter_versus_dataset_filtered;
// set versus y scale
scatter_versus_y = d3.scaleBand().range([0, SCATTER_VERSUS_HEIGHT])
// set versus x scale
scatter_versus_x_fatal = d3.scaleLinear().range([0, SCATTER_VERSUS_WIDTH / 3]);
scatter_versus_x_nonfatal = d3.scaleLinear().range([-1 * SCATTER_VERSUS_WIDTH / 3, 0 ])
// set the versus colors
scatter_versus_z = d3.scaleOrdinal().range(STACK_COLOURS);
...
function updateScatterVersus(code){
// filter the set
scatter_versus_dataset_filtered = scatter_versus_dataset.filter(function (d) { return (d.majorOccCodeGroup == code) })
scatter_versus_y.domain(scatter_versus_dataset_filtered.map(function (d) { return d.occupation; })).padding(BAR_PADDING);
scatter_versus_x_fatal.domain([0, d3.max(scatter_versus_dataset_filtered, function (d) { return d.f_total_rate; })]).nice();
scatter_versus_x_nonfatal.domain([d3.min(scatter_versus_dataset_filtered, function (d) { return +-1 * d.nf_total_rate; }), 0]).nice();
var bars = d3.selectAll("#scatter_versus_fatal_rect")
.data(scatter_versus_dataset_filtered)
bars.exit()
.remove()
bars.transition()
.duration(600)
.attr("y", function (d) {
return scatter_versus_y(d.occupation);
})
.attr("x", function (d) {
return scatter_versus_x_fatal(0) + SCATTER_VERSUS_GAP_HALF;
})
.attr("width", function (d) {
return scatter_versus_x_fatal(d.f_total_rate);
})
.attr("height", scatter_versus_y.bandwidth())
bars.enter()
.append("rect")
.attr('id', 'scatter_versus_fatal_rect')
.classed("bar", true)
.attr("y", function (d) {
return scatter_versus_y(d.occupation);
})
.attr("x", function (d) {
return scatter_versus_x_fatal(0) + SCATTER_VERSUS_GAP_HALF;
})
.attr("width", function (d) {
return scatter_versus_x_fatal(d.f_total_rate);
})
.attr("height", scatter_versus_y.bandwidth())
}
The process for redrawing the other side of the chart is exactly the same. The problem is still there if i only draw one of the sides.
The data is just from a csv, and I don't think it's the problem - the filtered set has the right number of entries and it's fine in other charts. It's probably something to do with the removal and redrawing but I can't find many examples of this. Or perhaps a key? I can upload some data if needed but it's a pretty big CSV.
id in HTML is unique, only 1 tag should have it.
Select the div for the bars, then selectAll tags with class is bar and bind data.
Remove the id you add to the rects.
var bars = d3.select("#scatter_versus_fatal_rect")
.selectAll(".bar")
.data(scatter_versus_dataset_filtered);
bars.enter()
.append("rect")
// .attr('id', 'scatter_versus_fatal_rect')
.classed("bar", true)
......
I tried to set numbers as translate attributes of groups.
But, to set the numbers, I need to access data.
I found it's impossible to access data with function(d){}.
How to access data in .attr()?
var xCol = 'month'
var wraps = g.selectAll('.wrap').data(data);
wraps.enter().append('g')
.attr('class', 'wrap')
.attr('transform', 'translate('+function(d){return xScale(d[xCol])}()+', '+ (-margin.top)+')')
>>>index.js:132 Uncaught TypeError: Cannot read property 'month' of undefined
And I want to make several groups .wrap and draw bars in each groups
But, I have no idea to forward data to child elements.
var bars = wraps.selectAll('.bar').data(function(d){return d});
bars.enter().append('rect')
.attr('class', 'bar')
.attr('x', function(d){return xScale(d.d[xCol])})
...
I think you may just have the idea right but missing one small thing. The second argument to the attr function should be another function. Like below.
var svg = d3.select('svg');
var dataSet = [10, 20, 30, 40];
var circle = svg.selectAll('circle')
.data(dataSet)
.enter()
.append('circle')
.attr('r', function(d) {return d;})
.attr('cx', function(d,i) {return i * 100 + 50;})
.attr('cy', 50)
.attr('fill', 'red')
So the function then returns the result. This fiddle shows it in action
http://jsfiddle.net/bdkxgph5/1/
So in your case replace you attr call with
.attr('transform', function(d) { return 'translate('+xScale(d[xCol])+', '+ (-margin.top)+')'})
I'm learning to build D3 graph in a rails application. but somehow i can't load the graph.
So basically, what i did is download the d3.zip from d3 github. put d3.js and d3.min.js in assets/javascript/ and require them both in the application.js.
Here is the d3 code in my view,
<% content_for(:inline_javascript) do %>
<script>
var w = 200,
h = 200,
p = 10;
var data = [{count:100,year:1999},
{count:240,year:2010},
{count:290,year:2009}];
var bar_height = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d.count; }) ) // min max of count
.range([p,h-p]); // min max of area to plot in
var bar_xpos = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d.year; }) ) // min max of year
.range([p,w-p]); // min max of area to plot in
var svg = d3.select("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(d) {
return bar_xpos(d.year); })
.attr("y", function(d) {
return h - bar_height(d.count); })
.attr("width", 10)
.attr("height", function(d) {return bar_height(d.count); })
.attr("fill", "steelblue")
</script>
<%end%>
but the page show nothing. and there's no errors when i inspect elements on the browser. So i wonder if anyone can tell me what is wrong with what i did?
Thanks!
Edit:
so finally i found out in rails, i need to have a placeholder for the d3 graph, and so it needs a id to identify the graph. The idea is like that. but i only need to add line,
<svg class="chart"></svg>
The graph shows on the page!
D3's approach is somewhat similar to jQuery. It will not do anything unless you explicitly ask it to.
To do any visualization first it is required to select the node(s) to operate on.
D3 provides select and selectAll methods for this.
And If no elements in the current document match the specified selector, they return the empty selection, hence no errors will be shown.
I'm new to D3, and spent already a few hours to find out anything about dealing with structured data, but without positive result.
I want to create a bar chart using data structure below.
Bars are drawn (horizontally), but only for user "jim".
var data = [{"user":"jim","scores":[40,20,30,24,18,40]},
{"user":"ray","scores":[24,20,30,41,12,34]}];
var chart = d3.select("div#charts").append("svg")
.data(data)
.attr("class","chart")
.attr("width",800)
.attr("height",350);
chart.selectAll("rect")
.data(function(d){return d3.values(d.scores);})
.enter().append("rect")
.attr("y", function(d,i){return i * 20;})
.attr("width",function(d){return d;})
.attr("height", 20);
Could anyone point what I did wrong?
When you join data to a selection via selection.data, the number of elements in your data array should match the number of elements in the selection. Your data array has two elements (for Jim and Ray), but the selection you are binding it to only has one SVG element. Are you trying to create multiple SVG elements, or put the score rects for both Jim and Ray in the same SVG element?
If you want to bind both data elements to the singular SVG element, you can wrap the data in another array:
var chart = d3.select("#charts").append("svg")
.data([data])
.attr("class", "chart")
…
Alternatively, use selection.datum, which binds data directly without computing a join:
var chart = d3.select("#charts").append("svg")
.datum(data)
.attr("class", "chart")
…
If you want to create multiple SVG elements for each person, then you'll need a data-join:
var chart = d3.select("#charts").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "chart")
…
A second problem is that you shouldn't use d3.values with an array; that function is for extracting the values of an object. Assuming you wanted one SVG element per person (so, two in this example), then the data for the rect is simply that person's associated scores:
var rect = chart.selectAll("rect")
.data(function(d) { return d.scores; })
.enter().append("rect")
…
If you haven't already, I recommend reading these tutorials:
Thinking with Joins
Nested Selections
This may clarify the nested aspect, in addition to mbostock's fine answer.
Your data has 2 degrees of nesting. You have an array of 2 objects, each has an array of ints. If you want your final image to reflect these differences, you need to do a join for each.
Here's one solution: Each user is represented by a group g element, with each score represented by a rect. You can do this a couple of ways: Either use datum on the svg, then an identity function on each g, or you can directly join the data on the g. Using data on the g is more typical, but here are both ways:
Using datum on the svg:
var chart = d3.select('body').append('svg')
.datum(data) // <---- datum
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(function(d){ return d; }) // <----- identity function
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Using data on the group (g) element:
var chart = d3.select('body').append('svg')
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(data) // <--- attach directly to the g
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Again, you don't have to create these g elements, but by doing so I can now represent the user scores differently (they have different y from the transform) and I can also give them different styles, like this:
.jim {
fill: red;
}
.ray {
fill: blue;
}