Related
I'm trying to create an interactive bar chart of the top Forbes 100 companies, with buttons to change between sales and profit.
The first issue I'm having is with the domain:
x.domain([0, d3.max(data, d => d[xValue])])
Error says "data not defined"
but I defined it here:
d3.csv("data/data_clean.csv").then(data => {
data.forEach(d => {
d.sales_usd_billion = Number(d.sales_usd_billion)
d.profit_usd_billion = Number(d.profit_usd_billion)
})
data snapshot:
rank,company,country,sales_usd_billion,sales_unit,profit_usd_billion,profit_unit,assets_usd_billion,market_usd_billion,sales_usd,profit_usd,assets_usd
1,Berkshire Hathaway,United States,276.09,B,89.8,B,958.78,741.48,276.09,89.8,958.78
2,ICBC,China,208.13,B,54.03,B,5518.51,214.43,208.13,54.03,5518.51
3,Saudi Arabian Oil Company (Saudi Aramco),Saudi Arabia,400.38,B,105.36,B,576.04,2292.08,400.38,105.36,576.04
4,JPMorgan Chase,United States,124.54,B,42.12,B,3954.69,374.45,124.54,42.12,3954.69
5,China Construction Bank,China,202.07,B,46.89,B,4746.95,181.32,202.07,46.89,4746.95
6,Amazon,United States,469.82,B,33.36,B,420.55,1468.4,469.82,33.36,420.55
7,Apple,United States,378.7,B,100.56,B,381.19,2640.32,378.7,100.56,381.19
8,Agricultural Bank of China,China,181.42,B,37.38,B,4561.05,133.38,181.42,37.38,4561.05
9,Bank of America,United States,96.83,B,31,B,3238.22,303.1,96.83,31,3238.22
10,Toyota Motor,Japan,281.75,B,28.15,B,552.46,237.73,281.75,28.15,552.46
11,Alphabet,United States,257.49,B,76.03,B,359.27,1581.72,257.49,76.03,359.27
12,Microsoft,United States,184.9,B,71.19,B,340.39,2054.37,184.9,71.19,340.39
13,Bank of China,China,152.43,B,33.57,B,4192.84,117.83,152.43,33.57,4192.84
14,Samsung Group,South Korea,244.16,B,34.27,B,358.88,367.26,244.16,34.27,358.88
FULL CODE:
//Forbes companies bar chart
//set up chart area
const MARGIN = { LEFT: 250, RIGHT: 10, TOP: 50, BOTTOM: 100 }
const WIDTH = 1000 - MARGIN.LEFT - MARGIN.RIGHT
const HEIGHT = 1100 - MARGIN.TOP - MARGIN.BOTTOM
const svg = d3.select("#chart-area").append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM)
const g = svg.append("g")
.attr("transform", `translate(${MARGIN.LEFT}, ${MARGIN.TOP})`)
// X label
g.append("text")
.attr("class", "x axis-label")
.attr("x", WIDTH / 2)
.attr("y", HEIGHT + 50)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
// Y label
const yLabel = g.append("text")
.attr("class", "y axis-label")
.attr("x", - (HEIGHT / 2))
.attr("y", -200)
.attr("font-size", "20px")
.attr("text-anchor", "middle")
.attr("transform", "rotate(-90)")
.text("Company")
//scales
const x = d3.scaleLinear()
.range([0, WIDTH])
const y = d3.scaleBand()
.range([HEIGHT, 0])
//axis generators
const xAxisCall = d3.axisBottom()
const yAxisCall = d3.axisLeft()
//axis groups
const xAxisGroup = g.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${HEIGHT})`)
const yAxisGroup = g.append("g")
.attr("class", "y axis")
//event listeners
$("#var-select").on("change", update)
d3.csv("data/data_clean.csv").then(data => {
data.forEach(d => {
d.sales_usd_billion = Number(d.sales_usd_billion)
d.profit_usd_billion = Number(d.profit_usd_billion)
})
update()
})
function update() {
const t = d3.transition().duration(750)
//filter based on selections
const xValue = $("#var-select").val()
x.domain([0, d3.max(data, d => d[xValue])])
y.domain(data.map(d => d.company))
data.sort(function(a, b) {
return b.rank - a.rank;
})
//update axes
xAxisCall.scale(x)
xAxis.transition(t).call(xAxisCall)
yAxisCall.scale(y)
yAxis.transition(t).call(yAxisCall)
//***Tooltips */
//*** --- */
rects.enter().append("rect")
.attr("y", d => y(d.company) +3)
.attr("x", 0)
.attr("width", d => x(d[value]))
.attr("height", d => 4)
You need to pass data in your update method and change function definition as function update(data). Its a simple scope problem, I would suggest that try debugging the code and then ask for help here.To learn more about debugging, follow a javascript debugging tutorial
I am trying to implement a line chart through this function. when I try to add the path, the path alone is not getting displayed. any help would be appreciated. (last 10 lines of the code add the path to the graph)
function drawLineChart(country) {
console.log(countryValueMap[country])
var margin = {top: 60, right: 110, bottom: 50, left: 50},
width = 800 - margin.left - margin.right + 50,
height = 500 - margin.top - margin.bottom + 50;
// document.getElementById("my_dataviz").innerHTML = "";
const lineSvg = d3.select("#linechart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const x = d3.scaleTime()
.range([0 , width])
.domain([new Date(1958,0,1), new Date(2011,0,1)])
lineSvg.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x).ticks(5).tickFormat(d => d.getFullYear()))
const y = d3.scaleLinear()
.domain([0,countryMaxValueMap[country]])
.range([height, 0])
lineSvg.append("g")
.call(d3.axisRight(y).tickSize(width)).call(g => g.select(".domain")
.remove())
.call(g => g.selectAll(".tick:not(:first-of-type) line")
.attr("stroke-opacity", 0.5)
.attr("stroke-dasharray", "2,2"))
.call(g => g.selectAll(".tick text")
.attr("x", 4)
.attr("dy", -4));
lineSvg.append("path")
.datum(countryValueMap[country])
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.year) })
.y(function(d) { return y(d.gdp) })
)
}
When I try to execute the above code, this is the output I am getting.
I tried logging the value of d3.year and d3.gdp and the variable works fine.
Given that the line is at 1970 (the start of epoch time), it looks like something is wrong with the how the years are represented in your data. Since you are using a scaleTime, d.year must be a date object. It can't just be an integer representing the year.
I am working with a tidy-long data structure with three columns: date, ID, num_orders.
date ID num_orders
"2018-08-22" 1 3
"2018-08-23" 7 1
"2018-08-23" 10 1
"2018-08-23" 17 1
"2018-08-23" 19 1
.
.
.
I would like to plot a line for each ID with date and num_orders as the x- and y-axis respectively, using D3.js. I am using this as a model for what I am doing, but in that code, the author is using the nest() function, which is no longer used in v6 of D3; the method used now is group(). So my code now is:
const margin = { top: 10, right: 30, bottom: 30, left: 60 };
const width = 1000 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;
const 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})`);
d3.json("./data.json")
.then( function(data) {
const grouped_data = d3.group(data, d => d.ID);
parseDate = d3.timeParse('%Y-%m-%d')
const xScale = d3.scaleTime()
.domain(d3.extent(data, d => parseDate(d.date)))
.range([0, width]);
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.num_orders)])
.range([height, 0]);
svg.append('g')
.call(d3.axisLeft(yScale));
const myColor = d3.scaleOrdinal()
.domain(grouped_data.keys())
.range(d3.schemeSet3);
svg.selectAll('.line')
.data(grouped_data)
.enter()
.append('path')
.attr('fill', 'none')
.attr('stroke', d => myColor(d.keys))
.attr('stroke-width', 1.5)
.attr('d', function(d){
return d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.num_orders))
(d.values);
});
} )
So far I can get the axis with tick-marks to show up, but none of the lines are showing up, which makes me think the problem is in the final svg.selectAll('.line') statement. I'm pretty new to D3, so any guidance on this problem is appreciated. (And if you have any overall suggestions for my code, I also welcome this.)
d3.line() is accepting arrays only, while d.values() is an iterator.
By converting it into an array the problem is solved.
Notice that, on the snippet I removed the parseDate because I am generating data as Dates.
You most likely will need to keep the parseDate
const margin = { top: 10, right: 30, bottom: 30, left: 60 };
const width = 1000 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;
var x = d3.timeDays(new Date(2020, 06, 01), new Date(2020, 10, 30));
var y = Array.from({length: x.length}, Math.random).map(n => Math.floor(n * 10) + 5);
var data = x.map((v, i) => {
return {
"date": v,
"id": Math.floor(Math.random() * (10 + 1)),
"num_orders": y[i]
};
});
const 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})`);
const grouped_data = d3.group(data, d => d.id);
parseDate = d3.timeParse('%Y-%m-%d');
const xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width]);
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.num_orders)])
.range([height, 0]);
svg.append('g')
.call(d3.axisLeft(yScale));
const myColor = d3.scaleOrdinal()
.domain(grouped_data.keys())
.range(d3.schemeSet3);
const line = d3.line()
.x(d => { return xScale(d.date); })
.y(d => yScale(d.num_orders));
svg.selectAll('.line')
.data(grouped_data)
.enter()
.append('path')
.attr('fill', 'none')
.attr('stroke', d => myColor(d[0]))
.attr('stroke-width', 1.5)
.attr('d', (d) => line(Array.from(d.values())[1]));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js"></script>
<div id="my_dataviz"></div>
Try to parse the data on the line generator
const margin = { top: 10, right: 30, bottom: 30, left: 60 };
const width = 1000 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;
const 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})`);
d3.json("./data.json")
.then( function(data) {
const grouped_data = d3.group(data, d => d.ID);
parseDate = d3.timeParse('%Y-%m-%d')
const xScale = d3.scaleTime()
.domain(d3.extent(data, d => parseDate(d.date)))
.range([0, width]);
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.num_orders)])
.range([height, 0]);
svg.append('g')
.call(d3.axisLeft(yScale));
const myColor = d3.scaleOrdinal()
.domain(grouped_data.keys())
.range(d3.schemeSet3);
svg.selectAll('.line')
.data(grouped_data)
.enter()
.append('path')
.attr('fill', 'none')
.attr('stroke', d => myColor(d.keys))
.attr('stroke-width', 1.5)
.attr('d', function(d){
return d3.line()
.x(d => xScale(parseData(d.date)))
.y(d => yScale(d.num_orders))
(d.values);
});
} )
I have tried everything from https://observablehq.com/#d3/styled-axes and other tutorials but no chance: formatting the x-axis doesn't work, for instance with .call(d3.axisBottom(x).ticks(10)). My chart always shows every tick (> 100) and so it's overlapping. The x-axis displays dates, like 2020-03-06.
Here is my code:
<script>
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 80, left: 80},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var x = d3.scaleBand().rangeRound([0, width]).paddingInner(0.15),
y = d3.scaleLinear().rangeRound([height, 0]);
var g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.json("10.json").then(function (data) {
x.domain(data.map(function (d) { return d.TIMESTAMP; }));
y.domain([0, d3.max(data, function (d) { return Number(d.TURNOVER); })]);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(10));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y).ticks(10));
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.TIMESTAMP); })
.attr("y", function(d) { return y(d.TURNOVER); })
.attr("width", x.bandwidth())
.attr("height", function(d) { return height - y(d.TURNOVER); });
})
.catch((error) => {
throw error;
});
</script>
You should use date & time scale for your x scale which is x = d3.scaleTime().
In ticks(10), You might want to slice it by month, or days. For example .ticks(d3.timeDay.every(4)). This will slice to every 4 days.
You also want to change your date format. You can use d3.timeFormat("%m/%d"), which will give you "02/06".
Here is some example. Hope this helps.
// Let create some json dataset
var data = [];
for (var i = 0; i < 1000; ++i) {
let date = parseInt(Math.random() * 30) + 1;
let dateStr = (date < 10) ? '0' + date : date
data.push({
TIMESTAMP: '2020-02-' + dateStr.toString(),
TURNOVER: Math.random() * 100
});
}
// Create svg
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 80, left: 80},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var x = d3.scaleTime().range([0, width]),
y = d3.scaleLinear().rangeRound([height, 0]);
var g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Find min and max date
var minDate = d3.min(data.map(function(d) { return new Date(d.TIMESTAMP); }));
var maxDate = d3.max(data.map(function(d) { return new Date(d.TIMESTAMP); }));
// Set x and y data domains
x.domain([minDate, maxDate]);
y.domain([0, d3.max(data, function(d) { return d.TURNOVER; })]);
// Create x axis
var xAxis = d3.axisBottom(x)
.ticks(d3.timeDay.every(4)) // Slice time every 4 days
.tickFormat(d3.timeFormat("%m/%d")); // Change time format as m/d
// Add x axis
g.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0, 10)')
.call(xAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="100"></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