I'm trying to generate multiple panels of multiple lines plots in D3 with a 2 levels nested data structure.
Can someone please point me on how to properly generate line plots. I've intuitively tried to use a 2 levels nested data structure, but I can`t find how to properly distribute the lines in their corresponding panels.
See here for the results I have so far:
http://jtremblay.github.io/viz/example.html
Here is my code.
var s = `condition,taxon,abundance,date
condition01,speciesA,0.31,2017-04-13
condition01,speciesA,0.54,2017-04-20
condition01,speciesB,0.21,2017-04-13
condition01,speciesB,0.60,2017-04-20
condition02,speciesA,0.31,2017-04-13
condition02,speciesA,0.48,2017-04-20
condition02,speciesB,0.19,2017-04-13
condition02,speciesB,0.61,2017-04-20
condition03,speciesA,0.13,2017-04-13
condition03,speciesA,0.11,2017-04-20
condition03,speciesB,0.04,2017-04-13
condition03,speciesB,0.11,2017-04-20
`;
var data = d3.csvParse(s);
data.forEach(function(d) { // Make every date in the csv data a javascript date object format
var aDate = new Date(d.date);
d.date = aDate;
});
var taxa = data.map(function (d){
return d.taxon
});
taxa = taxa.filter(onlyUniqueArray);
var dates = data.map(function (d){
return d.dates
});
var dataNested = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d.condition;})
.key(function(d) { return d.taxon;})
.entries(data);
console.log(dataNested);
var fillColors = ["#0000CD", "#00FF00", "#FF0000", "#808080"]
// color palette
var color = d3.scaleOrdinal()
.domain(taxa)
.range(fillColors);
//Margins
var margin = { top: 20, right: 20, bottom: 60, left: 50},
width = 500 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// Define dom and svg
var dom = d3.select("#viz");
var svg = dom.selectAll("multipleLineCharts")
.data(dataNested)
.enter()
.append("div")
.attr("class", "chart")
.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 + ")")
//.attr("fake", function(d) {console.log("d inside svg:"); console.log(d);})
// Add X axis --> it is a date format
var xScale = d3.scaleTime()
.rangeRound([0, width])
xScale.domain(d3.extent(data, function(d) {return d.date; }));
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "x axis")
.call(d3.axisBottom(xScale))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-90)")
.attr("dx", "-0.8em")
.attr("dy", "-0.45em")
//Add Y axis - Here because we want all panels to be on same scale, we cant use the dates from the global data structure.
var yScale = d3.scaleLinear()
.domain([
d3.min(data, function(d) { return d.abundance; } ),
d3.max(data, function(d) { return d.abundance; } )
])
.range([ height, 0 ]);
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale).ticks(5));
//Add Z scale (colors)
var zScale = d3.scaleOrdinal()
.range(fillColors);
zScale.domain(taxa);
// generate lines.
svg
.append("path")
.attr("class", "line")
.style("stroke", function(d) { return zScale(d.key); })
.attr("d", function(d, i){
return d3.line()
.x(function(d) { return xScale(d.date); })
.y(function(d) { return yScale(d.abundance); })
(data); //I know something else should go in there, but can't figure out what/how exactly...
})
/* Util functions */
function onlyUniqueArray(value, index, self) {
return self.indexOf(value) === index;
}
I don't understand how to effectively handle my data structure for what I want to do...
Is my 2x nested data structure is adequate for what I'm trying to accomplish? I've tried with a one level nested data structure, but with no success.
Finally solved it. This example helped me to understand how to handle nested selections : http://bl.ocks.org/stepheneb/1183998
Essentially, the last block of code was replaced with this:
// generate lines.
var lines = svg.selectAll("lines")
.data(function(d) { return d.values;})
.enter()
.append("path")
.attr("class", "line")
.attr("d", function(d){
return d3.line()
.x(function(d) { return xScale(d.date); })
.y(function(d) { return yScale(d.abundance); })
(d.values);
})
.style("stroke", function(d) { return zScale(d.key); })
With a working example here: http://jtremblay.github.io/viz/example-fixed.html
Related
how can i create a time scale over seconds, minutes, hours, days, months and years. in my code i get a second line when the seconds overlap.
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 600 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// parse the date / time
var parseTime = d3.timeParse("%Y-%m-%dT%H:%M:%S");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the 0 line
var valueline0 = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.value0); });
// append the svg object to the id="chart" of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom + 75 )
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Get the data
d3.csv("/trace/O00.csv", function(error, data) {
if (error) throw error;
// format the data
data.forEach(function(d) {
d.date = parseTime(d.date);
d.value0 = +d.value0;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) {
return Math.max(d.value0); })]);
// Add the valueline0 path.
svg.append("path")
.data([data])
.attr("class", "line")
.style("stroke", "steelblue")
.attr("d", valueline0);
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%Y-%m-%d %H:%M:%S")))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(y));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
O00.csv:
date,value0
2020-07-14T14:03:51,35.66
2020-07-14T14:04:01,23.56
2020-07-14T14:03:11,32.64
2020-07-14T14:03:21,22.55
2020-07-14T14:03:31,28.60
2020-07-14T14:03:41,38.70
2020-07-14T14:03:51,35.66
2020-07-14T14:04:01,23.56
2020-07-14T14:04:11,21.54
chart with 2lines
the second line starts with the 7th data record (2020-07-14T14:03:51,35.66) because the seconds (51) from the 1st data record (2020-07-14T14:03:51,35.66) are repeated.
Thanks in advance, Onka
There is "only one line". You have Dates with multiple values. If you don't want one of the values, then you have to remove that value from your dataset, by filtering the data in some way.
If you want to remove the extra datapoint you'll have to figure out which one is the correct value. For instance if we say, "Let's use the max value", convert this code:
// format the data
data.forEach(function(d) {
d.date = parseTime(d.date);
d.value0 = +d.value0;
});
To this
// format the data
data.forEach(function(d) {
d.date = parseTime(d.date);
d.value0 = +d.value0;
});
const dataMap = {};
let dupCount = 0;;
data.forEach((d, index) => {
if (!dataMap[d.date]) {
dataMap[d.date] = true;
} else {
// remove the duplicate from the CSV
data.splice(index - dupCount, 1);
dupCount++;
}
});
Alternative, and much simpler, would be to first filter the data from the CSV using the csv parser: https://www.npmjs.com/package/csv-parser and then passing that to the .data(filteredCsvData) function rather than using the builtin d3.csv() which doesn't contain what you need.
the problem was due to the non-consecutive records in the csv file. if the sequence is correct, everything works as desired! done! thanks for all...
I am able to see glucose readings but time shows up as: 0NaN-NaN-NaNTNaN:NaN:NaN.NaNZ
I am trying to parse a dataset of time of the format "Y-M-D H:M:S.MS". I need it to be formatted properly so that I can show it on the x axis. I have attached sample dataset to this code.
My code looks like this:
<script>
function overview(){
// Set the dimensions of the canvas / graph
var margin = {top: 10, right: 20, bottom: 30, left: 30},
width = 600 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// // Parse the date / time
var parseDate = d3.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ");
var x = d3.scaleTime()
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var xAxis = d3.axisBottom()
.tickFormat(d3.timeFormat("%H"));
var yAxis = d3.axisLeft();
// Define the line
var valueline = d3.line()
.x(function(d) { return x(d.time); })
.y(function(d) { return y(d.glucoseReading); });
// Adds the svg canvas
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 + ")");
// Get the data
d3.csv("glucose.csv", function(error, data) {
data.forEach(function(d) {
d.time = parseDate(d.time);
d.glucoseReading = +d.glucoseReading;
console.log(d.time);
console.log(d.glucoseReading);
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.time; }));
y.domain([0, d3.max(data, function(d) { return d.glucoseReading; })]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));
// Add the scatterplot
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.glucoseReading); });
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
}
overview();
</script>
My Dataset looks like:
You want to convert (parse) strings to dates, not the other way around. Therefore, instead of d3.utcFormat(), you have to use d3.utcParse(). On top of that, your specifier is incorrect: there is no timezone in your strings.
So, this should be your parseDate function and specifier:
var parseDate = d3.utcParse("%Y-%m-%d %H:%M:%S.%L")
Here is it working (check your browse console, not the snippet's one):
var parseDate = d3.utcParse("%Y-%m-%d %H:%M:%S.%L")
var string = "2017-08-23 00:03:52.591";
console.log(parseDate(string))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Hey guys I created a time series line chart using publicly available stock data.
Where I got to is the following:
Looks like what it is doing is connecting the first datapoint with the last datapoint which is why it is creating a line across the entire chart.
I looked online and read that to create a non continuous line chart I can add
.defined(function(d) { return d; })
I did but it didn't help.
My code:
//Set dimensions and margins for the graph
var margin = {top: 20, right: 20, bottom: 100, left: 70},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//Create canvas
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 + ")");
//Parse date
var parseDate = d3.timeParse("%Y-%m-%d");
//Set the ranges
var x = d3.scaleTime()
.range([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
// Define the line
var valueLine = d3.line()
.defined(function(d) { return d; })
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.value); });
//Import data from api
d3.json("api_all.php", function(error, data) {
if (error) throw error;
data.forEach(e => {
e.date = parseDate(e.date);
e.value = +e.close;
e.stockName = e.stock_name;
e.stockSymbol = e.stock_symbol;
});
//Create nest variable
var nest = d3.nest()
.key(function(d){ return d.stockSymbol; })
.entries(data);
console.log(nest);
//Scale the range of the data
//x axis scale for entire dataset
x.domain(d3.extent(data, function(d) { return d.date; }));
//y.domain([0, d3.max(data, function(d) { return d.value; })]);
//Add the x axis
var xaxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "x axis")
.call(d3.axisBottom(x));
//Add x axis label
svg.append("text")
.attr("transform", "translate(" + (width/2) + "," + (height + margin.top + 10) + ")")
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Date")
.attr("class", "x axis label");
//Create dropdown
var dropDown = d3.select("#dropDown")
dropDown
.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(stock){
// Filter the data to include only stock of interest
var selectStock = nest.filter(function(d){
return d.key == stock;
})
console.log(selectStock)
//Unnest selectStock for y axis
var unnested = function(data, children){
var out = [];
data.forEach(function(d, i){
console.log(i, d);
d_keys = Object.keys(d);
console.log(i, d_keys)
values = d[children];
values.forEach(function(v){
d_keys.forEach(function(k){
if (k != children) { v[k] = d[k]}
})
out.push(v);
})
})
return out;
}
var selectStockUnnested = unnested(selectStock, "values");
//Scale y axis
var selectStockGroups = svg.selectAll(".stockGroups")
.data(selectStock, function(d){
return d ? d.key : this.key;
})
.enter()
.append("g")
.attr("class", "stockGroups")
.each(function(d){
y.domain([0, d3.max(selectStockUnnested, function(d) { return d.value; })])
console.log(selectStockUnnested);
});
var initialPath = selectStockGroups.selectAll(".line")
.data(selectStock)
.enter()
.append("path")
initialPath
.attr("d", function(d){ return valueLine(d.values) })
.attr("class", "line")
//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 y axis label
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - 60)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Price")
.attr("class", "y axis label");
}
// Create initial graph
initialGraph("1301.T")
// Update the data
var updateGraph = function(stock){
// Filter the data to include only stock of interest
var selectStock = nest.filter(function(d){
return d.key == stock;
})
console.log(selectStock);
//Unnest selectStock for y axis
var unnested = function(data, children){
var out = [];
data.forEach(function(d, i){
console.log(i, d);
d_keys = Object.keys(d);
console.log(i, d_keys)
values = d[children];
values.forEach(function(v){
d_keys.forEach(function(k){
if (k != children) { v[k] = d[k]}
})
out.push(v);
})
})
return out;
}
var selectStockUnnested = unnested(selectStock, "values");
// Select all of the grouped elements and update the data
var selectStockGroups = svg.selectAll(".stockGroups")
.data(selectStock)
.each(function(d){
y.domain([0, d3.max(selectStockUnnested, function(d) { return d.value; })])
});
// Select all the lines and transition to new positions
selectStockGroups.selectAll("path.line")
.data(selectStock)
.transition()
.duration(1000)
.attr("d", function(d){
return valueLine(d.values)
})
// Update the Y-axis
d3.select(".y")
.transition()
.duration(1500)
.call(d3.axisLeft(y)
.ticks(5)
.tickSizeInner(0)
.tickPadding(6)
.tickSize(0, 0));
}
// Run update function when dropdown selection changes
dropDown.on('change', function(){
// Find which stock was selected from the dropdown
var selectedStock = d3.select(this)
.select("select")
.property("value")
console.log(selectedStock);
// Run update function with the selected stock
updateGraph(selectedStock)
});
});
</script>
</body>
I'm trying to get 2 completely different d3 charts (2 line charts but totally different data - one with several lines and negative data, other with one line positive data) on the same page.
Right now, I only get the first one to be generated and shown correctly on the HTML page, the second chart doesn't show at all (not even svg container is generated).
Here is my code:
(function() {
// Get the data
d3.json("../assets/js/json/temperature.json", function(data) {
// Set the dimensions of the canvas / graph
var margin = {top: 30, right: 20, bottom: 30, left: 25},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%Y-%m-%d %H:%M:%S").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
// Define the line
var valueline = d3.svg.line()
.x(function(d) { return x(d.temps); })
.y(function(d) { return y(d.temperature); });
// prepare data
data.forEach(function(d) {
d.temps = parseDate(d.temps);
d.temperature = +d.temperature;
});
// Adds the svg canvas
var svg = d3.select("#graphTemp")
.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 + ")");
// Scale the range of the data on domain
x.domain(d3.extent(data, function(d) { return d.temps; }));
y.domain([0, d3.max(data, function(d) { return d.temperature; })]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
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")
.text("Temperatures");
});
})();
(function(){
// loads the data and loads it into chart - main function
d3.json("../assets/js/json/maitrise.json", function(data) {
var m = {top: 20, right: 5, bottom: 30, left: 40},
w = 70 - m.left - m.right,
h = 30 - m.top - m.bottom;
var x = d3.scale.linear().domain([0, data.length]).range([0 + m.left, w - m.right]);
var y = d3.scale.linear()
.rangeRound([h, 0]);
var line = d3.svg.line()
.interpolate("cardinal")
.x(function(d,i) { return x(i); })
.y(function (d) { return y(d.value); });
var color = d3.scale.ordinal()
.range(["#28c6af","#ffd837","#e6443c","#9c8305","#d3c47c"]);
var svg2 = d3.select("#maitrisee").append("svg")
.attr("width", w + m.left + m.right)
.attr("height", h + m.top + m.bottom)
.append("g")
.attr("transform", "translate(" + m.left + "," + m.top + ")");
// prep axis variables
var xAxis2 = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis2 = d3.svg.axis()
.scale(y)
.orient("left");
//console.log("Inital Data", data);
var labelVar = 'id'; //A
var varNames = d3.keys(data[0])
.filter(function (key) { return key !== labelVar;}); //B
color.domain(varNames); //C
var seriesData = varNames.map(function (name) { //D
return {
name: name,
values: data.map(function (d) {
return {name: name, label: d[labelVar], value: +d[name]};
})
};
});
console.log("seriesData", seriesData);
y.domain([
d3.min(seriesData, function (c) {
return d3.min(c.values, function (d) { return d.value; });
}),
d3.max(seriesData, function (c) {
return d3.max(c.values, function (d) { return d.value; });
})
]);
var series = svg2.selectAll(".series")
.data(seriesData)
.enter().append("g")
.attr("class", function (d) { return d.name; });
series.append("path")
.attr("class", "line")
.attr("d", function (d) { return line(d.values); })
.style("stroke", function (d) { return color(d.name); })
.style("stroke-width", "2px")
.style("fill", "none");
});
})();
OK, I found where the error was coming from. There was a piece of javascript in the middle of the HTML page that stopped d3 to generate the second graph further down in the page.
Thanks for all the help!
I apologize if this seems like a repeat. I've seen a lot of similar threads, but none seems to be solving the exact problem I have.
I need to create a multi-series line chart for a class. I'm not well versed in javascript and am completely new to D3, so I'm working through this as best I can. I started with this example: http://bl.ocks.org/mbostock/3884955
I copied the code in to an editor and all I've really done to it is change the format of the time scale to four digit year (which seems to be working), rename the variables "city" and "cities" to "category" and "categories", and relabel the Temperature axis with "Percent".
Here's my code:
<body>
<script src="http://d3js.org/d3.v3.js"></script>
<script>
var margin = {top: 20, right: 80, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//Change date format to four digit year.
var parseDate = d3.time.format("%Y").parse;
//x and y axes
var x = d3.time.scale()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var line = d3.svg.line()
//change from "basis" to "linear"
.interpolate("basis")
.x(function(d) { return x(d.date); })
//changed d.temperature to d.percent
.y(function(d) { return y(d.percent); });
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 + ")");
d3.tsv("data.tsv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "date"; }));
data.forEach(function(d) {
d.date = parseDate(d.date);
});
//changied variable "cities" to "categories"
var categories = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
//changed below to percent
return {date: d.date, percent: +d[name]};
})
};
});
x.domain(d3.extent(data, function(d) { return d.date; }));
//changed "cities" to "categories"
y.domain([
//Temp to percent
d3.min(categories, function(c) { return d3.min(c.values, function(v) { return v.percent; }); }),
d3.max(categories, function(c) { return d3.max(c.values, function(v) { return v.percent; }); })
]);
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")
//Changed Temperature to percent for label
.text("Percent (%)");
//Changed "city" to "category" and "cities" to "categories"
var category = svg.selectAll(".category")
.data(categories)
.enter().append("g")
.attr("class", "category");
category.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
category.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.percent) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
});
</script>
I know this is a lot - pretty much everything. I just have no idea where the problem is. If there is anyone willing to look at this, I'd appreciate it very much. Here's a link to where my current graph is hosted:http://www.pitt.edu/~kac232/hmwrk1ptA_test.html
Currently, what's happening is that I am missing a line for one category, and also my paths are perfectly straight, whigh makes no sense to me. I would expect them to stagger at least a little.
Thanks!
You're going to kick yourself, but the error is in your data's headings. You repeated the heading Bachelor's degree or more. Change one of the names and it will work as expected.