d3 path.line stroke-width with IF statement / ternary operator - javascript

Trying to change line stroke-width with an IF statement / ternary operator e.g. if d.country === "China" stroke-width: 2. This has to be a path.line attribute so this is what I'm attaching it to after line is called.
I've added countryName to the emissions object, I have also noticed that condition is always FALSE so the stroke-width is 0.5. Why its not TRUE?
Codepen
//Define line chart with and height
const width = fullWidth - margin.left - margin.right;
const height = fullHeight - margin.top - margin.bottom;
//Define x and y scale range
let xScale = d3.scaleLinear()
.range([0, width])
let yScale = d3.scaleLinear()
.range([0, height])
//Define x and y axis
let xAxis = d3.axisBottom(xScale)
.ticks(15)
let yAxis = d3.axisLeft(yScale)
.ticks(10)
//Draw svg
let svg = d3.select("body")
.append("svg")
.attr("width", fullWidth)
.attr("height", fullHeight)
.append("g")
.attr("transform", "translate(" + 53 + "," + 0 +")");
d3.json("https://api.myjson.com/bins/izmg6").then(data => {
console.log(data);
//Structure data so should be an array of arrays etc [[x,y], [x,y], [x,y]]
let years = d3.keys(data[0]).slice(0, 50);
console.log(years);
let dataset = [];
data.forEach((d, i) => {
let myEmissions = [];
years.forEach(y => {
if (d[y]) {
myEmissions.push({
country: d.countryName,
year: y,
amount: d[y]
})
}
})
dataset.push({
country: d.countryName,
emissions: myEmissions
});
})
console.log(dataset);
//Define x and y domain
xScale
.domain(d3.extent(years, d =>d))
yScale
.domain([d3.max(dataset, d =>
d3.max(d.emissions, d =>
+d.amount)), 0])
//Generate line
let line = d3.line()
.curve(d3.curveBasis)
.x(d =>
xScale(d.year))
.y(d =>
yScale(d.amount));
let groups = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
groups.append("title")
.text(d => d.country)
groups.selectAll("path")
.data(d => [d.emissions])
.enter()
.append("path").classed("line", true)
.attr("d", line)
.style("stroke-width", d =>
d.country === "China" ? 10 : 0.5
)
}).catch(error => console.log(error))

What rioV8 meant is that you already have your group selection, so you just need to use groups to append new elements.
groups is a selection of all your g, it's where you want to append your paths. The same way you're not selecting again to add the titles.
groups
.append("path").classed("line", true)
.attr("d", d=> line(d.emissions))
.style("stroke-width", d =>
d.country === "China" ? 5 : 0.5
)

Related

How to place the bars of a bar chart in the right positions of the xAxis using d3.js?

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>

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)

D3JS V6: Multi Line Group

