D3: scale data on the x-axis every 7 years - javascript

I have a d3 project where I want to include all my dates but only on certain intervals. Right now it displays everything which is too cluttered. I only want to display the labels on the x axis every 7 years. so for example 1947, 1954, 1961, 1968, etc. Pease help and thank you in advance.
Here is my code:
loadData = ()=> {
req = new XMLHttpRequest();
req.open("GET", "https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json" , true);
req.send();
req.onload= ()=>{
json = JSON.parse(req.responseText);
//dynmaic height
/*var margin = {top: 20, right: 200, bottom: 0, left: 20},
width = 300,
height = datajson.length * 20 + margin.top + margin.bottom;*/
//create measurements
const margin = 60
const width = 1000 - margin;
const height = 600 - margin;
const maxYScale = d3.max(json.data, (d) => d[1]);
//date formatter
const formatDate = d3.timeParse("%Y-%m-%d"); //convert from string to date format
const parseDate = d3.timeFormat("%Y"); //format date to cstring
//create svg
const svg = d3.select("svg");
const chart = svg.append("g")
.attr("transform", `translate(${margin}, ${margin})`);
//y-axis: split charts into 2 equal parts using scaling function
const yScale = d3.scaleLinear()
.range([height, 0]) //length
.domain([0, maxYScale]); //content
//create x-axis
const yAxis = d3.axisLeft(yScale);
//append y-axis
chart.append("g")
.call(yAxis);
//create x-scale
const xScale = d3.scaleBand()
.range([0, width]) //length
//.domain(json.data.filter((date, key) => { return (key % 20 === 0)}).map((d)=> parseDate(formatDate(d[0]))))
.domain(json.data.map((d)=> parseDate(formatDate(d[0]))))
.padding(0.2);
//create x-axis
const xAxis = d3.axisBottom(xScale);
//append x-axis
chart.append("g")
.attr(`transform`, `translate(0, ${height})`)
.call(xAxis);
//make bars
chart.selectAll("rect")
.data(json.data)
.enter()
.append("rect")
.attr("x", (d) => xScale(parseDate(formatDate(d[0]))))
.attr("y", (d) => yScale(d[1]))
.attr("height", (d) => height - yScale(d[1]))
.attr("width", xScale.bandwidth())
}
}
loadData();
Here is my codepen:
codepen

I am just going to answer my own question as I found the solution. In order to set intervals in the x axis I simply used tickValues. Then I used my scale and a filter function to filter the intervals based on the data I had. Below you may find the answer.
const xAxis = d3.axisBottom(xScale)
.tickValues(xScale.domain().filter(function(d) { return (d % 7 === 0)}));

Related

How to parse new Date.now() object with D3.js to generate a linechart

I'm trying to make a linechart with D3 and React where the x axis is based on Date.now() object and all the ticks are a minute apart on a 10mn window.
I can't generate the line because I get "NaNNaNNaN" in my svg path;
Can't seem to figure out how to have ticks minutes apart on my x axis;
Here's how the data looks like
// data state
data = [
{"loadAverage":0.008333333333333333,"timestamp":1632740462342},
{"loadAverage":0.008333333333333333,"timestamp":1632740459323},
{"loadAverage":0.013333333333333334,"timestamp":1632740471400}
];
the timestamp key is a new Date.now() coming from the server
useEffect(() => {
const svg = d3.select(d3Container.current);
let margin = { top: 20, right: 20, bottom: 50, left: 70 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
let x = d3
.scaleTime()
.domain(d3.extent(data, (d) => timeFormat(d.timestamp)))
.range([0, width]);
let y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.loadAverage)])
.nice()
.range([height, 0]);
// Parse the date
let parseTime = d3.timeParse("%s");
let timeFormat = d3.timeFormat("%M:%S");
// Constructing the line
const myLine = d3
.line()
.x((d) => {
const convertedTime = parseTime(d.timestamp);
return x(convertedTime);
})
.y((d) => {
return y(d.loadAverage);
});
svg
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg
.select("svg")
.selectAll("path")
.data([data])
.join("path")
.attr("d", (value) => myLine(value))
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round");
// Add the x Axis
svg
.select("svg")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the y Axis
svg
.select("svg")
.append("g")
.call(d3.axisLeft(y).tickFormat(timeFormat).ticks(10));
}, [data]);
This is my first time using D3, any help would be greatly appreciated !
Edit: here's what I tried so far
// Constructing the line
const myLine = d3
.line()
.x((d) => {
const convertedTime = new Date(d.timestamp);
return x(convertedTime);
})
.y((d) => {
return y(d.loadAverage);
});
Even tried to return convertedTime wrapped up by parsetime like so parsetime(convertedTime) Didn't work either.
I think you have a problem in Initializing x scale domain
// set the ranges
let x = d3
.scaleTime()
// ⬇️ here is the issue, just get rid of timeFormat
.domain(d3.extent(data, (d) => timeFormat(d.timestamp)))
.range([0, width]);
the scaleTime expect the domain to be a [Date|number, Date|number], you are using timeFormat which convert number|Date into a string based on the given format.
Try to use this instead:
let x = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.timestamp))
.range([0, width]);
// The short vesion
let x = d3.scaleTime(d3.extent(data, (d) => d.timestamp), [0, width])
Constructing the line
const myLine = d3
.line()
.x((d) => x(d.timestamp))
.y((d) => y(d.loadAverage));
If you need to convert timestamps into Dates, you can map the whole data array
data = data.map(d=> d.timestamp = new Date(d.timestamp), d)

