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.
Related
I am building a SVG visualisation using d3.js (in PowerBI - so version 3 of d3), and I am struggling aligning my data points and fixed lines with the appropriate y-axis tick marker.
For example, with 8 axis points, the lines are almost right, just slightly above
But when there is only 1 or 2 points, it's way off
I am trying to dynamically calculate the offset as the number of y-axis ticks will depend on the PowerBI filter I have.
My current calculation is that I am taking the height of the svg, dividing it by the number of ticks, and then dividing that by two so it lands in the centre. The y-axis is ordinal.
Relevant code is:
var margin = {top: 20, right: 250, bottom: 50, left: 50},
width = pbi.width - margin.left - margin.right,
height = pbi.height - margin.top - margin.bottom,
legendleft = pbi.width - margin.right;
var y = d3.scale.ordinal()
.rangeRoundBands([0, height], barPad, barOuterPad);
var svg = d3.select("#chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom + legendleft)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
y.domain(Data.map(function(d) { return d.reportingyear; }));
var YearSize = new Set(yearArray).size // Gets the number of ticks on the y-axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0, 6)")
.call(d3.svg.axis().scale(y).orient("left"));
// Chart the rows
var publishedRow = svg.append("g").attr("id", "publishedgroup").selectAll(null)
.data(rowArray)
.enter();
publishedRow.append("line")
.style("stroke", "grey")
.style("stroke-width", 1)
.style("stroke-dasharray", ("2, 2"))
.attr("x1", function(d) { return 0; })
.attr("y1", function(d) { return y(d.entry)+((pbi.height-margin.top-margin.bottom) / (new Set(yearArray).size) / 2); })
.attr("x2", function(d) { return width; })
.attr("y2", function(d) { return y(d.entry)+((pbi.height-margin.top-margin.bottom) / (new Set(yearArray).size) / 2); });
publishedRow.append("circle")
.attr("cx", function(d) {return x(new Date(d.date))} )
.attr("cy", function(d) {return y(d.year)+((pbi.height-margin.top-margin.bottom) / (new Set(yearArray).size) / 2); })
.attr("r", 7)
.style("fill", function(d, i) { return milestoneMap[d.milestone]; })
});
It is the .attr lines that have the code that dynamically calculates the offset.
Is there an easier way to do this? Or can I get some advice as to why my calculation isn't working?
Thank you!
I should have used
.rangePoints rather than .rangeRoundBands
Then a static offset of 6 worked.
Problem solved
I am trying to plot a multi-line graph with d3.js but I am getting this error:
Error: attribute d: Expected moveto path command ('M' or 'm'), "function t(t){va…".
I have been stuck at it a while now and tried everything I could think of, to help you in your reflections, Find the code that I use below.
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 60},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
const dateParser = d3.timeFormat("%Y-%m-%d %H:%M:%S");
data.forEach(function(d) {
d.ts = dateParser(new Date(d.ts));
d.value = parseFloat(d.value)
});
// append the svg object to the body of the page
var svg = d3.select("#graph")
.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 + ")");
//Read the data
// group the data: I want to draw one line per group
var sumstat = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d.key;})
.entries(data);
// Add X axis --> it is a date format
var x = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.ts; }))
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(5));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.value; })])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));
// color palette
var res = sumstat.map(function(d){ return d.key }) // list of group names
var color = d3.scaleOrdinal()
.domain(res).range(['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628','#f781bf','#999999'])
// Draw the line
svg.selectAll(".line")
.data(sumstat)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 1.5)
.attr("d", function(d){
return d3.line()
.x(function(d) { return x(d.ts); })
.y(function(d) { return y(d.value); })
})
I cannot figure what I am doing wrong, in case the example I am looking at is this link. I am fairly new to d3 and it is not an easy library to use
When you set the d attribute, you return the line generator itself from the data linking function, but you fail to execute it. Configuring the generator and executing it is a two-step process:
first, contruct and configure the generator (line is a function)
var line = d3.line()
.x(function(d) { return x(d.ts); })
.y(function(d) { return y(d.value); })
then, pass the function to attr(), so it will be executed as line(d)
svg.selectAll(".line")
.data(sumstat)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function(d){ return color(d.key) })
.attr("stroke-width", 1.5)
.attr("d", line)
I've got my d3 code and it's displaying no errors in the console but it's also not showing my bar chart. I'm a bit lost as to where I'm going wrong since I'm not getting any errors and it is reading in my data. Any guidance would be appreciated.
async function drawBars() {
//setting the margin of svg
const margin = {top: 50, right: 50, bottom: 50, left: 50};
//set the width and heigh using the current width and height of the div
const width = 600 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;
//create svg and append to visualisation div
const svg = d3.select("#visualisation").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 the ranges for the scales
const x = d3.scaleBand()
.range([0, width]);
const y = d3.scaleLinear()
.range([height, 0]);
const data = await d3.csv("./../remote_work.csv")
console.log(data);
//scale the range of the data in the domains
x.domain(data.map(function(d) {
return d.Benefits;
}));
y.domain([0, d3.max(data, function(d) {
return d.Percentage;
})]);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x(d.Benefits);
})
.attr("width", x.bandwidth())
.attr("y", function(d) {
return y(d.Percentage);
})
.attr("height", function(d) {
return height - y(d.Percentage);
});
//add the axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
}
drawBars()
You need to cast the d.Percentage to a number using either Number(d.Percentage) or +d.Percentage.
This is because the CSV file reader doesn't know whether any part of it is a number, whether it is a string that just happens to be made up completely of numbers, or whether this is the only value that is also a number, and the same column in another row is actually a string.
That is why it doesn't take any chances and just treats everything as a string. This means that it's up to you to make sure the data is of the right type.
I'd do this by changing your code to something like this:
const data = (await d3.csv(...).then(
rows => rows.map(d => ({
Benefits: d.Benefits,
Percentage: Number(d.Percentage)
})));
My working snippet with generated data:
const data = ["Lease car", "401(k)", "Flexible hours"]
.map(v => ({
Benefits: v,
Percentage: Math.random().toString()
}))
.map(d => ({
Benefits: d.Benefits,
Percentage: Number(d.Percentage)
}));
function drawBars() {
//setting the margin of svg
const margin = {
top: 50,
right: 50,
bottom: 50,
left: 50
};
//set the width and heigh using the current width and height of the div
const width = 600 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;
//create svg and append to visualisation div
const svg = d3.select("#visualisation").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 the ranges for the scales
const x = d3.scaleBand()
.range([0, width]);
const y = d3.scaleLinear()
.range([height, 0]);
//scale the range of the data in the domains
x.domain(data.map(function(d) {
return d.Benefits;
}));
y.domain([0, d3.max(data, function(d) {
return d.Percentage;
})]);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x(d.Benefits);
})
.attr("width", x.bandwidth())
.attr("y", function(d) {
return y(d.Percentage);
})
.attr("height", function(d) {
return height - y(d.Percentage);
});
//add the axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
}
drawBars()
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="visualisation"></div>
I am in the process of making a simple set of boxplots on a graph, but for some reason the line elements are not showing up
Code is:
var margin = {
top: 10,
right: 30,
bottom: 30,
left: 40
},
width = 900 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var svgBox = 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 + ")");
var yScale = d3.scaleLinear()
.range([height, 0]);
var xScale = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(0.05)
.align(0.1);
var center = 200
var width = 100
d3.csv("boxPlotData.csv", function(dataset) {
var max = d3.max(dataset, function(d) {
return +d.max;
});
yScale.domain([0, max])
xScale.domain(dataset.map(function(d) {
return d.borough;
}));
svgBox.append("g").call(d3.axisLeft(yScale));
svgBox.append("g").attr("transform", "translate(0," + height + ")").call(d3.axisBottom(xScale));
svgBox.selectAll("line").data(dataset).enter()
.append("line")
.attr("x1", d => xScale(d.borough) + width / 3.5)
.attr("x2", d => xScale(d.borough) + width / 3.5)
.attr("y1", d => yScale(+d.min))
.attr("y2", d => yScale(+d.max))
.attr("stroke", "black");
svgBox.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", d => xScale(d.borough) + width / 3.5)
.attr("y", d => yScale(+d.q3))
.attr("height", d => (yScale(+d.q1) - yScale(+d.q3)))
.attr("width", width)
.attr("stroke", "black")
.style("fill", "#69b3a2");
});
And my data is of the form
Data CSV
The code is entering the "rect" elements as expected, but the line components aren't showing up anywhere in the html?
The issue is you cannot just use svgBox.selectAll("line") because it will select the axis tick marks as well as the axis lines. Instead, I suggest add a class to your line with attr("class", "line") and use svgBox.selectAll(".line") to specifically select lines to append.
So the line append snippet should be:
svgBox.selectAll(".line")
.data(dataset)
.enter()
.append("line")
.attr("class", "line")
.attr("x1", d => xScale(d.borough) + width / 3.5)
.attr("x2", d => xScale(d.borough) + width / 3.5)
.attr("y1", d => yScale(+d.min))
.attr("y2", d => yScale(+d.max))
.attr("stroke", "black");
Here is the working snippet Block: https://bl.ocks.org/akulmehta/4b29fb357ea7f02a1b47b611e03a5468/
Spent hours on this and still not really sure whats going wrong.
My plot is supposed to update based on a bunch of parameters the user selects. When the plot needs to add new data points the new points are not displayed correctly on the plot.
Check out the new plot:
With these parameters all the circles should be in a line. While the original "line" is in the correct location the new "line" does not match up with the grid.
Here is the function to make a new plot. This works fine, all the data points are where they should be.
export const newPlot = (Params) => {
d3.selectAll("svg").remove();
let margin = {top: 50, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
let x = d3.scaleLinear().range([0, width]);
let y = d3.scaleLinear().range([height, 0]);
let svg = d3.select('.plot').append("svg")
.attr('class', 'svgPlot')
.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(`../assets/data/${Params.type}${Params.Year}.json`, (error, data) => {
if (error) throw error;
const refinedData = parametrize(data, Params);
refinedData.forEach((d) => {
d[Params.xSelect] = Number(d[Params.xSelect]);
d[Params.ySelect] = Number(d[Params.ySelect]);
});
let min = d3.min(refinedData,(d) => d[Params.xSelect]);
x.domain([(min - 2 <= 0 ? 0 : min - 2),
d3.max(refinedData,(d) => d[Params.xSelect])]);
y.domain([0, d3.max(refinedData,(d) => d[Params.ySelect])]);
svg.selectAll("circles")
.data(refinedData)
.enter().append("circle")
.attr('id', (d) => `${d.Player}`)
.attr("r", 5)
.attr("cx", (d) => x((d[Params.xSelect])) )
.attr("cy", (d) => y((d[Params.ySelect])) );
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y));
svg.append('text')
.attr("class", "label")
.attr('id', 'xlabel')
.attr("transform","translate(" + (width - 20) + " ," + (height-5) + ")")
.style("fill", "white")
.style("text-anchor", "middle")
.text(`${Params.xSelect}`);
svg.append('text')
.attr("class", "label")
.attr('id', 'ylabel')
.attr("transform", "rotate(-90)")
.attr("y", 1)
.attr("x", (height/2 - 250))
.attr("dy", "1em")
.style("font-family", "sans-serif")
.style("fill", "white")
.style("text-anchor", "middle")
.text(`${Params.ySelect}`);
});
};
Here is the update function. Circles that are added are not in the correct location and are all offset by the same amount.
export const rePlot = (Params) => {
let margin = {top: 50, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
let xUp = d3.scaleLinear().range([0, width]);
let yUp = d3.scaleLinear().range([height, 0]);
let tooltip = d3.select("body").append("div")
.attr("class", "toolTip")
.style("display", "none");
let svg = d3.select('.svgPlot');
d3.json(`../assets/data/${Params.type}${Params.Year}.json`, (error, data) => {
if (error) throw error;
const refinedData = parametrize(data, Params);
refinedData.forEach((d) => {
d[Params.xSelect] = Number(d[Params.xSelect]);
d[Params.ySelect] = Number(d[Params.ySelect]);
});
let min = d3.min(refinedData,(d) => d[Params.xSelect]);
xUp.domain([(min - 2 <= 0 ? 0 : min - 2),
d3.max(refinedData,(d) => d[Params.xSelect])]);
yUp.domain([0, d3.max(refinedData,(d) => d[Params.ySelect])]);
svg.select('.x-axis')
.transition()
.duration(1000)
.call(d3.axisBottom(xUp));
svg.select('.y-axis')
.transition()
.duration(1000)
.call(d3.axisLeft(yUp));
svg.select('#xlabel')
.text(`${Params.xSelect}`);
svg.select('#ylabel')
.text(`${Params.ySelect}`);
let circle = svg.selectAll("circle")
.data(refinedData);
circle.exit()
.transition()
.remove();
circle.transition()
.duration(1000)
.attr("r", 5)
.attr("cx", (d) => xUp((d[Params.xSelect])) )
.attr("cy", (d) => yUp((d[Params.ySelect])) );
circle.enter().append("circle")
.attr('id', (d) => `${d.Player}`)
.attr("r", 5)
.attr("cx", (d) => xUp((d[Params.xSelect])) )
.attr("cy", (d) => yUp((d[Params.ySelect])) );
});
}
Your first set of circles gets appended to a group that is translated:
let svg = d3.select('.plot').append("svg")
.attr('class', 'svgPlot')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")");
In this case, the svg variable refers to a translated group. However, when you later reselect, you actually append to the root SVG element:
let svg = d3.select('.svgPlot');
This is the origin of the difference.