I'm stuck trying to make a multi line graph with D3JS V6. The problem is about group the data (long format) and trying to plot the path for every group.
My data follows this structure:
Sexo
Año
D
Male
2000
25
Male
2001
58
Female
2000
55
Female
2001
75
Total
2000
80
Total
2001
133
function dataviz2() {
var dataset;
h = 400
w = 650
padding = 40
count = 0
// Convertir Strings a Fechas
var parseTime = d3.timeParse('%Y')
// Convertir Fechas a Strings
var parseDate = d3.timeFormat('%Y')
var rowConverter = function (d) {
return {
Sexo: d.Sexo,
Año: parseTime(d.Año),
N: parseInt(d.N),
}
}
d3.csv('data.csv', rowConverter).then(function (data) {
dataset = data
dataset.sort(function (x, y) {
return d3.ascending(x.Año, y.Año)
})
dataBySex = d3.group(data, d => d.Sexo)
dataBySexGr = Array.from(dataBySex, ([sexo, value]) => ({ sexo, value }))
console.log(dataBySex)
console.log(dataBySexGr)
chart()
})
function chart() {
// Scale X
xScale = d3.scaleTime()
.domain([
d3.min(dataset, function (d) { return d.Año }),
d3.max(dataset, function (d) { return d.Año })
])
.range([padding, w - padding])
// Scale Y
yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function (d) { return d.N })])
.range([h - padding, padding])
// Create Axis
var xAxis = d3.axisBottom(xScale)
var yAxis = d3.axisLeft(yScale)
var svg = d3.select('#dataviz1')
.append('svg')
.attr('width', w)
.attr('height', h)
let id = 0;
const ids = function () {
return "line-" + id++
}
var lines = svg.selectAll('.line')
.data(dataBySexGr)
.join('path')
.attr("class", ids)
**.attr('d', d => {
return d3.line()
.x(d => x(d.Año))
.y(d => y(d.N))
})**
.attr('stroke', 'blue')
.attr('stroke-width', 3)
.attr('fill', 'none')
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(0,' + (h - padding) + ')')
.attr('visibility', 'visible')
.call(xAxis)
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(' + padding + ',0)')
.attr('visibility', 'visible')
.call(yAxis)
}
}
What I know after a lot of tries is that the .attr('d', d => ...) is the problem. The argument of that function says "'d' is declared, but its value is never read". When I check the HTML, I receive the different 3 paths I'm looking for but there are no values, are a function.
I'm really stuck in here, so I appreciate any help.
Thanks!!
d3.line returns a line generator function, you can see that function as the d attribute. You need to pass the datum and execute that function to get the path data:
.attr('d', d => {
return d3.line()
.x(d => x(d.Año))
.y(d => y(d.N))(d)
})
Or alternatively, you can define the line generator before hand and simply use .attr('d',line):
const line = d3.line()
.x(d => x(d.Año))
.y(d => y(d.N))
selection.attr("d",line);

Independent scales for small multiple line chart

Link to the notebook.
I'm working on a small multiple line chart using d3.v5 on Observable, with the dataset structured like follows:
For visualization, the y scale takes num from the values array for the domain. There are several rows with unique key values, which I wanted to use to produce the small multiples. The image above shows the first key.
After visualizing the small multiple, I noticed that all the line charts are using the same y scale, which is not what I intended to do. This is what I currently have:
const y_scale = d3
.scaleLinear()
.domain([0, d3.max(series, d => d3.max(d.values, m => m.num))])
.range([width/2, width/2 - start_y - margin.bottom]);
Is there a way to adjust the domain so that each chart would have its own scale based on its own num values?
Edit 1: Notebook link added on top
The idiomatic D3 solution here would be using local variables. However, there are several different working alternatives.
For using local variables, we first declare them...
const localScale = d3.local();
const localLine = d3.local();
Then, we set the different scales in the "enter" selection:
var enter = my_group
.enter()
.append("g")
.attr("class", "chart_group")
.each(function(d) {
const yScale = localScale.set(this, d3
.scaleLinear()
.domain([0, d3.max(d.values, d => d.num)])
.range([panel_width / 2, panel_width / 2 - start_y - margin]));
localLine.set(this, d3
.line()
.x(d => x_scale(d.date))
.y(d => yScale(d.num)));
});
Finally, we get those scales:
sub_group
.select(".chart_line")
.attr("d", function(d) {
return localLine.get(this)(d)
})
Here is the whole cell, copy/paste this in your notebook, replacing your cell:
chart = {
const panels_per_row = 4;
const panel_width = (width - margin * 8) / panels_per_row;
const height =
margin + (panel_width + margin) * (parseInt(my_data.length / 2) + 1);
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const start_x = 2;
const start_y = panel_width / 3 + margin;
const x_scale = d3
.scaleBand()
.domain(d3.set(series[0].values, d => d.date).values())
.range([0, panel_width]);
const localScale = d3.local();
const localLine = d3.local();
//join
var my_group = svg.selectAll('.chart_group').data(series, d => d.key);
//exit and remove
my_group.exit().remove();
//enter new groups
var enter = my_group
.enter()
.append("g")
.attr("class", "chart_group")
.each(function(d) {
const yScale = localScale.set(this, d3
.scaleLinear()
.domain([0, d3.max(d.values, d => d.num)])
.range([panel_width / 2, panel_width / 2 - start_y - margin]));
localLine.set(this, d3
.line()
.x(d => x_scale(d.date))
.y(d => yScale(d.num)));
});
//append elements to new group
enter.append("rect").attr("class", "group_rect");
enter.append("text").attr("class", "group_text");
enter.append("g").attr("class", "sub_chart_group");
//merge
my_group = my_group.merge(enter);
position_group_elements(my_group);
//join
var sub_group = my_group
.select(".sub_chart_group")
.selectAll('.sub_chart_elements_group')
.data(d => [d.values]); // data is wrapped in an array because this is a line/area chart
//exit and remove
sub_group.exit().remove();
//enter new groups
var sub_enter = sub_group
.enter()
.append("g")
.attr("class", "sub_chart_elements_group");
//append elements to new group
sub_enter.append("path").attr("class", "chart_line");
//merge
sub_group = sub_group.merge(sub_enter);
sub_group
.select(".chart_line")
.attr("d", function(d) {
return localLine.get(this)(d)
})
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("transform", "translate(" + start_x + "," + start_y + ")");
function position_group_elements(my_group) {
//position rectangle
my_group
.select(".group_rect")
.attr("x", function(d, i) {
//two groups per row so
var position = i % panels_per_row;
d.x_pos = position * (panel_width + margin) + margin;
d.y_pos =
parseInt(i / panels_per_row) * (panel_width + margin) + margin;
return d.x_pos;
})
.attr("y", d => d.y_pos)
.attr("fill", "#eee")
.attr("stroke", "#aaa")
.attr("stroke-width", 1)
.attr("width", panel_width)
.attr("height", panel_width);
//then position sub groups
my_group
.select(".sub_chart_group")
.attr("id", d => d.key)
.attr("transform", d => "translate(" + d.x_pos + "," + d.y_pos + ")");
}
return svg.node();
}