x axis with a full day in hours/quarters

I would like to plot out an entire day divided in quarters.
const height = 100,
width = 800;
const dayInQuarters = d3.timeMinute.every(15).range(startDate, endDate);
const xScale = d3.scaleTime()
.domain(dayInQuarters)
.range([0, 700])
const xAxis = d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%H:%M"))
const d3Root = document.getElementById('d3-root');
const svg = d3.select(d3Root).append('svg').attr('width', width).attr('height', height + 20);
svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(20,${height})`)
.call(xAxis);
What is going well here is that it automatically divides the day into 8 ticks being 3 hour slots.
But the width of the ticks are abnormally wide:
What I would like to be able is to contain the ticks into the 800px width.
Working example: https://jsfiddle.net/Spindle/3h51umra/23/
In a time scale you pass just the start and end dates as the array:
const xScale = d3.scaleTime()
.domain([startDate, endDate])
.range([0, 700])
Therefore, your daysInQuarters array goes in the axis' tickValues method:
const xAxis = d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%H:%M"))
.tickValues(dayInQuarters)
Here is your code with those changes:
const height = 100,
width = 800;
const startDate = new Date('2021-03-14T23:00:00Z');
const endDate = d3.timeDay.offset(startDate, 1);
const dayInQuarters = d3.timeMinute.every(15).range(startDate, endDate);
const xScale = d3.scaleTime()
.domain([startDate, endDate])
.range([0, 700])
const xAxis = d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%H:%M"))
.tickValues(dayInQuarters)
const d3Root = document.getElementById('d3-root');
const svg = d3.select(d3Root).append('svg').attr('width', width).attr('height', height + 20);
svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(20,${height})`)
.call(xAxis);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="d3-root"></div>
And the same code, with the ticks not overlapping:
const height = 100,
width = 1000;
const startDate = new Date('2021-03-14T23:00:00Z');
const endDate = d3.timeDay.offset(startDate, 1);
const dayInQuarters = d3.timeMinute.every(15).range(startDate, endDate);
const xScale = d3.scaleTime()
.domain([startDate, endDate])
.range([20, 980])
const xAxis = d3.axisBottom(xScale)
.tickFormat(d3.timeFormat("%H:%M"))
.tickValues(dayInQuarters)
const d3Root = document.getElementById('d3-root');
const svg = d3.select(d3Root).append('svg').attr('width', width).attr('height', height + 20);
svg.append('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${height-50})`)
.call(xAxis)
.selectAll(".tick text")
.attr("transform", "rotate(-90, 5, 17)")
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="d3-root"></div>
Finally, pay attention to the strings you're using: your SVG is 10020px tall, not 120.

Correctly displaying all months in y-axis of d3 graph

Trying to use a scaleTime()for my y axis and populate it with all 12 months.
Here I set the ranges and domain and then apply the full month name format with %B.
const yScale = d3.scaleTime()
.domain([new Date().setMonth(0),new Date().setMonth(11)])
.range([h-padding, padding]);
const yAxis = d3.axisLeft(yScale)
.tickFormat(d3.timeFormat("%B"));
I expect the y scale to show January at the bottom, December at the top and all month ordered in between.
Instead the first month shown is February, followed by all the months and then December is shown just below the final tick line.
Below is the full code snippet:
document.addEventListener('DOMContentLoaded', async function() {
const res = await d3.json("https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/global-temperature.json");
const BASE_TEMP = parseFloat(res.baseTemperature);
document.getElementById("description").textContent =res.monthlyVariance[0]["year"] + "-" + res.monthlyVariance[res.monthlyVariance.length-1]["year"] + ": Base Temperature: " + BASE_TEMP;
const w = 800;
const h = 500;
const padding = 60;
const barWidth = (w-padding)/res.monthlyVariance.length;
const barHeight = (h-padding)/12;
const xScale = d3.scaleTime()
.domain([new Date().setFullYear(d3.min(res.monthlyVariance, (d,i)=>parseInt(d["year"]))),new Date().setFullYear(d3.max(res.monthlyVariance, (d,i)=>parseInt(d["year"])))])
.range([padding, w - padding]);
const yScale = d3.scaleTime()
.domain([new Date().setMonth(0),new Date().setMonth(11)])
.range([h-padding, padding]);
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale)
.tickFormat(d3.timeFormat("%B"));
const svg = d3.select("#canvas")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.append("g")
.attr("id", "x-axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append("g")
.attr("id", "y-axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.selectAll("rect")
.data(res.monthlyVariance)
.enter()
.append("rect")
.attr("width",barWidth)
.attr("height",barHeight)
.attr("x",(d,i)=>xScale(new Date().setFullYear(d["year"])))
.attr("y", (d,i)=>yScale(new Date().setMonth(parseInt(d["month"])-1)))
.attr("data-year",(d,i)=>d["year"])
.attr("data-month",(d,i)=>d["month"])
.attr("data-temp",(d,i)=>(BASE_TEMP+parseFloat(d["variance"])))
.attr("class", "cell")
.attr("fill", function(d,i){
let variance = parseInt(d["variance"]);
let color = "green";
if (variance<-2) {color="blue"}
else if (variance<-1) {color="lightgreen"}
else if (variance<1) {color="green"}
else if (variance<2) {color="orange"}
else {color="red"}
return color;});
});
#container {
position:absolute;
width:800px;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
text-align:center;
}
#title {
font-size:20px;
}
.cell:hover {
background:lightgrey;
}
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.1/d3.min.js"></script>
<div id="container">
<div id="title">Variation in Monthly Land Temperature</div>
<div id="description">Update this Message from Dataset</div>
<div id="canvas"></div>
</div>
</body>
You have a few options here. The first, most straight-forward way, is to specify the number of ticks you want - in your case 12.
const yAxis = d3.axisLeft(yScale)
.ticks(12)
.tickFormat(d3.timeFormat("%B"));
With this approach what you might find that d3 will still fail to render the appropriate number of ticks, which is obviously frustrating.
According to the docs
The specified count is only a hint; the scale may return more or fewer
values depending on the domain.
Another option is to explicitly define the tick values.
const tickValues = Array(12).fill(1).map((val, index) => new Date().setMonth(index));
const yAxis = d3.axisLeft(yScale)
.tickValues(tickValues)
.tickFormat(d3.timeFormat("%B"));

