I'm writing a simple bar chart that draws a rect element for each piece of data. This is a part of my code without scales, which works fine:
const rect = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", (d, i) => (i*4))
.attr("y", (d) => h - d[1]/50)
However, if I add y-scale, my bars flip over, and if I add x-scale, I can only see one bar. Here's my code with scales:
const xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, (d) => d[0])])
.range([padding, w - padding]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, (d) => d[1])])
.range([h - padding, padding]);
//some other code
const rect = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", (d, i) => xScale(i*4))
.attr("y", (d) => yScale(d[1]/50))
Here's my CodePen with scales, that's what it looks like: http://codepen.io/enk/pen/vgbvWq?editors=1111
I'd be really greatful if somebody told me what am I doing wrong.
D3 scales are not messing with your chart. The problem here is simple.
Right now, this is your dataset:
[["1947-01-01", 243.1], ...]
As you can see, d[0] is a string (representing a date), not a number. And, in d3.scaleLinear, the domain is an array with two values only.
The solution here is using a scaleBand instead:
const xScale = d3.scaleBand()
.domain(dataset.map(d => d[0]))
.range([padding, w - padding]);
And changing the code of the rectangles to actually use the scales (I made several changes, check them):
const rect = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width", xScale.bandwidth())
.attr("x", (d => xScale(d[0]))) //xScale
.attr("height", (d => h - yScale(d[1]))) //yScale
.style("y", (d => yScale(d[1])))
.attr("data-date", (d) => d[0])
.attr("data-gdp", (d) => d[1])
.attr("class", 'bar')
Here is your updated CodePen: http://codepen.io/anon/pen/NdJGdo?editors=0010
Related
I'm trying to plot a simple bar chart with date as x axis and temperature value as y axis. I'm using d3 library.
Created the xScale ans yScale as
const xScale = d3
.scaleTime()
.domain(d3.extent(dataset, xAccessor))
.range([0, dimensions.boundWidth]);
const yScale = d3
.scaleLinear()
.domain([0, d3.max(dataset, yAccessor)])
.range([dimensions.boundHeight, 0]);
Set the accessor functions as
const dateParse = d3.timeParse("%Y-%m-%d");
const xAccessor = (d) => dateParse(d["date"]);
const yAccessor = (d) => d["temperatureMax"];
And drew the rectangles using
const barsGroup = bounds.append("g");
const barGroups = barsGroup
.selectAll("mybars")
.data(dataset.slice(0, 10))
.join("g");
const barRects = barGroups
.append("rect")
.attr("x", (d) => xScale(xAccessor(d)))
.attr("y", (d) => yScale(yAccessor(d)))
.attr("width", "10")
.attr("height", (d) => dimensions.boundHeight - yScale(yAccessor(d)))
.attr("fill", "darkgray");
I'm trying to find out how to set the width attribute. Here I set it as a constant "10" just to see something on screen. Also trying to find out how to set proper spacing between each bars. I have a set up a codesandbox.io with the above set up. Any suggestions would be really helpful.
I am working with line-area chart using d3. But somehow the chart is not showing and there's no error in the console. I'm unable to figure out the issue. It will be helpful if anyone can help me with that.
I've added the entire code in Codepen. This is the link
Here's the sample code for ref:
const svg = d3.select("#chart")
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
svg.append('defs');
svg.call(createGradient);
svg.call(createGlowFilter);
const xScale = d3.scaleTime()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.x)),
d3.max(parsedData, d => d3.max(d.values, v => v.x))
])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.y)),
d3.max(parsedData, d => d3.max(d.values, v => v.y))
])
.range([height, 0]);
const line = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveCatmullRom.alpha());
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', (d) => {
const lineValues = line(d.values).slice(1);
const splitedValues = lineValues.split(',');
return `M0,${height},${lineValues},l0,${height - splitedValues[splitedValues.length - 1]}`
})
.style('fill', 'url(#gradient)')
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', d => line(d.values))
.attr('stroke-width', '2')
.style('fill', 'none')
.style('filter', 'url(#glow)')
.attr('stroke', '#e4647f');
Thanks in advance!
I've created a fork of your code that has the line and area showing.
Here are the major changes.
First, I parsed the strings into dates:
const parseTime = d3.timeParse("%b %Y");
const parsedData = bar_chart_data.map(val => ({
close: val.y,
date: parseTime(val.x)
}));
parsedData is now an array of objects, so we can update the scales accordingly:
const xScale = d3.scaleTime()
// extent returns an array containing the min and max
.domain(d3.extent(parsedData, d => d.date))
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(parsedData, d => d.close)])
.range([height, 0]);
Next, I switched d3.line() to d3.area(), since you want to draw an area underneath the line. This will save you from having to manually construct the "d" attribute for the area.
const area = d3.area()
.x(d => xScale(d.date))
.y1(d => yScale(d.close)) // top line
.y0(yScale(0)) // base line, always at 0
.curve(d3.curveCatmullRom.alpha(0.5));
For drawing the line and area, we do not need to do a data join, we can just use append() and pass the parsedData array to the area generator for the area and to area.lineY1() for the top line:
svg.append("path")
.attr("d", area(parsedData))
.style("fill", "url(#gradient)");
svg.append("path")
.attr("d", area.lineY1()(parsedData))
.attr("stroke-width", "2")
.style("fill", "none")
.style("filter", "url(#glow)")
.attr("stroke", "#e4647f");
I'm trying to create integer bands in the y axes.
Have tried changing .scaleband to .scalelinear and .ticks "arbitraryMetric" is stored as an integer.
Code:
const categories = vizData.map(d => d.arbitraryMetric);
const yScale = d3
.scaleBand()
.domain(categories)
.range([0, vizHeight - timelineMargin.bottom]);
const labels = vizCanvas
.selectAll('text')
.data(categories)
.enter()
.append('text')
.style('font-family', style.fontFamily)
.style('fill', '#3C4043')
.attr('x', timelineMargin.left - 10)
.attr('y', d => yScale(d) + yScale.bandwidth() / 2)
.attr('text-anchor', 'end')
.text(d => d);
This following solution will do the trick - credit to this answer
const yAxisTicks = yScale.ticks()
.filter(tick => Number.isInteger(tick));
const yAxis = d3.axisLeft(yScale)
.tickValues(yAxisTicks)
.tickFormat(d3.format('d'));
I ran into a common problem while making a bar graph in d3: my y-axis was upside down. After some googling, I found out that the best way to fix this is by reversing the y-domain. The only problem is, when I did that, the bars on my graph switched positions, so the largest was at the beginning instead of the end. I need the y-axis to be correct, without changing my bars.
Bars in correct positions, but the y-axis is upside-down
Bars are incorrect, but the y-axis is right-side-up
Here is the code: https://codepen.io/lucassorenson/pen/rPRadR?editors=0010
const yScale = d3.scaleLinear()
.domain([0, d3.max(json, (d) => d[1])])
.range([h - padding, padding]);
const yAxis = d3.axisLeft(yScale);
This is the code that I changed. If you reverse the range ([padding, h - padding]), the bars are correct but the axis is not.
The fix is simply to exchange the callback functions of the attributes "height" and "y".
Your code was:
...
svg.selectAll('rect')
.data(json)
.enter()
.append('rect')
.attr('width', 3)
.attr('height', (d) => yScale(d[1]))
.attr('x', function(d, i){
return i*4 + padding
})
.attr('y', function(d){
return h - yScale(d[1]) - padding
})
change it to:
...
svg.selectAll('rect')
.data(json)
.enter()
.append('rect')
.attr('width', 3)
.attr('y', (d) => yScale(d[1]))
.attr('x', function(d, i){
return i*4 + padding
})
.attr('height', function(d){
return h - yScale(d[1]) - padding
})
With the attribute "y" you typically set the upper edge of the rectangle and with "height" you denote how far it goes down.
Have spent the last 2 days looking through stackoverflow and online examples as to why my charts aren't displaying properly.
I'm sure I'm missing something in terms of the scaling portion of the code. If I copy the dark part at the bottom of the x-Axis on the chart to notepad it gives me all of the x-axis elements.
Can anyone point me in the right direction?
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded',function(){
req.open("GET",'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json',true);
req.send();
req.onload=function(){
json=JSON.parse(req.responseText);
document.getElementsByClassName('title')[0].innerHTML=json.name;
dataset=json.data;
const w = 500;
const h = 300;
const padding = 10;
// create an array with all date names
const dates = dataset.map(function(d) {
return d[0];
});
const xScale = d3.scaleBand()
.rangeRound([padding, w-padding])
.padding([.02])
.domain(dates);
console.log("Scale Bandwidth: " + xScale.bandwidth());
const yScale = d3.scaleLinear()
.rangeRound([h-padding, padding])
.domain(0,d3.max(dataset, (d)=>d[1]));
console.log("Dataset Max Height: " + d3.max(dataset, (d)=>d[1]));
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
const svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width",(d,i)=>xScale.bandwidth())
.attr("height",(d,i)=>(h-yScale(d[1])))
.attr("x", (d,i)=>xScale(d[0]))
.attr("y", (d,i)=>yScale(d[1]))
.attr("fill", "navy")
.attr("class", "bar");
};
});
</script>
<h1 class="title">Title Will Go Here</h1>
</body>
D3 now uses Promises instead of asynchronous callbacks to load data. Promises simplify the structure of asynchronous code, especially in modern browsers that support async and await.
Changes in D3 5.0
Also, you are right in that your yScale is broken. Linear scales need a range and a domain, each being passed a 2 value array.
const yScale = d3.scaleLinear()
.range([h - padding, padding])
.domain([0, d3.max(dataset, (d) => d[1])]);
document.addEventListener('DOMContentLoaded', async function() {
const res = await d3.json("https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json");
//console.log(res.data)
const dataset = res.data
const w = 500;
const h = 300;
const padding = 10;
// create an array with all date names
const dates = dataset.map(function(d) {
return d[0];
});
const max = d3.max(dataset, function(d) { return d[1]} )
const xScale = d3.scaleBand()
.rangeRound([0, w])
.padding([.02])
.domain(dates);
console.log("Scale Bandwidth: " + xScale.bandwidth());
const yScale = d3.scaleLinear()
.range([h - padding, padding])
.domain([0, d3.max(dataset, (d) => d[1])]);
console.log("Dataset Max Height: " + d3.max(dataset, (d) => d[1]));
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
const svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width", (d, i) => xScale.bandwidth())
.attr("height", (d, i) => (h - yScale(d[1])) )
.attr("x", (d, i) => xScale(d[0]))
.attr("y", (d, i) => yScale(d[1]))
.attr("fill", "navy")
.attr("class", "bar");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>
Codepen