I am trying to graph this set of data (showing a snippet of a larger set) link to photo of chart shown below
month_started Member_casual count_of_rides
7 casual 435,289
8 casual 407,009
9 member 385,843
8 member 385,305
7 member 373,710
As you can see I have a month_started that maps to a member or casual attribute. I want to make a staked bar chart of the month_started as an independent variable (x) and the count_of_rides as a dependent (y) with the casual value stacked on top of the member per month.
I currently have a d3 bar chart that overlays both month_started = 7 on top of each other instead of stacked. I'm not sure how I can get d3 to recognize that it needs to read off of the member_casual variable to separate the two values.
I have additional code but here is the relevant sections I believe
Also, the .data(bike_trips) I believe should be .data(stackedData) but for some reason doesn't show up any bars, which makes me think my error could be with the stakedData variable.
Could anyone point me in the right direction please
bike_trip_chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
svg.append("g")
.attr("fill-opacity", .8)
.selectAll("rect")
.data(bike_trips)
.join("rect")
.attr("fill", (d) => colorScale(d.key))
.attr("x", d => x(d.month_started))
.attr("width", x.bandwidth())
.attr("y", d => y1(d.count_of_rides))
.attr("height", d => y1(0) - y1(d.count_of_rides));
svg.append("path")
.attr("fill", "none")
.attr("stroke", "#274e13")
.attr("stroke-miterlimit", 4)
.attr("stroke-width", 4)
.attr("d", line(chicago_weather));
svg.append("g")
.attr("fill", "none")
.attr("pointer-events", "all")
.selectAll("rect")
.data(bike_trips)
.join("rect")
.attr("x", d => x(d.month_started))
.attr("width", x.bandwidth())
.attr("y", 0)
.attr("height", height);
svg.append("g")
.call(xAxis);
svg.append("g")
.call(y1Axis);
svg.append("g")
.call(y2Axis);
return svg.node();
}
stackedData = d3.stack()
.keys(["member","casual"])
(bike_trips)
colorScale = d3.scaleOrdinal()
.domain(["member","casual"])
.range(["#E4BA14","#45818e"]);
Before you pass your data to d3.stack(), you want it to look like this:
[
{month: 7, casual: 435289, member: 373710},
{month: 8, casual: 407009, member: 385305},
{month: 9, member: 385843}
]
Here's a full example:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// set up
const margin = { top: 25, right: 25, bottom: 50, left: 50 };
const width = 500 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const 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},${margin.top})`);
// data
const data = [
{ month_started: 7, member_casual: "casual", count_of_rides: 435289 },
{ month_started: 8, member_casual: "casual", count_of_rides: 407009 },
{ month_started: 9, member_casual: "member", count_of_rides: 385843 },
{ month_started: 8, member_casual: "member", count_of_rides: 385305 },
{ month_started: 7, member_casual: "member", count_of_rides: 373710 },
];
const keys = ["member", "casual"];
const months = Array.from(new Set(data.map(d => d.month_started))).sort(d3.ascending);
// get a map from the month_started to the member_casual to the count_of_rides
const monthToTypeToCount = d3.rollup(
data,
// g is an array that contains a single element
// get the count for this element
g => g[0].count_of_rides,
// group by month first
d => d.month_started,
// then group by member of casual
d => d.member_casual
);
// put the data in the format mentioned above
const countsByMonth = Array.from(monthToTypeToCount, ([month, counts]) => {
// counts is a map from member_casual to count_of_rides
counts.set("month", month);
counts.set("total", d3.sum(counts.values()));
// turn the map into an object
return Object.fromEntries(counts);
});
const stackedData = d3.stack()
.keys(keys)
// return 0 if a month doesn't have a count for member/casual
.value((d, key) => d[key] ?? 0)
(countsByMonth);
// scales
const x = d3.scaleBand()
.domain(months)
.range([0, width])
.padding(0.25);
const y = d3.scaleLinear()
.domain([0, d3.max(countsByMonth, d => d.total)])
.range([height, 0]);
const color = d3.scaleOrdinal()
.domain(keys)
.range(["#E4BA14","#45818e"]);
// axes
const xAxis = d3.axisBottom(x);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
const yAxis = d3.axisLeft(y);
svg.append('g')
.call(yAxis);
// draw bars
const groups = svg.append('g')
.selectAll('g')
.data(stackedData)
.join('g')
.attr('fill', d => color(d.key));
groups.selectAll('rect')
.data(d => d)
.join('rect')
.attr('x', d => x(d.data.month))
.attr('y', d => y(d[1]))
.attr('width', x.bandwidth())
.attr('height', d => y(d[0]) - y(d[1]));
// title
svg.append('g')
.attr('transform', `translate(${width / 2},${-10})`)
.attr('font-family', 'sans-serif')
.append('text')
.attr('text-anchor', 'middle')
.call(
text => text.append('tspan')
.attr('fill', color('member'))
.text('member')
)
.call(
text => text.append('tspan')
.attr('fill', 'black')
.text(' vs. ')
)
.call(
text => text.append('tspan')
.attr('fill', color('casual'))
.text('casual')
)
</script>
</body>
</html>
Related
I'm using d3 to build a horizontal bar chart.
When it is first rendered the values are displayed within the bar:
However, when further data is added something is preventing it and the new bars from showing:
This problem is related to my code to display the values within the bars.
When I remove this chunk of code, the new bars show (just without the values in them):
Where am I going wrong?
function renderBarChart(data, metric, countryID) {
data = sortByHighestValues(data, metric)
const width = 0.9 * screen.width
const height = 0.8 * screen.height
const margin = { top: 0, right: 0, bottom: 20, left: 30 }
const innerHeight = height - margin.top - margin.bottom
const xScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d[metric])])
.range([margin.left, width]);
const yScale = d3
.scaleBand()
.domain(data.map((d) => d[countryID]))
.range([0, innerHeight])
.padding(0.2);
const yAxis = d3.axisLeft(yScale);
const xAxis = d3.axisBottom(xScale).ticks(10);
if (!barChartAxisRendered) {
renderYAxis(width, height, margin, yAxis)
renderXAxis(width, height, margin, xAxis, innerHeight)
} else {
updateXAxis(width, height, xAxis)
updateYAxis(width, height, yAxis)
}
barChartAxisRendered = true
renderBars(data, yScale, xScale, margin, metric, countryID)
};
function renderBars(data, yScale, xScale, margin, metric, countryID) {
let selectDataForBarCharts = d3.select("svg")
.selectAll("rect")
.data(data)
selectDataForBarCharts
.enter()
.append("rect")
.merge(selectDataForBarCharts)
.attr("fill", d => setBarColor(d))
.attr("y", (d) => yScale(d[countryID]))
.attr("width", (d) => xScale(d[metric]))
.attr("height", yScale.bandwidth())
.attr("transform", `translate(${margin.left}, ${margin.top})`)
selectDataForBarCharts
.enter()
.append("text")
.merge(selectDataForBarCharts)
.attr("class", "casesPerCapitaValues")
.attr('text-anchor', 'middle')
.attr("x", d => xScale(d[metric])-10)
.attr("y", d => yScale(d[countryID]) + 13)
.attr("fill", "white")
.style("font-size", "10px")
.text(d => d.casesPerCapita)
}
Your code breaks because you overwrite the rectangles with text. Make a different selections for texts and rects, or make a group containing corresponding rect+text of one row.
I want to do an animation on the lines, but the second line will draw from two parts, one from begining, and the other from close to the second last point and disappear, so I got a result like this
I was following others'code
const pathLength = path.node().getTotalLength();
const transitionPath = d3.transition().ease(d3.easeQuad).duration(3000);
path
.attrs({
"stroke-dashoffset": pathLength,
"stroke-dasharray": pathLength,
})
.transition(transitionPath)
.attr("stroke-dashoffset", 0);
if you need all the code, I can paste, but it is really just this part that works with the animation, thank you!
I think you're accidentally using the same path length twice - namely that of the first path. path.node() returns the first node, even if there are multiple nodes in the selection.
const data = [{
category: "series_1",
values: [{
name: "A",
value: 10
},
{
name: "B",
value: 21
},
{
name: "C",
value: 19
},
{
name: "D",
value: 23
},
{
name: "E",
value: 20
},
],
}, ];
let counter = 1;
const add_set = (arr) => {
let copy = JSON.parse(JSON.stringify(arr[0]));
const random = () => Math.floor(Math.random() * 20 + 1);
const add = (arr) => {
counter++;
copy.values.map((i) => (i.value = random()));
copy.category = `series_${counter}`;
arr.push(copy);
};
add(arr);
};
add_set(data);
//No.1 define the svg
let graphWidth = 600,
graphHeight = 300;
let margin = {
top: 60,
right: 10,
bottom: 30,
left: 45
};
let totalWidth = graphWidth + margin.left + margin.right,
totalHeight = graphHeight + margin.top + margin.bottom;
let svg = d3
.select("body")
.append("svg")
.attr("width", totalWidth)
.attr("height", totalHeight);
//No.2 define mainGraph
let mainGraph = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//No.3 define axises
let categoriesNames = data[0].values.map((d) => d.name);
let xScale = d3
.scalePoint()
.domain(categoriesNames)
.range([0, graphWidth]); // scalepoint make the axis starts with value compared with scaleBand
let colorScale = d3.scaleOrdinal(d3.schemeCategory10);
colorScale.domain(data.map((d) => d.category));
let yScale = d3
.scaleLinear()
.range([graphHeight, 0])
.domain([
d3.min(data, (i) => d3.min(i.values, (x) => x.value)),
d3.max(data, (i) => d3.max(i.values, (x) => x.value)),
]); //* If an arrow function is simply returning a single line of code, you can omit the statement brackets and the return keyword
//No.4 set axises
mainGraph
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + graphHeight + ")")
.call(d3.axisBottom(xScale));
mainGraph.append("g").attr("class", "y axis").call(d3.axisLeft(yScale));
//No.5 make lines
let lineGenerator = d3
.line()
.x((d) => xScale(d.name))
.y((d) => yScale(d.value))
.curve(d3.curveMonotoneX);
var lines = mainGraph
.selectAll(".path")
.data(data.map((i) => i.values))
.enter()
.append("path")
.attr("d", lineGenerator)
.attr("fill", "none")
.attr("stroke-width", 3)
.attr("stroke", (d, i) => colorScale(i));
//No.6 append circles
let circleData = data.map((i) => i.values);
mainGraph
.selectAll(".circle-container")
.data(circleData)
.enter()
.append("g")
.attr("class", "circle-container")
.attr("fill", (d, i) => console.log(d) || colorScale(i))
.selectAll("circle")
.data((d) => d)
.enter()
.append("circle")
.attrs({
cx: (d) => xScale(d.name),
cy: (d) => yScale(d.value),
r: 3,
opacity: 1,
});
// HERE we let the lines grow
lines
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `0 ${pathLength}`
})
.transition()
.duration(2500)
.attr("stroke-dasharray", function(d) {
// Get the path length of the current element
const pathLength = this.getTotalLength();
return `${pathLength} ${pathLength}`
});
.line {
stroke: blue;
fill: none;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
I'm working with D3 to create a line-graph. This graph is available here jsfiddle.
I'm trying to draw lines manually to represent certain data-point-values. I've tried to add comments to most of the lines in the code, so hopefully you can follow along.
My problem is that I cannot seem to draw negative numbers in a good way, if i do, then the graph-data-lines are misaligned. So my question is: How can i scale my graph so that I can show both negative and positive numbers? In this case, the graph should go from 2 to -2 based on the max/min values i've set.
currently. I'm scaling my graph like this
//
// Setup y scale
//
var y = d3.scale.linear()
.domain([0, max])
.range([height, 0]);
//
// Setup x scale
//
var x = d3.time.scale()
.domain(d3.extent(data, dateFn))
.range([0, width]);
In my mind, doing .domain([-2,max]) would be sufficient, but that seems to make things worse.
Also, my lines do not seem to match what the data-lines are saying. In the jsfiddle, the green line is set at 1. But the data-lines whose value are 1, are not on that green line.
So, this is pretty much a scale question i guess.
Visual (picasso-esc) representation of what the graph should look like if it worked.
As you want your y domain to be [-2, 2] as opposed to be driven by the data, you can remove a lot of setup and helper functions from your drawGraph function.
After drawing your graph, you can simply loop through the yLines array, and draw a line for each with the specified color, at the specified val according to your yScale.
Update: EDITED: As you will be supplied the values of nominal, upperTolerance, lowerTolerance, innerUpperTolerance, innerLowerTolerance from your endpoint (and they don't need to be calculated from the data on the client side), just feed those values into your data-driven yScale to draw the coloured lines.
Below I have just used the values 1, 1.8, -1.8, but you will receive values that will be more meaningfully tied to your data.
// Setup
const yLines = [{
val: 1,
color: 'green'
},
{
val: 1.8,
color: 'yellow'
},
{
val: -1.8,
color: 'red'
}
]
const margin = {
top: 10,
right: 80,
bottom: 60,
left: 20
};
const strokeWidth = 3;
const pointRadius = 4;
const svgWidth = 600;
const svgHeight = 600;
const width = svgWidth - margin.left - margin.right;
const height = svgHeight - margin.top - margin.bottom;
const stroke = '#2990ea'; // blue
const areaFill = 'rgba(41,144,234,0.1)'; // lighter blue
const format = d3.time.format("%b %e %Y");
const valueFn = function(d) {
return d.value
};
const dateFn = function(d) {
return format.parse(d.name)
};
// select the div and append svg to it
const graph = d3.select('#chart').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.style('overflow', 'visible');
const transformGroup = graph.append('g')
.attr('tranform', `translate(${margin.left}, ${margin.right})`)
// Make a group for yLines
const extraLines = transformGroup.append('g')
.attr('class', 'extra-lines')
// Generate some dummy data
const getData = function() {
let JSONData = [];
for (var i = 0; i < 30; i++) {
JSONData.push({
"name": moment().add(i, 'days').format('MMM D YYYY'),
"value": Math.floor(Math.random() * (Math.floor(Math.random() * 20))) - 10
})
}
return JSONData.slice()
}
const drawGraph = function(data) {
console.log(data)
// Setup y scale
const y = d3.scale.linear()
.domain(d3.extent(data.map((d) => d.value)))
.range([height, 0]);
// Setup y axis
const yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10)
.tickSize(0, 0, 0)
// append group & call yAxis
transformGroup.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + margin.left + ",0)")
.call(yAxis);
// Draw extra coloured lines from yLines array
extraLines.selectAll('.extra-line')
.data(yLines)
.enter()
.append('line')
.attr('class', 'extra-line')
.attr('x1', margin.left)
.attr('x2', svgWidth - margin.right)
.attr('stroke', d => d.color)
.attr('y1', d => y(+d.val))
.attr('y2', d => y(+d.val))
.attr('stroke-width', strokeWidth)
.attr('opacity', 0.5)
// Setup x scale
const x = d3.time.scale()
.domain(d3.extent(data, dateFn))
.range([0, width])
// function for filling area under chart
const area = d3.svg.area()
.x(d => x(format.parse(d.name)))
.y0(height)
.y1(d => y(d.value))
// function for drawing line
const line = d3.svg.line()
.x(d => x(format.parse(d.name)))
.y(d => y(d.value))
const lineStart = d3.svg.line()
.x(d => x(format.parse(d.name)))
.y(d => y(0))
// make the line
transformGroup.append('path')
.attr('stroke', stroke)
.attr('stroke-width', strokeWidth)
.attr('fill', 'none')
.attr('transform', `translate(${margin.left}, ${margin.top})`)
.attr('d', lineStart(data))
.attr('d', line(data))
// fill area under the graph
transformGroup.append("path")
.datum(data)
.attr("class", "area")
.attr('fill', areaFill)
.attr('transform', `translate(${margin.left}, ${margin.top})`)
.attr('d', lineStart(data))
.attr("d", area)
}
drawGraph(getData())
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<div id="chart" style="margin: 0 auto;"></div>
This is a scatterplot displaying beer by alcohol by volume (x) and IBU (y). I cannot get the axis labels to display. I checked the DOM, and the text is in the tree, but for some reason, it is not displaying on the screen.
For the y-label, the margins don't seem to be working, and for the x, it is positioned where I want it to be, but I simply cannot see any text.
const dataFiles = ['https://raw.githubusercontent.com/inspectordanno/beer_components/master/scatterplot/data/beers.csv', 'https://raw.githubusercontent.com/inspectordanno/beer_components/master/scatterplot/data/breweries.csv']; //data files to be parsed
const promises = []; //empty array which will contain the promises
dataFiles.forEach(url => {
promises.push(d3.csv(url)); //this parses each csv file and pushes it to the array
});
//dimensions and margins of plot
const margin = {
top: 10,
right: 30,
bottom: 30,
left: 60
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
padding = 30;
const svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
Promise.all(promises).then(data => { //here, I am combining the two arrays in the promise into one big array of beer objects
data[0].forEach(beer => {
const breweryid = beer.brewery_id;
data[1].forEach(brewery => {
if (parseInt(breweryid) == parseInt(brewery.id)) {
beer.brewery_name = brewery.name;
beer.brewery_city = brewery.city;
beer.brewery_state = brewery.state;
}
});
});
let beers = data[0]; //beer data
const xScale = d3.scaleLinear()
.domain([0, d3.max(beers, d => d.abv)])
.range([padding, width - padding *2]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(beers, d => d.ibu)])
.range([height - padding, padding]);
const xAxis = d3.axisBottom()
.scale(xScale)
.ticks(10) //ask steven
.tickFormat(d3.format(',.1%'));
const yAxis = d3.axisLeft()
.scale(yScale)
.ticks(10); //ask steven
// .tickFormat
svg.selectAll('circle')
.data(beers)
.enter()
.append('circle')
.attr('cx', d => {
return xScale(d.abv);
})
.attr('cy', d => {
return yScale(d.ibu);
})
.attr('r', 1)
.attr('fill', 'steelblue')
.on('mouseover', d => console.log(d))
//Create X axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (height - padding) + ")")
.call(xAxis);
//text label for x axis
svg.append("text")
.attr("transform", `translate(${(width/2)}, ${height + margin.top * 4})`)
.style("text-anchor", "middle")
.text("Alcohol by Volume");
// text label for the y axis
svg.append("text")
.attr("transform", "rotate(-90)")
.attr('y', 0 - margin.left * .75)
.attr('x', 0 - (height/2))
.style("text-anchor", "middle")
.text("IBU")
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
}) //end .then of promise
.catch(err => console.log(err)); // catching error in promises
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="main.js" charset="utf-8"></script>
</body>
</html>
The way you're dealing with the margins, dimensions, paddings and translates makes little sense. For instance, you set the properties of the margin object, but your SVG has a width and height that simply subtracts those values.
I'm not personally neither a user nor a fan of that pattern (that Mike Bostock calls margin convention), but this is how you can do it:
First, set the margins, as you did, and define the width and height. Then, add the margins for setting the SVG dimensions:
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
Finally, append a <g> element, translating it using margin.left and margin.top:
.append("g")
.attr("transform", "translate(" + margin.left + ","+ margin.top + ")");
Here is your code with that change (and changing the translate for the axes as well):
const dataFiles = ['https://raw.githubusercontent.com/inspectordanno/beer_components/master/scatterplot/data/beers.csv', 'https://raw.githubusercontent.com/inspectordanno/beer_components/master/scatterplot/data/breweries.csv']; //data files to be parsed
const promises = []; //empty array which will contain the promises
dataFiles.forEach(url => {
promises.push(d3.csv(url)); //this parses each csv file and pushes it to the array
});
//dimensions and margins of plot
const margin = {
top: 10,
right: 30,
bottom: 200,
left: 100
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
const 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 + ")");
Promise.all(promises).then(data => { //here, I am combining the two arrays in the promise into one big array of beer objects
data[0].forEach(beer => {
const breweryid = beer.brewery_id;
data[1].forEach(brewery => {
if (parseInt(breweryid) == parseInt(brewery.id)) {
beer.brewery_name = brewery.name;
beer.brewery_city = brewery.city;
beer.brewery_state = brewery.state;
}
});
});
let beers = data[0]; //beer data
const xScale = d3.scaleLinear()
.domain([0, d3.max(beers, d => d.abv)])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(beers, d => d.ibu)])
.range([height, 0]);
const xAxis = d3.axisBottom()
.scale(xScale)
.ticks(10) //ask steven
.tickFormat(d3.format(',.1%'));
const yAxis = d3.axisLeft()
.scale(yScale)
.ticks(10); //ask steven
// .tickFormat
svg.selectAll('circle')
.data(beers)
.enter()
.append('circle')
.attr('cx', d => {
return xScale(d.abv);
})
.attr('cy', d => {
return yScale(d.ibu);
})
.attr('r', 1)
.attr('fill', 'steelblue')
.on('mouseover', d => console.log(d))
//Create X axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (height) + ")")
.call(xAxis);
//text label for x axis
svg.append("text")
.attr("transform", `translate(${(width/2)}, ${height + margin.top * 4})`)
.style("text-anchor", "middle")
.text("Alcohol by Volume");
// text label for the y axis
svg.append("text")
.attr("transform", "rotate(-90)")
.attr('y', 0 - margin.left * .75)
.attr('x', 0 - (height/2))
.style("text-anchor", "middle")
.text("IBU")
//Create Y axis
svg.append("g")
.attr("class", "axis")
.call(yAxis);
}) //end .then of promise
.catch(err => console.log(err)); // catching error in promises
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="main.js" charset="utf-8"></script>
</body>
</html>
I'm using this mbostock example as a guide, but it's in d3 v3. I've read that in v4, selections have changed, but this doesn't seem to be my issue since the data never changes - this is a static chart (for now).
I've got an array of objects, in which I expect to create a group (g) for each object, and two rectangles (rect) for each field1 and field2 of that object that correlate to the date. I'm able to create the groups, but when I .data(d => d) while attempting to make the rectangles, the enter group appears to be empty.
Can someone explain why this is?
jsfiddle (not working)
UPDATE: jsfiddle (working) – thanks to #gerardo-furtado
var models = [
{
"date":"2016-10-13T07:00:00.000Z",
"field1":19,
"field2":83
},
{
"date":"2016-10-14T07:00:00.000Z",
"field1":67,
"field2":93
},
{
"date":"2016-10-15T07:00:00.000Z",
"field1":10,
"field2":56
},
{
"date":"2016-10-16T07:00:00.000Z",
"field1":98,
"field2":43
}
];
models = models.map(i => {
i.date = new Date(i.date);
return i;
});
var container = d3.select('body'),
width = 500,
height = 300,
margin = {top: 30, right: 20, bottom: 30, left: 50},
barPadding = .2,
axisTicks = {qty: 5, outerSize: 0, dateFormat: '%m-%d'};
var svg = container
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
var xScale0 = d3.scaleBand().range([0, width - margin.left - margin.right]).padding(barPadding);
var xScale1 = d3.scaleBand();
var yScale = d3.scaleLinear().range([height - margin.top - margin.bottom, 0]);
var xAxis = d3.axisBottom(xScale0).tickFormat(d3.timeFormat(axisTicks.dateFormat)).tickSizeOuter(axisTicks.outerSize);
var yAxis = d3.axisLeft(yScale).ticks(axisTicks.qty).tickSizeOuter(axisTicks.outerSize);
xScale0.domain(models.map(d => d.date));
xScale1.domain(['field1', 'field2']).range([0, xScale0.bandwidth()]);
yScale.domain([0, d3.max(models, d => d.field1 > d.field2 ? d.field1 : d.field2)]);
var date = svg.selectAll(".date")
.data(models)
.enter().append("g")
.attr("class", "date")
.attr("transform", d => `translate(${xScale0(d.date)},0)`);
/* Add field1 bars */
var rect = date.selectAll(".bar")
.data(d => d) // FIXME: the .enter() group seems to contain nothing
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", d => { xScale1('field1') })
.attr("y", d => { yScale(d.field1) })
.attr("width", xScale1.bandwidth())
.attr("height", d => {
return height - margin.top - margin.bottom - yScale(d.field1)
});
/* Add field2 bars */
// var rect = date.selectAll(".bar")
// .data(d => d)
// .enter()
// .append("rect")
// .attr("class", "bar")
// .attr("x", d => { xScale1('field2') })
// .attr("y", d => { yScale(d.field2) })
// .attr("width", xScale1.bandwidth())
// .attr("height", d => {
// return height - margin.top - margin.bottom - yScale(d.field1)
// });
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0,${height - margin.top - margin.bottom})`)
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
In D3, data has to be an array. If you look at Bostock's example, layers is an array of arrays. This is the result of console.log(layers):
Array[4]
0:Array[58]
1:Array[58]
2:Array[58]
3:Array[58]
Thus, because layers is an array of arrays, when later in the code Bostock writes this:
.data(function(d) { return d; })
He is passing an array to data, which works.
However, in your code, you're passing a object to data in your enter selection for rect:
Object {date: Thu Oct 13 2016 18:00:00 GMT+1100, field1: 19, field2: 83}
Solution: pass an array to data. This is the only change I made:
.data(d => [d])
Now you have an array:
[{date:Thu Oct 13 2016 18:00:00 GMT+1100, field1: 19, field2: 83}]
Here is your updated fiddle: https://jsfiddle.net/96d3wtys/
PS: you'll have to adjust the y and height of your bars.