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.
Related
I'm making a bar chart but I'm having problems to match the bar positions with the xAxis. They're not in the right place, for example, by hovering the bar above the 2010 mark, you can see it shows a 2007 value. 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;
const barthickness = 2.909090909090909;
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]);
const xAxis = d3.axisBottom(xScale);
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', barthickness)
.attr('x', (d, i) => padding + (3.2* 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>
The problem is that you are not using your x-scale to position the bars. You are using padding + (3.2* i) to set the x coordinate of the bars, which does not line up with your scale. Your chart is 840 pixels wide and has 275 bars, which would be ~3.055 pixels per bar. Your code is placing bars every 3.2 pixels, which is too far.
Typically with bar charts, rather than hard-coding a bar thickness, you use a band scale. You'll want to use your scales both in your axes and to position the bars.
Alternatively, since you are working with temporal data, you could also consider using an area chart instead of a bar chart.
Below I've provided two similarly looking charts for your data. One is a bar chart and the other an area chart.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="bar-chart"></div>
<div id="area-chart"></div>
<script>
const url = 'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json';
d3.json(url).then(json => {
// convert the string into Date objects
const parse = d3.timeParse('%Y-%m-%d');
const data = json.data.map(d => ({ date: parse(d[0]), value: d[1] }));
barchart(data);
areachart(data);
});
function barchart(data) {
// set up
const margin = { top: 20, right: 20, bottom: 20, left: 30 };
const width = 600 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select('#bar-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})`);
// scales
const x = d3.scaleBand()
.domain(data.map(d => d.date))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0]);
// axes
// by default, axes for band scales show tick marks for every bar
// that would be too cluttered for this data, so we override this
// by explicitly setting tickValues()
const [minDate, maxDate] = d3.extent(data, d => d.date);
const xAxis = d3.axisBottom(x)
.tickSizeOuter(0)
// only show the year in the tick labels
.tickFormat(d3.timeFormat('%Y'))
.tickValues(d3.timeTicks(minDate, maxDate, 10));
const yAxis = d3.axisLeft(y)
.tickSizeOuter(0)
.ticks(10, '~s');
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis);
svg.append('g')
.call(yAxis);
// bars
// function to convert Date into string showing the month and year
const format = d3.timeFormat('%b %Y');
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => x(d.date))
.attr('width', d => x.bandwidth())
.attr('y', d => y(d.value))
.attr('height', d => height - y(d.value))
.attr('fill', 'steelblue')
.append('title')
.text(d => `${format(d.date)}: ${d.value}`)
}
function areachart(data) {
// set up
const margin = { top: 20, right: 20, bottom: 20, left: 30 };
const width = 600 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select('#area-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})`);
// scales
const x = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0]);
// area generator
const area = d3.area()
.x(d => x(d.date))
.y0(y(0))
.y1(d => y(d.value))
.curve(d3.curveStepAfter);
// axes
const xAxis = d3.axisBottom(x)
.tickSizeOuter(0)
// only show the year in the tick labels
.tickFormat(d3.timeFormat('%Y'));
const yAxis = d3.axisLeft(y)
.tickSizeOuter(0)
.ticks(10, '~s');
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis);
svg.append('g')
.call(yAxis);
// area
svg.append('path')
.attr('d', area(data))
.attr('fill', 'steelblue')
}
</script>
</body>
</html>
Barchart image
D3.js bar chart, bars extending from top to bottom, instead of bottom to top.
I am not sure what attributes i should be changing to correct this.
I have posted my code and an image of the resulting chart.
...
const marketCataRender = marketCataData => {
const marketCataSVG = d3.select('.marketCataChart').append('svg')
marketCataSVG.attr('class', 'marketCataSVG')
.attr('height', marketCataHeight)
.attr('width', marketCataWidth);
// x y values
const xValue = d => d.loc_start_str;
const yValue = d => d.total_matched;
// x y scales
const xScale = d3.scaleBand()
.domain(marketCataData.map(xValue))
.range(\[0, innerWidth\]);
const yScale = d3.scaleLinear()
.domain(d3.extent(marketCataData, yValue))
.range(\[innerHeight, 0\])
.nice();
// x y axis
const xAxis = d3.axisBottom(xScale)
const yAxis = d3.axisLeft(yScale)
// set chart group to make it easier to transform
const g = marketCataSVG.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// x y axis groups
const xAxisG = g.append('g')
.call(xAxis)
.attr('transform', `translate(0, ${innerHeight})`)
.selectAll('text')
.style('text-anchor', 'end')
.attr('transform', `rotate(-90)`)
.attr('x', -7)
const yAxisG = g.append('g')
.call(yAxis)
// Apply bar chart rectangle to chart
const marketCataRect = g.selectAll('rect')
marketCataRect.data(marketCataData)
.enter().append('rect')
.attr('x', d => xScale(xValue(d)))
.attr('height', d => yScale(yValue(d)))
.attr('width', xScale.bandwidth());
}][1]
...
You haven't declared the Y coordinates for your rectangles.
You need to scale the y coordinate of your rectangles.
const marketCataRect = g.selectAll('rect')
marketCataRect.data(marketCataData)
.enter().append('rect')
.attr('x', d => xScale(d.loc_start_str) )
.attr('y', d => yScale(d.total_matched) ) // set y
.attr('height', d => marketCataHeight - yScale(d.total_matched)) // find height by subtracting y value from height of the chart.
.attr('width', xScale.bandwidth());
example here: https://bl.ocks.org/d3noob/8952219
Try to always provide a Minimal, Complete, and Verifiable example. https://stackoverflow.com/help/mcve
I tried to do this by taking your code and adding dummy data etc. and then modifying it
The result is this (Demo here- https://codepen.io/Alexander9111/pen/gObEZym):
HTML:
<div class="marketCataChart"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
Javascript:
const marketCataHeight = 800;
const marketCataWidth = 2000;
const innerWidth = 1500;
const innerHeight = 500;
const margin = {
top: 30,
left: 30,
bottom: 30,
right: 30
};
const marketCataRender = marketCataData => {
const marketCataSVG = d3.select('.marketCataChart').append('svg')
marketCataSVG.attr('class', 'marketCataSVG')
.attr('height', marketCataHeight)
.attr('width', marketCataWidth);
// x y values
const xValue = d => d.loc_start_str;
const yValue = d => d.total_matched;
// x y scales
const xScale = d3.scaleBand();
xScale.domain(marketCataData.map(xValue))
.range([0, innerWidth]);
const yScale = d3.scaleLinear();
yScale.domain(d3.extent(marketCataData, yValue))
.range([innerHeight, 0])
.nice();
// x y axis
//const xAxis = d3.axisBottom(xScale)
const xAxis = d3.axisTop(xScale) //change to axisTop
const yAxis = d3.axisLeft(yScale)
// set chart group to make it easier to transform
const g = marketCataSVG.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// x y axis groups
const xAxisG = g.append('g')
.call(xAxis)
.attr('class', 'x-axis')
.attr('transform', `translate(0, ${0})`) // no longer need to translate by innerHeight as the x-axis is on the top
.selectAll('text')
.style('text-anchor', 'middle')
.attr('transform', `rotate(-0)`) //-90
.attr('x', -7)
const yAxisG = g.append('g')
.call(yAxis)
.attr('class', 'y-axis');
// Apply bar chart rectangle to chart
const marketCataRect = g.selectAll('rect');
marketCataRect.data(marketCataData)
.enter().append('rect')
.attr('x', d => xScale(xValue(d)))
.attr('height', d => yScale(yValue(d)))
.attr('width', xScale.bandwidth());
//Optional - add chart border:
g.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', innerWidth)
.attr('height', innerHeight)
.attr('stroke', 'black')
.attr('stroke-width', '1px')
.attr('fill', 'none')
.attr('class', 'chart-boarder');
};
const marketCataData = [
{loc_start_str: "example0", total_matched: 0},
{loc_start_str: "example1", total_matched: 100},
{loc_start_str: "example2", total_matched: 200},
{loc_start_str: "example3", total_matched: 300},
{loc_start_str: "example4", total_matched: 400},
]
marketCataRender(marketCataData);
Most important lines were: const xAxis = d3.axisTop(xScale) and const xAxisG.attr('transform', `translate(0, ${0})`)
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)}));
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"));
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/