Line not appearing in d3.js linechart (only axes are visible)

I am trying to create a line graph in d3.js but only my axes are appearing; the line doesn't show.
Things that are working:1. My axes are labelled correctly 2. Looking at the elements of the page in Chrome it seems the x and y attributes for the line are 'working' (i.e. the data for coordinates are defined for the line/are not 'NaN' values). I think there must be something wrong with attributes associated with my line (end of the Javascript code).
Are there any reasons this might be happening?
This is what my plot/graph output currently looks like:
Current state of plot
Here is my HTML, Javascript and the data I've used for the plot:
HTML:
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<div id="merit-order-chart"></div>
</body>
<script type="text/javascript" src="/src.js"></script>
</html>
JAVASCRIPT:
// create a SVG element
let svg2 = d3.select("#merit-order-chart").append("svg");
// sizing parameters
let margin2 = {top: 20, right: 50, bottom: 40, left: 80};
let width2 = 800;
let height2 = 400;
let chartWidth2 = width2 - margin2.left - margin2.right;
let chartHeight2 = height2 - margin2.top - margin2.bottom;
// sizing the SVG
svg2.attr("width", width2 + "px")
.attr("height", height2 + "px");
// creating the x and y scales
let y2 = d3.scaleLinear()
.clamp(true)
.range([chartHeight2, 0]);
let x2 = d3.scaleTime()
.clamp(true)
.range([0, chartWidth2]);
// formatting of the x and y axes
let xAxis2 = d3.axisBottom()
.scale(x2)
.tickFormat(d3.timeFormat("%Y-%m-%d %H:%M:%S"))
.ticks(4);
let yAxis2 = d3.axisLeft()
.scale(y2)
.ticks(8);
// adding a 'group' element for all the things attached to the chart
let chart2 = svg2.append("g")
.attr("transform", `translate(${margin2.left},${margin2.top})`);
// adding the x and y axis elements to the chart group (g)
const xg2 = chart2.append("g")
.classed("x axis", true)
.attr("transform", `translate(0,${chartHeight2})`)
.call(xAxis2);
const yg2 = chart2.append("g")
.classed("y axis", true)
.call(yAxis2);
d3.csv("/price-data.csv", (err, csv) => {
const clean2 = csv.map(d2 => {
// clean up number formats
d2.p = parseFloat(d2.p);
d2.settlementdate = Date.parse(d2.settlementdate)
d2.index = parseFloat(d2.index);
return d2;
});
// re-sizing the x and y axes
x2.domain([d3.min(clean2, d2 => d2.settlementdate), d3.max(clean2, d2 => d2.settlementdate)]);
xg2.call(xAxis2);
y2.domain([-1000, 14125]);
yg2.call(yAxis2);
chart2.selectAll(".prices")
.data(clean2)
.enter()
.append("line")
.attr("x", d2 => x2(d2.settlementdate))
.attr("y", d2 => y2(d2.p))
.attr("stroke-width", 5)
.attr("stroke", "black")
//.style("stroke", "rgb(6,120,155)");
});
DATA (.csv):
settlementdate,p,index
1/1/2017 0:00,50,1
1/1/2017 0:05,35,2
1/1/2017 0:10,100,3
1/1/2017 0:15,5000,4
You need to use a line generator, currently you are passing an array of objects representing each point, and appending a line for each one - this approach won't work (partly because lines don't have x and y attributes, but x1,x2,y1,y2 attributes).
You need to use a line generator:
let line = d3.line()
.x(function(d) { return x2(d.settlementdate); }) // x value for each point
.y(function(d) { return y2(d.p); }) // y value for each point
This will return a path with one vertex for every coordinate fed to it. Consequently you'll want to append a path rather than a line, and the drawing instructions for a path are contained in the d attribute, so you can use .attr("d", line).
Lastly, since you want one path per dataset, rather than one path per datapoint, nest your data into an array. By doing so you are getting one line with many points, rather than many lines with no points.
I changed the scale to show the curve, but it cuts out the peak as a result:
chart2.selectAll(".prices")
.data([clean2])
.enter()
.append("path")
.attr("d",line)
.attr("stroke-width", 5)
.attr("stroke", "black")
.attr("fill","none")
var csv = [
{ settlementdate: "1/1/2017 0:00",p:50,index:1 },
{ settlementdate: "1/1/2017 0:05",p:35,index:2 },
{ settlementdate: "1/1/2017 0:10",p:100,index:3 },
{ settlementdate: "1/1/2017 0:15",p:5000,index:4 }
]
// create a SVG element
let svg2 = d3.select("#merit-order-chart").append("svg");
// sizing parameters
let margin2 = {top: 20, right: 50, bottom: 40, left: 80};
let width2 = 800;
let height2 = 400;
let chartWidth2 = width2 - margin2.left - margin2.right;
let chartHeight2 = height2 - margin2.top - margin2.bottom;
// sizing the SVG
svg2.attr("width", width2 + "px")
.attr("height", height2 + "px");
// creating the x and y scales
let y2 = d3.scaleLinear()
.clamp(true)
.range([chartHeight2, 0]);
let x2 = d3.scaleTime()
.clamp(true)
.range([0, chartWidth2]);
// formatting of the x and y axes
let xAxis2 = d3.axisBottom()
.scale(x2)
.tickFormat(d3.timeFormat("%Y-%m-%d %H:%M:%S"))
.ticks(4);
let yAxis2 = d3.axisLeft()
.scale(y2)
.ticks(8);
// adding a 'group' element for all the things attached to the chart
let chart2 = svg2.append("g")
.attr("transform", `translate(${margin2.left},${margin2.top})`);
// adding the x and y axis elements to the chart group (g)
const xg2 = chart2.append("g")
.classed("x axis", true)
.attr("transform", `translate(0,${chartHeight2})`)
.call(xAxis2);
const yg2 = chart2.append("g")
.classed("y axis", true)
.call(yAxis2);
let line = d3.line()
.x(function(d) { return x2(d.settlementdate); })
.y(function(d) { return y2(d.p); })
const clean2 = csv.map(d2 => {
// clean up number formats
d2.p = parseFloat(d2.p);
d2.settlementdate = Date.parse(d2.settlementdate)
d2.index = parseFloat(d2.index);
return d2;
});
// re-sizing the x and y axes
x2.domain([d3.min(clean2, d2 => d2.settlementdate), d3.max(clean2, d2 => d2.settlementdate)]);
xg2.call(xAxis2);
y2.domain([0, 200]);
yg2.call(yAxis2);
chart2.selectAll(".prices")
.data([clean2])
.enter()
.append("path")
.attr("d",line)
.attr("stroke-width", 5)
.attr("stroke", "black")
.attr("fill","none")
//.style("stroke", "rgb(6,120,155)");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<div id="merit-order-chart"></div>

