I have a dataset that the age field is represent as range, such as 0--8,9--17,18--23,etc.
year,gender,age,population
2002,Female,0--8,0
2002,Female,9--17,25
2002,Female,18--20,291
2002,Female,21--23,375
2002,Female,24--26,212
2002,Female,27--29,108
2002,Female,30--38,74
2002,Female,39--47,0
I want to use this age in the X axis of my barplot. How can I calculate the domain in this range form? Seems like I can't just use
d3.max(dataset, function(d) { return +d.age})
Below is my code until creating yScale:
function dataProcessor(d) {
return {
year: +d.year,
gender: d.gender,
age: +d.age,
population: +d.population
};
}
width = 600
height = 400
var svg = d3.select("svg");
var margin = {top: 50,
right: 50,
bottom: 50,
left: 100
};
chartWidth = +svg.attr("width") - margin.left - margin.right;
chartHeight = +svg.attr("height") - margin.top - margin.bottom;
g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv('years.csv', dataProcessor).then(function(data) {
});
var xScale = d3.scaleBand().rangeRound([0, width])
.padding(0.1)
.domain([0, data.length-1])
var populationMax = d3.max(data, function(d) {
return +d.population
});
var populationMin = d3.min(data, function(d) {
return +d.population
});
var yScale = d3.scaleLinear()
.domain([populationMin, populationMax])
.range([height, 0]);
These age ranges aren’t continuous; they’re ordinal. Essentially, these are labels that happen to have a particular order, not numbers.
Rather than use d3.scaleLinear(), use d3.scaleBand() where the domain is ["0--8", "9--17", ... , "39--47"]. d3.scaleBand() maps from a discrete domain to continuous range. It’s designed to be used for things like bar charts, so it also includes a bandwidth() method to tell you how wide each band is based on the size of your domain, range, and any padding that you’ve configured the scale for.
let x = d3.scaleBand(["0--8", "9--17", "18--20", "21--23",
"24--26", "27--29", "30--38" , "39--47"]);
.range([[0, width]);
Then (assuming you already have a data-bound selection called bars) you can position each bar with something like
bars.
.attr("x", d => x(d.age))
.attr("width", x.bandwidth());
Related
I have a csv that has the following structure:
date
A
B
'2022-01-02'
120
150
'2022-01-03'
160
170
Now, I am trying to build a symbol chart that has the x value as the date and the y value as the value of either A or B.
I would like to have the symbols in A to have the same colors as each other, and group B as well.
The problem is whenever I use my code to do this it gives me only 2 symbols instead of 4. i.e. it gives me one symbol per column not per data point. Also I am not sure how to force all points in the same column to be the same color?
I am using d3 v5
This is my code
var margin = {top: 20, right: 10, bottom: 20, left: 10};
var width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
const timeConv = d3.timeParse("%Y-%m-%d");
const dataset = d3.csv("xxx.csv");
dataset.then(function(data) {
var slices = data.columns.slice(1).map(function(id) {
return {
id: id,
values: data.map(function(d){
return {
date: timeConv(d.date),
measurement: +d[id]
};
})
};
});
var svg2 = d3.select("body").append("svg")
.attr("width", (width + margin.left + margin.right))
.attr("height", (height + margin.top + margin.bottom))
.attr("transform", "translate(" + margin.left/4 + "," + margin.top/4 + ")");
// scales
const xScale = d3.scaleTime().range([0,width]);
const yScale = d3.scaleLinear().rangeRound([height, 0]);
xScale.domain(d3.extent(data, function(d){
return timeConv(d.date)}));
yScale.domain([(0), d3.max(slices1, function(c) {
return d3.max(c.values, function(d) {
return d.measurement; });
})
]);
// axis
const yaxis = d3.axisLeft()
.ticks(9)
.scale(yScale);
const xaxis = d3.axisBottom()
.ticks(d3.timeMonth.every(3))
.tickFormat(d3.timeFormat('%b %y'))
.scale(xScale);
var slices2 = slices.filter(slice => RegExp('someRegex').test(slice.id))
//console.log(xScale(timeConv(new Date(d.value.date))))
const symbol = d3.symbol().size(500)
const symbols = svg2_2.append('g').attr('id','symbols').selectAll("symbols")
.data(slices2)
.enter()
symbols.append("path").attr("d", symbol)
I am new creating charts with d3 and Im trying to build a line chart with ordinal axis. I have managed to visualize the y axis and the x axis correctly, but I am not able to create the line.
I have this code:
var margin = {top: 10, right: 30, bottom: 30, left: 60},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var data = [{
name: "cat",
value: 10
}, {
name: "dog",
value: 3
}, {
name: "pig",
value: 7
}, {
name: "bird",
value: 7
}];
//append the svg object to the body of the page
var svg = d3.select("#div-svg" ).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 + ")");
//set scale
var xScale = d3.scaleBand()
.domain(d3.extent(data, function(d){return d["name"]}))
.range([0, width]),
yScale = d3.scaleLinear()
.domain([min(this.value_array), max(this.value_array)])
.range([height, 0]);
//add Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
svg.append("g")
.call(d3.axisLeft(yScale));
Up to here everything works fine. The next part is the onethat doesn't work.
//line
var line = d3.line()
.x(function(d) { return xScale(d[0]); })
.y(function(d) { return yScale(d.value); })
If I put d[0] I get the following error "type number is not assignable to parameter of tye string". If I put d.name I get the following error "'name' does not exist on type [number,number]". I don't know how to proceed to display the chart. Do you have any solution?
Also I would like it to be an area chart not just a linechart, in case the procedure changes.
d3.extent is not what you want to use for the domain. You need an array with each of the bar names.
var xScale = d3.scaleBand()
.domain(data.map(function(d){return d["name"]}))
.range([0, width])
The snippet below creates a single x axis with starting ticks of 10. During zoom I'm updating ticks on the rescaled axis with:
.ticks(startTicks * Math.floor(event.transform.k))
With .scaleExtent([1, 50]) I can get down from years to 3-hourly blocks fairly smoothly (besides a little label overlap here and there).
But, when I request the number of ticks applied on the scale (xScale.ticks().length) I get a different number to the one I just assigned.
Also, when I get the labels (xScale.ticks().map(xScale.tickFormat())) they differ from the ones rendered as I get deeper into the zoom.
Reading here:
An optional count argument requests more or fewer ticks. The number of
ticks returned, however, is not necessarily equal to the requested
count. Ticks are restricted to nicely-rounded values (multiples of 1,
2, 5 and powers of 10), and the scale’s domain can not always be
subdivided in exactly count such intervals. See d3.ticks for more
details.
I understand I might not get the number of ticks I request, but it's counter-intuitive that:
I request more and more ticks (per k) - between 10 and 500
Then the returned ticks fluctuates between 5 and 19.
Why is this ? Is there a better or 'standard' way to update ticks whilst zooming for scaleTime or scaleUtc ?
var margin = {top: 0, right: 25, bottom: 20, left: 25}
var width = 600 - margin.left - margin.right;
var height = 40 - margin.top - margin.bottom;
// x domain
var x = d3.timeDays(new Date(2020, 00, 01), new Date(2025, 00, 01));
// start with 10 ticks
var startTicks = 10;
// zoom function
var zoom = d3.zoom()
.on("zoom", (event) => {
var t = event.transform;
xScale
.domain(t.rescaleX(xScale2).domain())
.range([0, width].map(d => t.applyX(d)));
var zoomedRangeWidth = xScale.range()[1] - xScale.range()[0];
var zrw = zoomedRangeWidth.toFixed(4);
var kAppliedToWidth = kw = t.k * width;
var kw = kAppliedToWidth.toFixed(4);
var zoomTicks = zt = startTicks * Math.floor(t.k);
svg.select(".x-axis")
.call(d3.axisBottom(xScale)
.ticks(zt)
);
var realTicks = rt = xScale.ticks().length;
console.log(`zrw: ${zrw}, kw: ${kw}, zt: ${zt}, rt: ${rt}`);
console.log(`labels: ${xScale.ticks().map(xScale.tickFormat())}`);
})
.scaleExtent([1, 50]);
// x scale
var xScale = d3.scaleTime()
.domain(d3.extent(x))
.range([0, width]);
// x scale copy
var xScale2 = xScale.copy();
// svg
var svg = d3.select("#scale")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// clippath
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("width", width)
.attr("height", height);
// x-axis
svg.append("g")
.attr("class", "x-axis")
.attr("clip-path", "url(#clip)")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)
.ticks(startTicks));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="scale"></div>
The issue is in how the xScale is being updated on zoom.
The current approach in the example is:
xScale
.domain(t.rescaleX(xScale2).domain())
.range([0, width].map(d => t.applyX(d)));
This is doing two things:
Creating a rescaled copy of xScale2, but only to get its domain.
Extending the range of the xScale depending on the transform.
Because of step 2, the scale range is growing outside of the screen. When you request 500 ticks but only see 10, it is because there are 490 out of the viewport.
The solution is that continuous scales don't need to have the range updated on zoom, because the rescaleX method is enough for the transformation process.
The appropriate way to rescale a continuous scale on zoom is:
xScale = t.rescaleX(xScale2)
Which changes only the domain and keeps the range intact.
Consider this example to illustrate why only changing the domain is enough: If a scale maps from a domain [0,1] to a range [0, 100], and it is transformed with rescaleX, the new scale will now map from another domain (say, [0.4, 0.6]) to the same range [0, 100]. This is the zoom concept: it was showing data from 0 to 1 in a 100 width viewport, but now it is showing data from 0.4 to 0.6 in the same viewport; it "zoomed in" to 0.4 and 0.6.
The incorrect format returned from xScale.tickFormat() was a consequence of the range extension, but also of a mismatch between the displayed ticks and the computed ticks. The method only return the same ticks that are displayed if it also consideres the same amount of ticks, which is informed in the first parameter (in your example, it would be xScale.tickFormat(zt)). Since it had no arguments, it defaults to 10, and the 10 ticks computed in the time scale could be different or be in a different time granularity than the zt ticks that are displayed.
In summary, the snippet needs three changes:
Change 1: Update only the domain directly with rescaleX.
Change 2: Fix zoom ticks to a number, such as 10.
Change 3: Consider the number of ticks when using the tickFormat method.
The snippet below is updated with those changes:
var margin = {top: 0, right: 25, bottom: 20, left: 25}
var width = 600 - margin.left - margin.right;
var height = 40 - margin.top - margin.bottom;
// x domain
var x = d3.timeDays(new Date(2020, 00, 01), new Date(2025, 00, 01));
// start with 10 ticks
var startTicks = 10;
// zoom function
var zoom = d3.zoom()
.on("zoom", (event) => {
var t = event.transform;
// Change 1: Update only the domain directly with rescaleX
xScale = t.rescaleX(xScale2);
var zoomedRangeWidth = xScale.range()[1] - xScale.range()[0];
var zrw = zoomedRangeWidth.toFixed(4);
var kAppliedToWidth = kw = t.k * width;
var kw = kAppliedToWidth.toFixed(4);
// Change 2: Fix zoom ticks to a number, such as 10
var zoomTicks = zt = 10
svg.select(".x-axis")
.call(d3.axisBottom(xScale)
.ticks(zt)
);
var realTicks = rt = xScale.ticks().length;
console.log(`zrw: ${zrw}, kw: ${kw}, zt: ${zt}, rt: ${rt}`);
// Change 3: Consider zt when using the tickFormat method
console.log(`labels: ${xScale.ticks().map(xScale.tickFormat(zt))}`);
})
.scaleExtent([1, 50]);
// x scale
var xScale = d3.scaleTime()
.domain(d3.extent(x))
.range([0, width]);
// x scale copy
var xScale2 = xScale.copy();
// svg
var svg = d3.select("#scale")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// clippath
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("width", width)
.attr("height", height);
// x-axis
svg.append("g")
.attr("class", "x-axis")
.attr("clip-path", "url(#clip)")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)
.ticks(startTicks));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="scale"></div>
I am making a scatterplot in D3 with the code below and the textlabels of the right circles are being cut off, after putting textlabels on the X and Y axis.
Here is a working jsFiddle:
https://jsfiddle.net/chemok78/1vcat0s8/4/
Adding the X and Y axis labels seems to move the whole chart to the right and up, making it move a bit out of the containing div. Anyone can help me how to fix this?
var url = "https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/cyclist-data.json";
d3.json(url, function(json) {
var data = json;
var margin = {
top: 40,
right: 100,
bottom: 80,
left: 80
};
var w = 1000 - margin.left - margin.right;
var h = 800 - margin.top - margin.bottom;
var svg = d3.select("#chart")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var maxRank = d3.max(data, function(d) {
return d.Place;
});
var minSeconds = d3.min(data, function(d) {
return d.Seconds;
});
var maxSeconds = d3.max(data, function(d) {
return d.Seconds;
});
var formatTime = d3.time.format("%M:%S");
var formatSeconds = formatMinutes = function(d) {
return formatTime(new Date(2016, 0, 0, 0, 1, d));
};
var xScale = d3.scale.linear()
.domain([maxSeconds + 5, minSeconds])
.range([0, w]);
var yScale = d3.scale.linear()
.domain([0, maxRank + 2])
.range([0, h]);
This has nothing to do with "moving the chart". Here is the problem.
When you do this:
var labels = svg.selectAll("text")
You're selecting text elements that already exist in your SVG. Because of that, your "enter" selection will have less elements compared to what it should contain.
The solution is simple: select something that doesn't exist:
var labels = svg.selectAll("foo")
Or, alternatively, move the blocks that append the axes' labels to the bottom of the code.
Here is your updated fiddle: https://jsfiddle.net/mp7LxsL4/
I'm trying to make a scatterplot that appears below an interactive worldmap with the help of D3. The scatterplot contains data from the country that the user clicked on in the worldmap. The problem is that when the user clicks on another country, the scatterplot of the previous country should disappear. This is not the case unfortunately, the second scatterplot just appears under the first scatterplot. Does anyone know how I can fix this? Any help would be greatly appreciated.
A part of the code I use for the scatterplot:
function ScatterCorruption(dataset, title){
var xValue = function(d) { return d.GDP;}
var yValue = function(d) { return d.Variable;}
// determine parameters
var margin = {top: 20, right: 20, bottom: 200, left: 70},
width = 600 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// formatters for axis and labels
var currencyFormat = d3.format("0.2f");
var decimalFormat = d3.format("0.2f");
// determine x scale
var x = d3.scale.linear()
.range([0, width]);
// determine y scale
var y = d3.scale.linear()
.range([height, 0]);
// determine x-axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
// determine y-axis
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// make svg
var svg = d3.select("body").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 + ")");
// add the tooltip area to the webpage
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
console.log(dataset)
// load in data
d3.tsv(dataset, function(error, data) {
if (error) throw error;
// convert data
data.forEach(function(d) {
d.GDP = +d.GDP;
d.Variable = +d.Variable;
});
You'll need to call this before rendering a new scatterplot: d3.selectAll("svg > *").remove(); so that your svg is clear again. Alternatively you can also do d3.select("svg").remove(); and then recreate the svg.