Related
I'm trying to get data labels to show inside of each bar of my stacked bar chart. When I view source, I can see the <text> elements in each bar with the correct number, but they aren't visible in the bar itself
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<div id="legend"></div>
<style>
</style>
<script>
// set the dimensions and margins of the graph
var margin = { top: 10, right: 30, bottom: 20, left: 50 },
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.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 + ")");
// Parse the Data
d3.csv("https://raw.githubusercontent.com/JakeRatliff/dh-valve-data/main/Valves%20Data%20-%20Sheet1.csv", function(data) {
// List of subgroups = header of the csv files = soil condition here
var subgroups = data.columns.slice(1)
subgroups.pop()
console.log(subgroups)
// List of groups = species here = value of the first column called group -> I show them on the X axis
var groups = d3.map(data, function(d) { return (d.Year) }).keys()
console.log(groups)
// Add X axis
var x = d3.scaleBand()
.domain(groups)
.range([0, width])
.padding([0.2])
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSizeOuter(0));
// Add Y axis
var y = d3.scaleLinear()
//.domain([0, 60])
.domain([0, d3.max(data, function(d) { return +d.Total; })])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
// color palette = one color per subgroup
var color = d3.scaleOrdinal()
.domain(subgroups)
.range(['#00539B', '#E0750B'])
//stack the data? --> stack per subgroup
var stackedData = d3.stack()
.keys(subgroups)
(data)
// Show the bars
svg.append("g")
.selectAll("g")
// Enter in the stack data = loop key per key = group per group
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) { return color(d.key); })
.selectAll("rect")
// enter a second time = loop subgroup per subgroup to add all rectangles
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) { return x(d.data.Year); })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); })
.attr("width", x.bandwidth())
.attr("class", "bar")
.append("text")
.attr("x", function(d) { return x(d.data.Year); })
.attr("y", function(d) { return y(d[1]); })
.text(function(d) { return d[1] })
})
</script>
Here is a codepen if that is preferred:
https://codepen.io/jake2134/pen/QWMQJOB
Thanks in advance for any help. I've Google around but the results seem to be outdated.
Use a variable to create a group, then append twice to it.
var bar_groups = svg.append("g")
.selectAll("g")
// Enter in the stack data = loop key per key = group per group
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) { return color(d.key); })
var bars = bar_groups.selectAll("g")
// enter a second time = loop subgroup per subgroup to add all rectangles
.data(function(d) { return d; })
.enter().append("g")
bars.append('rect')
.attr("x", function(d) { return x(d.data.Year); })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); })
.attr("width", x.bandwidth())
.attr("class", "bar")
bars.append("text")
.attr("x", function(d) { return x(d.data.Year); })
.attr("y", function(d) { return y(d[1]); })
.text(function(d) { return d[1] })
I am trying to add circles to the data points on the following line graph example: https://bl.ocks.org/ProQuestionAsker/8382f70af7f4a7355827c6dc4ee8817d
To generate the circles I have used the following:
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 3)
.attr("color", "pink")
.attr("cx", function(d) { return x(d.Month); })
.attr("cy", function(d) { return y(+d.Sales); });
However, as seen here, all the circles for each every fruit. I would like only the circles for selected fruits to appear, as per the lines.
Many thanks
James
You see the circles for each and every fruit because you're not filtering the data based on the dropdown selection.
Here's a snippet doing that data filtering and appending the dots:
var dataAsCsv = `Month,Sales,Fruit,Year
Jan,87,strawberry,2016
Feb,3,strawberry,2016
Mar,89,strawberry,2016
Apr,56,strawberry,2016
May,1,strawberry,2016
Jun,17,strawberry,2016
Jul,59,strawberry,2016
Aug,43,strawberry,2016
Sep,16,strawberry,2016
Oct,94,strawberry,2016
Nov,99,strawberry,2016
Dec,53,strawberry,2016
Jan,93,grape,2016
Feb,8,grape,2016
Mar,95,grape,2016
Apr,62,grape,2016
May,5,grape,2016
Jun,24,grape,2016
Jul,62,grape,2016
Aug,49,grape,2016
Sep,18,grape,2016
Oct,101,grape,2016
Nov,103,grape,2016
Dec,53,grape,2016
Jan,94,blueberry,2016
Feb,15,blueberry,2016
Mar,95,blueberry,2016
Apr,64,blueberry,2016
May,11,blueberry,2016
Jun,33,blueberry,2016
Jul,64,blueberry,2016
Aug,53,blueberry,2016
Sep,27,blueberry,2016
Oct,103,blueberry,2016
Nov,108,blueberry,2016
Dec,62,blueberry,2016
Jan,80,strawberry,2015
Feb,0,strawberry,2015
Mar,71,strawberry,2015
Apr,51,strawberry,2015
May,3,strawberry,2015
Jun,11,strawberry,2015
Jul,56,strawberry,2015
Aug,34,strawberry,2015
Sep,12,strawberry,2015
Oct,75,strawberry,2015
Nov,94,strawberry,2015
Dec,46,strawberry,2015
Jan,76,grape,2015
Feb,0,grape,2015
Mar,78,grape,2015
Apr,58,grape,2015
May,10,grape,2015
Jun,22,grape,2015
Jul,47,grape,2015
Aug,36,grape,2015
Sep,18,grape,2015
Oct,86,grape,2015
Nov,98,grape,2015
Dec,40,grape,2015
Jan,79,blueberry,2015
Feb,0,blueberry,2015
Mar,78,blueberry,2015
Apr,49,blueberry,2015
May,5,blueberry,2015
Jun,31,blueberry,2015
Jul,62,blueberry,2015
Aug,49,blueberry,2015
Sep,7,blueberry,2015
Oct,86,blueberry,2015
Nov,100,blueberry,2015
Dec,46,blueberry,2015`;
// Set the margins
var margin = {top: 60, right: 100, bottom: 20, left: 80},
width = 850 - margin.left - margin.right,
height = 370 - margin.top - margin.bottom;
// Parse the month variable
var parseMonth = d3.timeParse("%b");
var formatMonth = d3.timeFormat("%b");
var formatYear = d3.timeFormat("%Y");
var parseYear = d3.timeParse("%Y");
// Set the ranges
var x = d3.scaleTime().domain([parseMonth("Jan"), parseMonth("Dec")]).range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// Define the line
var valueLine = d3.line()
.x(function(d) { return x(d.Month); })
.y(function(d) { return y(+d.Sales); })
// Create the svg canvas in the "graph" div
var svg = d3.select("#graph")
.append("svg")
.style("width", width + margin.left + margin.right + "px")
.style("height", height + margin.top + margin.bottom + "px")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")")
.attr("class", "svg");
var data = d3.csvParse(dataAsCsv);
// Format the data
data.forEach(function(d) {
d.Month = parseMonth(d.Month);
d.Sales = +d.Sales;
d.Fruit = d.Fruit;
d.Year = formatYear(parseYear(+d.Year));
});
var nest = d3.nest()
.key(function(d){
return d.Fruit;
})
.key(function(d){
return d.Year;
})
.entries(data)
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.Month; }));
y.domain([0, d3.max(data, function(d) { return d.Sales; })]);
// Set up the x axis
var xaxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "x axis")
.call(d3.axisBottom(x)
.ticks(d3.timeMonth)
.tickSize(0, 0)
.tickFormat(d3.timeFormat("%B"))
.tickSizeInner(0)
.tickPadding(10));
// Add the Y Axis
var yaxis = svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(y)
.ticks(5)
.tickSizeInner(0)
.tickPadding(6)
.tickSize(0, 0));
// Add a label to the y axis
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - 60)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Monthly Sales")
.attr("class", "y axis label");
svg.append('g').classed('data-points', true);
// Create a dropdown
var fruitMenu = d3.select("#fruitDropdown")
fruitMenu
.append("select")
.selectAll("option")
.data(nest)
.enter()
.append("option")
.attr("value", function(d){
return d.key;
})
.text(function(d){
return d.key;
})
// Function to create the initial graph
var initialGraph = function(fruit){
// Filter the data to include only fruit of interest
var selectFruit = nest.filter(function(d){
return d.key == fruit;
})
var selectFruitGroups = svg.selectAll(".fruitGroups")
.data(selectFruit, function(d){
return d ? d.key : this.key;
})
.enter()
.append("g")
.attr("class", "fruitGroups")
var initialPath = selectFruitGroups.selectAll(".line")
.data(function(d) { return d.values; })
.enter()
.append("path")
initialPath
.attr("d", function(d){
return valueLine(d.values)
})
.attr("class", "line")
svg.select('g.data-points').selectAll("dot")
.data(data.filter(function(d) {
return d.Fruit === fruit;
}))
.enter().append("circle").classed('dot', true)
.attr("r", 3)
.style("fill", "pink").style('stroke', '#000')
.attr("cx", function(d) { return x(d.Month); })
.attr("cy", function(d) { return y(+d.Sales); });
}
// Create initial graph
initialGraph("strawberry")
// Update the data
var updateGraph = function(fruit){
// Filter the data to include only fruit of interest
var selectFruit = nest.filter(function(d){
return d.key == fruit;
})
// Select all of the grouped elements and update the data
var selectFruitGroups = svg.selectAll(".fruitGroups")
.data(selectFruit)
// Select all the lines and transition to new positions
selectFruitGroups.selectAll("path.line")
.data(function(d){
return (d.values);
})
.transition()
.duration(1000)
.attr("d", function(d){
return valueLine(d.values)
});
var circles = svg.select('g.data-points').selectAll(".dot")
.data(data.filter(function(d) {
return d.Fruit === fruit;
}));
circles
.enter().append("circle")
.merge(circles).classed('data-point', true)
.attr("r", 3)
.style("fill", "pink").style('stroke', '#000')
.transition().duration(1000)
.attr("cx", function(d) { return x(d.Month); })
.attr("cy", function(d) { return y(+d.Sales); });
}
// Run update function when dropdown selection changes
fruitMenu.on('change', function(){
// Find which fruit was selected from the dropdown
var selectedFruit = d3.select(this)
.select("select")
.property("value")
// Run update function with the selected fruit
updateGraph(selectedFruit)
});
.line {
fill: none;
stroke: #EF5285;
stroke-width: 2px;
}
<script src="https://d3js.org/d3.v4.js"></script>
<div id = "fruitDropdown"></div>
<div id="graph"></div>
Important code changes:
Instead of appending circles directly to the SVG, I've created a group <g class="data-points"></g> that holds all the dots.
svg.append('g').classed('data-points', true);
Enter/update/exit all dots within the above group in both functions i.e. initialGraph and updateGraph
InitialGraph:
svg.select('g.data-points').selectAll("dot")
.data(data.filter(function(d) {
return d.Fruit === fruit;
}))
.enter().append("circle").classed('dot', true)
.attr("r", 3)
.style("fill", "pink").style('stroke', '#000')
.attr("cx", function(d) { return x(d.Month); })
.attr("cy", function(d) { return y(+d.Sales); });
UpdateGraph:
var circles = svg.select('g.data-points').selectAll(".dot")
.data(data.filter(function(d) {
return d.Fruit === fruit;
}));
circles
.enter().append("circle")
.merge(circles).classed('data-point', true)
.attr("r", 3)
.style("fill", "pink").style('stroke', '#000')
.transition().duration(1000)
.attr("cx", function(d) { return x(d.Month); })
.attr("cy", function(d) { return y(+d.Sales); });
Observe the data filtering based on the fruit selected bound to the circles and the transition applied in order to match the transition for the lines.
Always use style for applying fill and not attr. It's a good practice. Adding color:pink wouldn't change the color of the circles but fill would. In addition, I've added a stroke in order to make them visible even with a pink color. You can always change that though.
I would suggest you to add a code snippet every time you ask a question and not provide links. That would be easier for anyone to debug and help fix errors.
Hope this helps. :)
I need to make a D3 BoxPlot.
I have a dataset with several billions of rows, and it is unfeasible to send the raw data to the client. So, I created an API and I send only the summarized version containing the max/min/std_dev values of each column.
In all the examples ( one two ) I saw using D3 BoxPlot, the data summarization is done on the client side (the opposite of my case).
Is it possible to use the BoxPlot with already calculated data? Does anyone have an example?
Well, since you are getting the already calculated data, the task here is even easier!
First, let's set the scales. In the data you copy/pasted in your comment you have max and min, which I'll use for the third and first quartiles. Since you don't have the second quartile (median) in your data, I'll use mean. Also, as your data have 3 identical objects, I change it a little bit, to make the boxes different.
So, setting the y scale:
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d.max
}) * 1.1])
.range([h - padding, padding]);
Which is a standard linear scale.
For the x scale, I'm using a band scale:
var xScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.label
}))
.range([padding, w - padding])
.padding(0.4);
Which is very good to give us the left and right limits of the rectangles.
Now, it's just a matter of printing the rectangles and the lines (the medians).
For the rectangles, notice the math to get the third quartile as the top of the rectangle, and the first quartile as its height (y and height attributes):
var boxes = svg.selectAll("foo")
.data(data)
.enter()
.append("rect")
.attr("fill", "none")
.attr("stroke", "black")
.attr("x", function(d) {
return xScale(d.label)
})
.attr("width", xScale.bandwidth())
.attr("y", function(d) {
return yScale(d.max)
})
.attr("height", function(d) {
return yScale(d.min) - yScale(d.max)
});
And, finally, for the lines, we just use mean for both y1 and y2 values:
var lines = svg.selectAll("foo")
.data(data)
.enter()
.append("line")
.attr("stroke", "black")
.attr("stroke-width", 4)
.attr("x1", function(d) {
return xScale(d.label)
})
.attr("x2", function(d) {
return xScale(d.label) + xScale.bandwidth()
})
.attr("y1", function(d) {
return yScale(d.mean)
})
.attr("y2", function(d) {
return yScale(d.mean)
})
Here is a demo with your data structure:
var data = [{
"count": "2",
"min": "1.6",
"max": "4.1",
"label": "labelA",
"stddev": "0.72",
"mean": "3.1"
}, {
"count": "2",
"min": "1.1",
"max": "2.9",
"label": "labelB",
"stddev": "0.72",
"mean": "2.2"
}, {
"count": "2",
"min": "2.4",
"max": "3.6",
"label": "labelC",
"stddev": "0.72",
"mean": "2.7"
}];
var w = 500,
h = 200,
padding = 30,
padding2 = 20;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d.max
}) * 1.1])
.range([h - padding2, 10]);
var xScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.label
}))
.range([padding, w - padding])
.padding(0.4);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
var gX = svg.append("g")
.attr("transform", "translate(0," + (h - padding2) + ")")
.call(xAxis);
var gY = svg.append("g")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
var boxes = svg.selectAll("foo")
.data(data)
.enter()
.append("rect")
.attr("fill", "none")
.attr("stroke", "black")
.attr("x", function(d) {
return xScale(d.label)
})
.attr("width", xScale.bandwidth())
.attr("y", function(d) {
return yScale(d.max)
})
.attr("height", function(d) {
return yScale(d.min) - yScale(d.max)
});
var lines = svg.selectAll("foo")
.data(data)
.enter()
.append("line")
.attr("stroke", "black")
.attr("stroke-width", 4)
.attr("x1", function(d) {
return xScale(d.label)
})
.attr("x2", function(d) {
return xScale(d.label) + xScale.bandwidth()
})
.attr("y1", function(d) {
return yScale(d.mean)
})
.attr("y2", function(d) {
return yScale(d.mean)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
PS: Once you have the data for the whiskers, you can simply add the code for creating the lines to this basic structure, following the same principle.
You can find a boxplot rendering component as part of d3fc:
https://d3fc.io/api/series-api.html#boxplot
You can use this to render data that has already been 'summarised'
(Full disclosure: I'm one of the authors of d3fc)
I'm building a grouped bar chart by nesting a .csv file. The chart will also be viewable as a line chart, so I want a nesting structure that suits the line object. My original .csv looks like this:
Month,Actual,Forecast,Budget
Jul-14,200000,-,74073.86651
Aug-14,198426.57,-,155530.2499
Sep-14,290681.62,-,220881.4631
Oct-14,362974.9,-,314506.6437
Nov-14,397662.09,-,382407.67
Dec-14,512434.27,-,442192.1932
Jan-15,511470.25,511470.25,495847.6137
Feb-15,-,536472.5467,520849.9105
Mar-15,-,612579.9047,596957.2684
Apr-15,-,680936.5086,465313.8723
May-15,-,755526.7173,739904.081
Jun-15,-,811512.772,895890.1357
and my nesting is like this:
d3.csv("data/net.csv", function(error, data) {
if (error) throw error;
var headers = d3.keys(data[0]).filter(function(head) {
return head != "Month";
});
data.forEach(function(d) {
d.month = parseDate(d.Month);
});
var categories = headers.map(function(name) {
return {
name: name, // "name": the csv headers except month
values: data.map(function(d) {
return {
date: d.month,
rate: +(d[name]),
};
}),
};
});
The code to build my chart is:
var bars = svg.selectAll(".barGroup")
.data(data) // Select nested data and append to new svg group elements
.enter()
.append("g")
.attr("class", "barGroup")
.attr("transform", function (d) { return "translate(" + xScale(d.month) + ",0)"; });
bars.selectAll("rect")
.data(categories)
.enter()
.append("rect")
.attr("width", barWidth)
.attr("x", function (d, i) { if (i < 2) {return 0;} else {return xScale.rangeBand() / 2;}})
.attr("y", function (d) { return yScale(d.rate); })
.attr("height", function (d) { return h - yScale(d.rate); })
.attr("class", function (d) { return lineClass(d.name); });
The g elements are fine and the individual bars are being mapped to them, with the x value and class applied correctly.
My problem comes in accessing the data for 'rate' for the height and y value of the bars. In the form above it gives a NaN. I've also tried using the category data to append g elements and then appending the rects with:
.data(function(d) { return d.values })
This allows me to access the rate data, but maps all 36 bars to each of the rangeBands.
It also works fine in a flatter data structure, but I can't seem to use it when it's nested two levels down, despite looking through a great many examples and SO questions.
How do I access the rate data?
In response to Cyril's request, here's the full code:
var margin = {top: 20, right: 18, bottom: 80, left: 50},
w = parseInt(d3.select("#bill").style("width")) - margin.left - margin.right,
h = parseInt(d3.select("#bill").style("height")) - margin.top - margin.bottom;
var customTimeFormat = d3.time.format.multi([
[".%L", function(d) { return d.getMilliseconds(); }],
[":%S", function(d) { return d.getSeconds(); }],
["%I:%M", function(d) { return d.getMinutes(); }],
["%I %p", function(d) { return d.getHours(); }],
["%a %d", function(d) { return d.getDay() && d.getDate() != 1; }],
["%b %d", function(d) { return d.getDate() != 1; }],
["%b", function(d) { return d.getMonth(); }],
["%Y", function() { return true; }]
]);
var parseDate = d3.time.format("%b-%y").parse;
var displayDate = d3.time.format("%b %Y");
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, w], .1);
var xScale1 = d3.scale.linear()
.domain([0, 2]);
var yScale = d3.scale.linear()
.range([h, 0])
.nice();
var xAxis = d3.svg.axis()
.scale(xScale)
.tickFormat(customTimeFormat)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.innerTickSize(-w)
.outerTickSize(0);
var svg = d3.select("#svgCont")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var thous = d3.format(",.0f")
var lineClass = d3.scale.ordinal().range(["actual", "forecast", "budget"]);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<p id='date'>" + displayDate(d.date) + "</p><p id='value'>$" + thous(d.rate);
})
d3.csv("data/net.csv", function(error, data) {
if (error) throw error;
var headers = d3.keys(data[0]).filter(function(head) {
return head != "Month";
});
data.forEach(function(d) {
d.month = parseDate(d.Month);
});
var categories = headers.map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {
date: d.month,
rate: +(d[name]),
};
}),
};
});
var min = d3.min(categories, function(d) {
return d3.min(d.values, function(d) {
return d.rate;
});
});
var max = d3.max(categories, function(d) {
return d3.max(d.values, function(d) {
return d.rate;
});
});
var minY = min < 0 ? min * 1.2 : min * 0.8;
xScale.domain(data.map(function(d) { return d.month; }));
yScale.domain([minY, (max * 1.1)]);
var barWidth = headers.length > 2 ? xScale.rangeBand() / 2 : xScale.rangeBand() ;
svg.call(tip);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
var bars = svg.selectAll(".barGroup")
.data(data)
.enter()
.append("g")
.attr("class", "barGroup")
.attr("transform", function (d) { return "translate(" + xScale(d.month) + ",0)"; });
bars.selectAll("rect")
.data(categories)
.enter()
.append("rect")
.attr("width", barWidth)
.attr("x", function (d, i) { if (i < 2) {return 0;} else {return xScale.rangeBand() / 2;}})
.attr("y", function (d) { return yScale(d.rate); })
.attr("height", function (d) { return h - yScale(d.rate); })
.attr("class", function (d) { return lineClass(d.name) + " bar"; });
var legend = svg.selectAll(".legend")
.data(headers)
.enter()
.append("g")
.attr("class", "legend");
legend.append("line")
.attr("class", function(d) { return lineClass(d); })
.attr("x1", 0)
.attr("x2", 40)
.attr("y1", function(d, i) { return (h + 30) + (i *14); })
.attr("y2", function(d, i) { return (h + 30) + (i *14); });
legend.append("text")
.attr("x", 50)
.attr("y", function(d, i) { return (h + 32) + (i *14); })
.text(function(d) { return d; });
svg.selectAll(".bar")
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
});
Update 18 Feb '16.
It seems I haven't explained what I was trying to do sufficiently well. The line and bar versions of the chart will be seen separately, i.e. users can see either one according to input to a select element. Also note that I don't have control over how the data comes in initially.
I have a version of exactly how it should work here.
This question was raised when I was still working through it, but I never solved the issue – I used a workaround of doing two separate nests of the data.
Link to jsfiddle:
https://jsfiddle.net/sladav/rLh4qwyf/1/
I think the root of the issue is that you want to use two variables that do not explicitly exist in your original data set: (1) Category and (2) Rate.
Your data is formatted in a wide format in that each category gets its own variable and the value for rate exists at the crossroads of month and one of the given categories. I think the way you're nesting ultimately is or at least should address this, but it is unclear to me if or where something gets lost in translation. Conceptually, I think it makes more sense to start with an organization that matches what you are trying to accomplish. I reformatted the original data and approached it again - on a conceptual level the nesting seems straightforward and simple...
NEW COLUMNS:
Month: Time Variable; mapped to X axis
Category: Categorical values [Actual, Forecast, Budget]; used to group/color
Rate: Numerical value; mapped to Y axis
Reorganized CSV (dropped NULLs):
Month,Category,Rate
Jul-14,Actual,200000
Aug-14,Actual,198426.57
Sep-14,Actual,290681.62
Oct-14,Actual,362974.9
Nov-14,Actual,397662.09
Dec-14,Actual,512434.27
Jan-15,Actual,511470.25
Jan-15,Forecast,511470.25
Feb-15,Forecast,536472.5467
Mar-15,Forecast,612579.9047
Apr-15,Forecast,680936.5086
May-15,Forecast,755526.7173
Jun-15,Forecast,811512.772
Jul-14,Budget,74073.86651
Aug-14,Budget,155530.2499
Sep-14,Budget,220881.4631
Oct-14,Budget,314506.6437
Nov-14,Budget,382407.67
Dec-14,Budget,442192.1932
Jan-15,Budget,495847.6137
Feb-15,Budget,520849.9105
Mar-15,Budget,596957.2684
Apr-15,Budget,465313.8723
May-15,Budget,739904.081
Jun-15,Budget,895890.1357
With your newly formatted data, you start by using d3.nest to GROUP your data explicitly with the CATEGORY variable. Now your data exists in two tiers. The first tier has three groups (one for each category). The second tier contains the RATE data for each line/set of bars. You have to nest your data selections as well - the first layer is used to draw the lines, the second layer for the bars.
Nesting your data:
var nestedData = d3.nest()
.key(function(d) { return d.Category;})
.entries(data)
Create svg groups for your grouped, 1st-tier data:
d3.select(".plot-space").selectAll(".g-category")
.data(nestedData)
.enter().append("g")
.attr("class", "g-category")
Use this data to add your lines/paths:
d3.selectAll(".g-category").append("path")
.attr("class", "line")
.attr("d", function(d){ return lineFunction(d.values);})
.style("stroke", function(d) {return color(d.key);})
Finally, "step into" 2nd-tier to add bars/rect:
d3.selectAll(".g-category").selectAll(".bars")
.data(function(d) {return d.values;})
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {return x(d.Month);})
.attr("y", function(d) {return y(d.Rate);})
.attr("width", 20)
.attr("height", function(d) {return height - y(d.Rate)})
.attr("fill", function(d) {return color(d.Category)})
This is a straightforward approach (to me at least), in that you take it one category at a time, using the grouped data to draw a line, then individual data points to draw the bars.
LAZY EDIT:
To get category bars side by side
Create ordinal scale mapping category to [1,nCategories]. Use this to dynamically offset bars with something like
translate( newScale(category)*barWidth )
To show either bars or lines (not both)
Create a function that selects bars/lines and transitions/toggles their visibility/opacity. Run when your drop-down input changes and with the drop-down input as input to the function.
The problem, I belive, is that you are binding the categories array to the bars selection, like this:
bars.selectAll("rect").data(categories)
As far as I can see (whithout a running demo) categories is an array with only four values (one for each category).
You have to go one step 'deeper' in your nested data structure.
To draw a set of bars for each category you would need to iterate over categories and bind the values array that contains the actual values to the selection.
Something like:
categories.each(function (category) {
var klass = category.name;
bars.selectAll("rect ." + klass)
.data(category.values)
.enter()
.append("rect")
.attr("class", klass)
.attr("width", barWidth)
.attr("x", function (d, i) { /* omitted */})
.attr("y", function (d) { return yScale(d.rate); })
.attr("height", function (d) { return h - yScale(d.rate); });
});
---- Edit
Instead of the above code, think about drawing the bars just like you do with the lines. Like this:
var bars = svg.selectAll(".barGroup")
.data(categories)
.enter()
.append("g")
.attr("class", function (d) { return lineClass(d.name) + "Bar barGroup"; })
.attr("transform", function (d, i) {
var x = i > 1 ? xScale.rangeBand() / 2 : 0;
return "translate(" + x + ",0)";
})
.selectAll('rect')
.data(function (d) { return d.values; })
.enter()
.append("rect")
.attr("class", "bar")
.attr("width", barWidth)
.attr("x", function (d, i) { return xScale(d.date); })
.attr("y", function (d, i) { return yScale(d.rate); })
.attr("height", function (d) { return h - yScale(d.rate); });
I'm trying to add a drop down menu that selects which measure to graph. I have 8 graphs, all graphing the same measure but by different ethnicities. Below is the code, any thoughts on what I'm doing wrong? Right now i get the error exit() is not a function.
Ok i've made some progress with the following, however it's still a little wonky. The graphs are changing but are going off the charts - the yAxis is rescaling to the max of all of the graphs, not the local one.:
function updateGraphs(newData) {
d3.selectAll("svg").each(function(d, i){
eachRace = d.values;
svg = d3.select(this);
yMax = d3.max(eachRace, function(d) { return d[newData]; });
yScale = d3.scale.linear().domain([0, yMax*1.25]).range([height/8, 0]).nice();
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(5);
line = d3.svg.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return yScale(d[newData]); })
d3.transition().duration(1000).selectAll(".line")
.attr("id", function(d) { return d.key ;})
.attr('class', 'line')
.attr('opacity', .8)
.attr('d', function(d) { return line(d.values); })
d3.selectAll(".y.axis")
.call(yAxis);
});
}
var svgContainer = d3.select("body").selectAll("svg")
.data(data2)
svgContainer.enter()
.append("svg")
.attr("width", 150)
.attr("height", 400)
.attr("transform", "translate(" + margin.left + "," + margin.top+ ")");
d3.selectAll("svg").each(function(d, i){
var eachRace = d.values;
var svg = d3.select(this);
var yMax = d3.max(eachRace, function(d) { return d.app; });
var yScale = d3.scale.linear().domain([0, yMax*1.25]).range([height/8, 0]).nice();
var yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(5);
var line = d3.svg.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return yScale(d.app); })
svg.append("path")
.attr("id", function(d) { return d.key ;})
.attr('class', 'line')
.attr('opacity', .8)
.attr('d', function(d) { return line(d.values); })
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
Thanks JSBob for working with me. I've got it working with:
function updateGraphs(newData) {
d3.selectAll("svg").each(function(d, i){
eachRace = d.values;
svg = d3.select(this);
yMax = d3.max(eachRace, function(d) { return d[newData]; });
yScale = d3.scale.linear().domain([0, yMax*1.25]).range([height/8, 0]).nice();
yAxis = d3.svg.axis().scale(yScale).orient("left").ticks(5);
console.log(yMax);
line = d3.svg.line()
.x(function (d) { return x(d.date); })
.y(function (d) { return yScale(d[newData]); })
svg.transition().duration(1000).select(".line")
.attr("id", function(d) { return d.key ;})
.attr('class', 'line')
.attr('opacity', .8)
.attr('d', function(d) { return line(d.values); });
svg.transition().duration(1000).select(".y.axis")
.call(yAxis);
});
}