d3 line chart - object digging

Trying to build a linechart (multiple lines). Initial data has been an array of object such as:
[{
2010: 8236.082,
countryName: "Afghanistan"
}]
Each line required an array of x/y pairs [[x,y],[x,y]]. My x and y are year and amount of emissions. This means I had to restructure my data it to make it look like this:
[
{
country: "Afganistan",
emissions: [
{ year: 2019, amount: 8236.082 }
]
}
]
However this doesn't work for me. Is the problem in the domain?
Please help.
Codepen
//Define full width, full height and margins
let fullWidth = 600;
let fullHeight = 700;
let margin = {
top: 20,
left: 70,
bottom: 100,
right: 10
}
//Define line chart with and height
let width = fullWidth - margin.left - margin.right;
let height = fullHeight - margin.top - margin.bottom;
//Define x and y scale range
let xScale = d3.scaleLinear()
.range([0, width])
let yScale = d3.scaleLinear()
.range([0, height])
//Draw svg
let svg = d3.select("body")
.attr("width", fullWidth)
.attr("height", fullHeight)
.append("svg")
.append("g")
d3.json("https://api.myjson.com/bins/izmg6").then(data => {
console.log(data);
//Structure data so should be an array of arrays etc [[x,y], [x,y], [x,y]]
let years = d3.keys(data[0]).slice(0, 50);
console.log(years);
let dataset = [];
data.forEach((d, i) => {
let myEmissions = [];
years.forEach(y => {
if (d[y]) {
myEmissions.push({
year: y,
amount: d[y]
})
}
})
dataset.push({
country: d.countryName,
emissions: myEmissions
});
})
console.log(dataset);
//Define x and y domain
xScale
.domain(d3.extent(years, d =>d))
yScale
.domain([d3.max(dataset, d =>
d3.max(d.emissions, d =>
+d.amount)), 0])
//Generate line
let line = d3.line()
.x(d =>
xScale(d.year))
.y(d =>
yScale(d.amount));
let groups = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
groups.append("title")
.text(d => d.country)
groups.selectAll("path")
.data(d => [d.emissions])
.enter()
.append("path")
.attr("d", line)
.attr("class", line)
}).catch(error => console.log(error))
Minor change/typo:
You're assigning height and width to the body and not the svg. Interchanging those 2 lines:
let svg = d3.select("body")
.append("svg")
.attr("width", fullWidth)
.attr("height", fullHeight)
And adding some CSS to the paths:
path.line {
fill: none;
stroke: #000;
}
Here's a fork: https://codepen.io/shashank2104/pen/GwqjVK

Categories