I can't figure out why my elements are bleeding into the bottom padding. It is preventing me from having a elements. I am sure it is something simple, it always is. I just can't figure out what's wrong. I have tried changing the height and the scales, but it just changes things I don't want changed. I keep looking at tutorials on scales and I haven't found anything that seems off. If I get I have the project on codepen, here is the link: https://codepen.io/critchey/pen/YzEXPrP
Here is a screen shot to know what I am talking about:
Here is the javascript:
//Mapping dataset to its x and y axes
const xData = dataset.map(d => {
let date = new Date(d[0]);
return date;
});
const xDates = xData.map(d => {
let dateFormatted = d.toLocaleString("default", {month: "short"}) + ' ' + d.getDate() + ', ' + d.getFullYear()
return dateFormatted;
});
const yData = dataset.map(d => d[1]);
//Variable for use inside D3
const h = 400;
const w = 800;
const pad = 40;
//Scales for the SVG element
const xDateScale = d3.scaleTime()
.domain([0, w])
.range([pad, w - pad]);
const xScale = d3.scaleLinear()
.domain([0, yData.length])
.range([pad, w - pad]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(yData, (d) => d)])
.range([h - pad, pad]);
//Declaring the SVG element
const svg = d3.select('#svg-container')
.append('svg')
.attr('width', w)
.attr('height', h)
//Declaring each bar
svg.selectAll('rect')
.data(yData)
.enter()
.append('rect')
.attr('x', (d, i) => xScale(i))
.attr('y', (d, i) => yScale(d))
.attr("width", w / (yData.length - pad * 2))
.attr("height", (d, i) => d)
.attr("class", "bar")
.append('title')
.text((d, i) => 'GDP: ' + d + ' | Date: ' + xDates[i])
//Axes Declarations
const xAxis = d3.axisBottom(xDateScale);
const yAxis = d3.axisLeft(yScale);
svg.append("g")
.attr("transform", "translate(0," + (h - pad / 2) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(" + pad + ",0)")
.call(yAxis)
The problem is that you're not using the correct value with the height attribute for your rects
Just make the following change and it should work:
.attr("height", (d, i) => yScale(0) - yScale(d))
Related
I started the D3.js challenge on FreeCodeCamp, the problem is that I solved it with the chart but it only gives me a display on the rectum, only one with the width and height that it I put, I'll show the code below.
The entire code on
<script>
//set d3
var w = 1000, h = 500;
var padding = 50;
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h)
//title
svg.append('text')
.attr('x', w / 2)
.attr('y', 50)
.text('United States GDP')
fetch('https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json')
.then((result)=>result.json())
.then((data)=>{
var the_data = data['data']
//get vals
var get_max = d3.max(data['data'])
var get_mix = d3.min(data['data'])
//for x
var max_x = Number(get_max[0].split('-')[0])
var min_x = Number(get_mix[0].split('-')[0])
//for y
var max_y = get_max[1]
var min_y = get_mix[1]
var xScale = d3.scaleLinear()
.domain([min_x, max_x])
.range([padding, w-padding])
var yScale = d3.scaleLinear()
.domain([min_y, max_y])
.range([h-padding, padding])
//the_chars
for(var i in the_data){
var get_year = Number(the_data[i][0].split('-')[0])
the_data[i][0] = get_year
}
svg.selectAll('rect')
.data(the_data)
.enter()
.append('rect')
.attr("x", (d) => { xScale(d[0]) })
.attr('y', (d)=>{ yScale(d[1]) })
.attr("width", 200)
.attr("height", 20)
//axis
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
//display axis
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append('g')
.attr('transform', 'translate(' + padding + ', 0)')
.call(yAxis)
})
Now, what I need to do to display the charts!
I mention that the script tags are embedded in the body
Problem: Arrow functions without a return value. Solution: Instead use an explicit or an implicit return.
.attr("x", (d) => { xScale(d[0]) }) // returns undefined
.attr("x", (d) => xScale(d[0])) // implicit return
.attr("x", (d) => { return xScale(d[0]) }) // explicit return
Problem: Fixed height value. Solution Evaluate the height of each based on the GDP value (d[1]) instead.
.attr('height', 20) // fixed height
.attr('height', d => yScale(min_y) - yScale(d[1]))
// subtract from min range to account for padding and inverted y coordinates in SVG
Full solution in this codepen
Trying to build a bar chart and, I don't know why, it's only showing the last value for each year and not all the values in both arrays, and that's what I thought that it was supposed to happen. How can I fix that?
let url = "https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json";
const padding = 50;
const height = 460;
const width = 940;
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var arr = [];
var years = [];
d3.json(url, function(data) {
for (let i = 0; i < data.data.length; i++) {
arr[i] = data.data[i];
years[i] = parseInt(data.data[i][0].slice(0,4));
}
const yScale = d3.scaleLinear()
.domain([0, d3.max(arr, (d) => d[1])])
.range([height - padding, padding]);
const xScale = d3.scaleLinear()
.domain([d3.min(years, d => d), d3.max(years, (d) => d)])
.range([padding, width - padding]);
let bandScale = d3.scaleBand().domain(years, d => d).range([padding, width - padding]);
const xAxis = d3.axisBottom(bandScale)
const yAxis = d3.axisLeft(yScale);
svg.append("g")
.attr("transform", "translate(0," + (height - padding) + ")")
.call(xAxis);
svg.append('g')
.attr('transform', 'translate(' + padding + ', 0)')
.call(yAxis)
svg.selectAll('rect')
.data(arr)
.enter()
.append('rect')
.attr('fill', 'blue')
.attr('height', d => height - padding - yScale(d[1]))
.attr('width', d => bandScale.bandwidth())
.attr('x', (d, i) => (bandScale(years[i])))
.attr('y', d => yScale(d[1]))
.append('title')
.text((d, i) => years[i] + ': ' + d[1])
});
<script src="https://d3js.org/d3.v4.min.js"></script>
I'm really having trouble with D3 and need some help changing my existing barchart to be a grouped barchart The barchart is being used within a tooltip and currently looks like:
Each colour represents a sector of industry (pink = retail, teal = groceries...etc).
I need to change the bar chart so that it compares the percentage change in each industry with the world average percentage change in this industry.
At the moment the bar chart is being created from an array of data. I also have an array with the world percentage values.
So imagine:
countryData = [10,-20,-30,-63,-23,20],
worldData = [23,-40,-23,-42,-23,40]
Where index 0 = retail sector, index 1 = grocery sector, etc.
I need to plot a grouped barchart comparing each sector to the world average (show the world average in red). This is a bit tricky to explain so I drew it for you (...excuse the shoddy drawing).
Please can someone help me change my existing tooltip?
Here's the current code. If you want to simulate the data values changing.
If you want to scrap my existing code that's fine.
.on('mouseover', ({ properties }) => {
// get county data
const mobilityData = covid.data[properties[key]] || {};
const {
retailAverage,
groceryAverage,
parksAverage,
transitAverage,
workplaceAverage,
residentialAverage,
} = getAverage(covid1);
let avgArray = [retailAverage, groceryAverage, parksAverage, transitAverage, workplaceAverage, retailAverage];
let categoriesNames = ["Retail", "Grocery", "Parks", "Transit", "Workplaces", "Residential"];
// create tooltip
div = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
div.html(properties[key]);
div.transition()
.duration(200)
.style('opacity', 0.9);
// calculate bar graph data for tooltip
const barData = [];
Object.keys(mobilityData).forEach((industry) => {
const stringMinusPercentage = mobilityData[industry].slice(0, -1);
barData.push(+stringMinusPercentage); // changing it to an integer value, from string
});
//combine the two lists for the combined bar graph
var combinedList = [];
for(var i = 0; i < barData.length; i++) {
const stringMinusPercentage2 = +(avgArray[i].slice(0, -1));
const object = {category: categoriesNames[i], country: barData[i], world: stringMinusPercentage2}
combinedList.push(object); //Push object into list
}
console.log(combinedList);
// barData = barData.sort(function (a, b) { return a - b; });
// sort into ascending ^ keeping this in case we need it later
const height2 = 220;
const width2 = 250;
const margin = {
left: 50, right: 10, top: 20, bottom: 15,
};
// create bar chart svg
const svgA = div.append('svg')
.attr('height', height2)
.attr('width', width2)
.style('border', '1px solid')
.append('g')
// apply the margins:
.attr('transform', `translate(${[`${margin.left},${margin.top}`]})`);
const barWidth = 30; // Width of the bars
// plot area is height - vertical margins.
const chartHeight = height2 - margin.top - margin.left;
// set the scale:
const yScale = d3.scaleLinear()
.domain([-100, 100])
.range([chartHeight, 0]);
// draw some rectangles:
svgA
.selectAll('rect')
.data(barData)
.enter()
.append('rect')
.attr('x', (d, i) => i * barWidth)
.attr('y', (d) => {
if (d < 0) {
return yScale(0); // if the value is under zero, the top of the bar is at yScale(0);
}
return yScale(d); // otherwise the rectangle top is above yScale(0) at yScale(d);
})
.attr('height', (d) => Math.abs(yScale(0) - yScale(d))) // the height of the rectangle is the difference between the scale value and yScale(0);
.attr('width', barWidth)
.style('fill', (d, i) => colours[i % 6]) // colour the bars depending on index
.style('stroke', 'black')
.style('stroke-width', '1px');
// Labelling the Y axis
const yAxis = d3.axisLeft(yScale);
svgA.append('text')
.attr('class', 'y label')
.attr('text-anchor', 'end')
.attr('x', -15)
.attr('y', -25)
.attr('dy', '-.75em')
.attr('transform', 'rotate(-90)')
.text('Percentage Change (%)');
svgA.append('g')
.call(yAxis);
})
.on('mouseout', () => {
div.style('opacity', 0);
div.remove();
})
.on('mousemove', () => div
.style('top', `${d3.event.pageY - 140}px`)
.style('left', `${d3.event.pageX + 15}px`));
svg.append('g')
.attr('transform', 'translate(25,25)')
.call(colorLegend, {
colorScale,
circleRadius: 10,
spacing: 30,
textOffset: 20,
});
};
drawMap(svg1, geoJson1, geoPath1, covid1, key1, 'impact1');
drawMap(svg2, geoJson2, geoPath2, covid2, key2, 'impact2');
};
In short I would suggest you to use two Band Scales for x axis. I've attached a code snippet showing the solution.
Enjoy ;)
//Assuming the following data final format
var finalData = [
{
"groupKey": "Retail",
"sectorValue": 70,
"worldValue": 60
},
{
"groupKey": "Grocery",
"sectorValue": 90,
"worldValue": 90
},
{
"groupKey": "other",
"sectorValue": -20,
"worldValue": 30
}
];
var colorRange = d3.scaleOrdinal().range(["#00BCD4", "#FFC400", "#ECEFF1"]);
var subGroupKeys = ["sectorValue", "worldValue"];
var svg = d3.select("svg");
var margin = {top: 20, right: 20, bottom: 30, left: 40};
var width = +svg.attr("width") - margin.left - margin.right;
var height = +svg.attr("height") - margin.top - margin.bottom;
var container = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// The scale spacing the groups, your "sectors":
var x0 = d3.scaleBand()
.domain(finalData.map(d => d.groupKey))
.rangeRound([0, width])
.paddingInner(0.1);
// The scale for spacing each group's bar, your "sector bar":
var x1 = d3.scaleBand()
.domain(subGroupKeys)
.rangeRound([0, x0.bandwidth()])
.padding(0.05);
var yScale = d3.scaleLinear()
.domain([-100, 100])
.rangeRound([height, 0]);
//and then you will need to append both, groups and bars
var groups = container.append('g')
.selectAll('g')
.data(finalData, d => d.groupKey)
.join("g")
.attr("transform", (d) => "translate(" + x0(d.groupKey) + ",0)");
//define groups bars, one per sub group
var bars = groups
.selectAll("rect")
.data(d => subGroupKeys.map(key => ({ key, value: d[key], groupKey: d.groupKey })), (d) => "" + d.groupKey + "_" + d.key)
.join("rect")
.attr("fill", d => colorRange(d.key))
.attr("x", d => x1(d.key))
.attr("width", (d) => x1.bandwidth())
.attr('y', (d) => Math.min(yScale(0), yScale(d.value)))
.attr('height', (d) => Math.abs(yScale(0) - yScale(d.value)));
//append x axis
container.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x0));
//append y axis
container.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale))
.append("text")
.attr("x", 2)
.attr("y", yScale(yScale.ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.text("Values");
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg width="600" height="400"></svg>
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
QUESTION:
Why are there no ticks on my X (time) axis xAxis?
CODE:
<script type="text/javascript">
d3.json("https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/GDP-data.json", function(error, json) {
if (error) {
return console.warn(error);
}
visualizeIt(json);
});
function visualizeIt(data) {
const dataset = data.data;
const margin = {
top: 10,
right: 6,
bottom: 20,
left: 70
}
const w = 900 - margin.left - margin.right;
const h = 500 - margin.top - margin.bottom;
const barWidth = Math.ceil(w / dataset.length);
const format = d3.timeFormat("%Y-%m");
const mindate = dataset[0][0];
const maxdate = dataset[274][0];
const xScale = d3.scaleTime()
.domain([mindate, maxdate])
.range([0, w]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, (d) => d[1])])
.range([h, 0]);
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale)
const svg = d3.select("#results")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom);
svg.append("g")
.attr("transform", "translate(50," + (h+margin.top) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(50," + margin.top + ")")
.call(yAxis);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", (d,i) => 50+barWidth*i)
.attr("y", (d, i) => yScale(d[1]) + margin.top)
.attr("width", barWidth)
.attr("height", (d, i) => h - yScale(d[1]))
.attr("fill", "navy")
.attr("class", "bar");
}
</script>
There are some problems in your code, but I'll address only those related to the x-axis ticks:
You want to parse the strings and return dates, not the other way around. Thus, instead of using timeFormat, your conts format should use timeParse:
const format = d3.timeParse(specifier)
That brings us to the second problem: the specifier in your const format is wrong. You're missing %d here, which is the day, since your strings are like this: "1947-01-01". So, it should be:
const format = d3.timeParse("%Y-%m-%d")
You have to use the parser in your dates:
const mindate = format(dataset[0][0]);
Do the same for maxdate.
Here is your updated CodePen: https://codepen.io/anon/pen/GmjRzY?editors=1010