D3 - tickFormat not working

If anyone could take a look at my code and let me know why D3 doesn't want to format Y axis ticks as %M:%S.
JSFiddle
Code:
function renderScatterPlot(data) {
// set up svg and bar dimensions/margins
const width = 1000;
const height = 500;
const padding = 20;
const margin = { top: 40, right: 20, bottom: 80, left: 80 };
// append svg to DOM
const svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
// for parsing time string in data and converting back to original format
const parse = d3.timeParse('%M:%S');
const format = d3.timeFormat('%M:%S')
// calculate min and max data values
const timeMin = d3.min(data, (d) => parse(d.Time));
const timeMax = d3.max(data, (d) => parse(d.Time));
// set up scales
const xScale = d3.scaleBand()
.domain(data.map((d) => d.Year))
.range([margin.left, width - margin.right])
.paddingInner(0.1)
const yScale = d3.scaleTime()
.domain([timeMin, timeMax])
.range([height - margin.bottom, margin.top])
// set up x and y axes
const xAxis = d3.axisBottom(xScale)
const yAxis = d3.axisLeft(yScale)
.tickFormat(format)
// append axes to svg
svg.append('g')
.attr('id', 'x-axis')
.attr('transform', `translate(0, ${height - margin.bottom})`)
.call(xAxis)
svg.append('g')
.call(d3.axisLeft(yScale))
.attr('transform', `translate(${margin.left}, 0)`)
.attr('id', 'y-axis')
}
$.getJSON('https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/cyclist-data.json', (data) => renderScatterPlot(data));
D3 is formatting your ticks, tickFormat is working just fine.
However, there is no point in doing this...
const yAxis = d3.axisLeft(yScale)
.tickFormat(format)
... if, when calling the axis, you simply ignore yAxis:
svg.append('g')
.call(d3.axisLeft(yScale))
It should be:
svg.append('g')
.call(yAxis)
Here is your updated fiddle: https://jsfiddle.net/swxbx0Lt/

Categories