My visual has a scaleBand for the y axis. I have a data set that has categories, but these categories are not at the observational level, so for instance if appending circles I couldn't just do: .attr('cy', function(d) {return yScale(???)}) The ??? denoting that there is nothing in my data at the observational level to split on. Instead, I need to split at the data label level, if that's the proper name for it. Here is the relevant section of code:
var margins = {top:20, left:100, bottom:20, right:20};
var width = 800;
var height = 500;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var xScale = d3.scaleLinear()
.range([0, width]);
var yScale = d3.scaleBand()
.rangeRound([height,0])
.padding(.1)
.paddingInner(.2);
var colorScale = d3.scaleOrdinal()
.range(["#003366","#366092","#4f81b9","#95b3d7","#b8cce4","#f6d18b","#e4a733","#a6a6a6","#d9d9d9","#ffffcc","#b29866"]);
var data = [
{'fmc':'GF', 'aum':66.88, 'aumxmmf':27.62},
{'fmc':'Ping An', 'aum':41.8, 'aumxmmf':10.76},
{'fmc':'Southern', 'aum':80.25, 'aumxmmf':27.47}
];
var xMin = d3.min(data, function(d) {return d.aum});
var xMax = d3.max(data, function(d) {return d.aum});
var fundTypes = ['aum', 'aumxmmf'];
xScale.domain([xMin, xMax]);
yScale.domain(fundTypes);
graphGroup.append('g')
.attr('class', 'x axis')
.attr('transform', "translate("+0+","+height+")")
.call(d3.axisBottom(xScale))
.selectAll('text');
graphGroup.append('g')
.attr('class', 'y axis')
.call(d3.axisRight(yScale))
.selectAll('text')
.attr("transform", "translate(-40,0)");
var aumCircles = graphGroup.selectAll('.aumCirc')
.data(data)
.enter()
.append('circle')
.attr('class','aumCirc')
.attr('cx', function(d) {return xScale(d.aum)})
.attr('cy', yScale('aum'))
.attr('r', 4)
.style('fill', function(d) {return colorScale(d.fmc)});
var aumxmmfCircles = graphGroup.selectAll('.aumxmmfCirc')
.data(data)
.enter()
.append('circle')
.attr('class', 'aumxmmfCirc')
.attr('cx', function(d) {return xScale(d.aumxmmf)})
.attr('cy', yScale('aumxmmf'))
.attr('r',4)
.style('fill', function(d) {return colorScale(d.fmc)});
<script src="https://d3js.org/d3.v5.min.js"></script>
While no error is thrown, the result is not what I expected: the circles are appended in the middle of the two tick marks on the yAxis.
Question
Why is this the default behavior for scaleBand? Shouldn't calling the axis on a static value (like 'aum' still result in being in line with the tick marks? I don't understand why I call the axis the tick marks are in one place but when I call the axis again the circles are in a totally different place.
I concluded the issue isn't because of the wrong g group through troubleshooting
While I could use trial and error with .paddingInner and other padding settings, but I don't think that would be a good long terms solution.
It seems to me that you have two questions here: one regarding how to use the data structure you have and another one regarding the scale. Because asking different, non-related issues in the same question is frowned upon here at S.O., I'll answer just the scale question:
You're using the wrong scale for the task, since a band scale has an associated bandwidth. Use a point scale instead:
var yScale = d3.scalePoint()
//etc...
Here is your code with that change:
var margins = {top:20, left:100, bottom:20, right:20};
var width = 800;
var height = 500;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var xScale = d3.scaleLinear()
.range([0, width]);
var yScale = d3.scalePoint()
.rangeRound([height,0])
.padding(.2);
var colorScale = d3.scaleOrdinal()
.range(["#003366","#366092","#4f81b9","#95b3d7","#b8cce4","#f6d18b","#e4a733","#a6a6a6","#d9d9d9","#ffffcc","#b29866"]);
var data = [
{'fmc':'GF', 'aum':66.88, 'aumxmmf':27.62},
{'fmc':'Ping An', 'aum':41.8, 'aumxmmf':10.76},
{'fmc':'Southern', 'aum':80.25, 'aumxmmf':27.47}
];
var xMin = d3.min(data, function(d) {return d.aum});
var xMax = d3.max(data, function(d) {return d.aum});
var fundTypes = ['aum', 'aumxmmf'];
xScale.domain([xMin, xMax]);
yScale.domain(fundTypes);
graphGroup.append('g')
.attr('class', 'x axis')
.attr('transform', "translate("+0+","+height+")")
.call(d3.axisBottom(xScale))
.selectAll('text');
graphGroup.append('g')
.attr('class', 'y axis')
.call(d3.axisRight(yScale))
.selectAll('text')
.attr("transform", "translate(-40,0)");
var aumCircles = graphGroup.selectAll('.aumCirc')
.data(data)
.enter()
.append('circle')
.attr('class','aumCirc')
.attr('cx', function(d) {return xScale(d.aum)})
.attr('cy', yScale('aum'))
.attr('r', 4)
.style('fill', function(d) {return colorScale(d.fmc)});
var aumxmmfCircles = graphGroup.selectAll('.aumxmmfCirc')
.data(data)
.enter()
.append('circle')
.attr('class', 'aumxmmfCirc')
.attr('cx', function(d) {return xScale(d.aumxmmf)})
.attr('cy', yScale('aumxmmf'))
.attr('r',4)
.style('fill', function(d) {return colorScale(d.fmc)});
<script src="https://d3js.org/d3.v5.min.js"></script>
Related
This is my first time using d3.js, so please bear with me. I am implementing this inside of a vue.js file as pure javascript.
I am trying to make a scatter plot with zooming capabilities. So far I have everything nearly working, but when I zoom I notice that the x-axis isn't scaling properly, but the y-axis is working properly. For instance, when looking at the original plot, a point may be at around 625 on the x-axis, but after zooming in the same point will be less than 600. This is not happening with the y-axis - those points scale properly. I am assuming that something is wrong with the scaling of the x-axis in my zoom function, but I just can't figure it out. Please take a look, and let me know if you can see where I went wrong.
Edit: I should mention that this is using d3.js version 7.4.4
<template>
<div id="reg_plot"></div>
</template>
<script>
import * as d3 from 'd3';
export default {
name: 'regCamGraph',
components: {
d3
},
methods: {
createSvg() {
// dimensions
var margin = {top: 20, right: 20, bottom: 30, left: 40},
svg_dx = 1400,
svg_dy =1000,
chart_dx = svg_dx - margin.right - margin.left,
chart_dy = svg_dy - margin.top - margin.bottom;
// data
var y = d3.randomNormal(400, 100);
var x_jitter = d3.randomUniform(-100, 1400);
var d = d3.range(1000)
.map(function() {
return [x_jitter(), y()];
});
// fill
var colorScale = d3.scaleLinear()
.domain(d3.extent(d, function(d) { return d[1]; }))
.range([0, 1]);
// y position
var yScale = d3.scaleLinear()
.domain(d3.extent(d, function(d) { return d[1]; }))
.range([chart_dy, margin.top]);
// x position
var xScale = d3.scaleLinear()
.domain(d3.extent(d, function(d) { return d[0]; }))
.range([margin.right, chart_dx]);
console.log("chart_dy: " + chart_dy);
console.log("margin.top: " + margin.top);
console.log("chart_dx: " + chart_dx);
console.log("margin.right: " + margin.right);
// y-axis
var yAxis = d3.axisLeft(yScale);
// x-axis
var xAxis = d3.axisBottom(xScale);
// zoom
var svg = d3.select("#reg_plot")
.append("svg")
.attr("width", svg_dx)
.attr("height", svg_dy);
svg.call(d3.zoom().on("zoom", zoom)); // ref [1]
// plot data
var circles = svg.append("g")
.attr("id", "circles")
.attr("transform", "translate(200, 0)")
.selectAll("circle")
.data(d)
.enter()
.append("circle")
.attr("r", 4)
.attr("cx", function(d) { return xScale(d[0]); })
.attr("cy", function(d) { return yScale(d[1]); })
.style("fill", function(d) {
var norm_color = colorScale(d[1]);
return d3.interpolateInferno(norm_color)
});
// add y-axis
var y_axis = svg.append("g")
.attr("id", "y_axis")
.attr("transform", "translate(75,0)")
.call(yAxis).style("font-size", "20px")
// add x-axis
var x_axis = svg.append("g")
.attr("id", "x_axis")
.attr("transform", `translate(${margin.left}, ${svg_dy - margin.bottom})`)
.call(xAxis).style("font-size", "20px")
function zoom(e) {
// re-scale y axis during zoom
y_axis.transition()
.duration(50)
.call(yAxis.scale(e.transform.rescaleY(yScale)));
// re-scale x axis during zoom
x_axis.transition()
.duration(50)
.call(xAxis.scale(e.transform.rescaleX(xScale)));
// re-draw circles using new y-axis scale
var new_xScale = e.transform.rescaleX(xScale);
var new_yScale = e.transform.rescaleY(yScale);
console.log(d);
x_axis.call(xAxis.scale(new_xScale));
y_axis.call(yAxis.scale(new_yScale));
circles.data(d)
.attr('cx', function(d) {return new_xScale(d[0])})
.attr('cy', function(d) {return new_yScale(d[1])});
}
}
},
mounted() {
this.createSvg();
}
}
</script>
Interestingly enough, after I set the clip region to prevent showing points outside of the axes the problem seemed to resolve itself. This is how I created the clip path:
// clip path
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("x", "0")
.attr("y", "0")
.attr('width', chart_dx)
.attr('height', chart_dy);
And I then added that attribute to the svg when plotting the data like this:
svg.append("g").attr("clip-path", "url(#clip)")
Updated clip path with plot data section:
// clip path
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("x", "0")
.attr("y", "0")
.attr('width', chart_dx)
.attr('height', chart_dy);
// plot data
var circles = svg.append("g")
.attr("id", "circles")
.attr("transform", "translate(75, 0)")
.attr("clip-path", "url(#clip)") //added here
.selectAll("circle")
.data(d)
.enter()
.append("circle")
.attr("r", 4)
.attr("cx", function(d) { return xScale(d[0]); })
.attr("cy", function(d) { return yScale(d[1]); })
.style("fill", function(d) {
var norm_color = colorScale(d[1]);
return d3.interpolateInferno(norm_color)
});
I ended up resolving this issue. I have updated the original post to show what worked for me.
Basically, after adding the clip region things started to work properly.
// clip path (this is the new clip region that I added. It prevents dots from being drawn outside of the axes.
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("x", "0")
.attr("y", "0")
.attr('width', chart_dx)
.attr('height', chart_dy);
// plot data
var circles = svg.append("g")
.attr("id", "circles")
.attr("transform", "translate(75, 0)")
.attr("clip-path", "url(#clip)") //added clip region to svg here
.selectAll("circle")
.data(d)
.enter()
.append("circle")
.attr("r", 4)
.attr("cx", function(d) { return xScale(d[0]); })
.attr("cy", function(d) { return yScale(d[1]); })
.style("fill", function(d) {
var norm_color = colorScale(d[1]);
return d3.interpolateInferno(norm_color)
});
I started the D3.js challenge on FreeCodeCamp, the problem is that I solved it with the chart but it only gives me a display on the rectum, only one with the width and height that it I put, I'll show the code below.
The entire code on
<script>
//set d3
var w = 1000, h = 500;
var padding = 50;
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h)
//title
svg.append('text')
.attr('x', w / 2)
.attr('y', 50)
.text('United States GDP')
fetch('https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json')
.then((result)=>result.json())
.then((data)=>{
var the_data = data['data']
//get vals
var get_max = d3.max(data['data'])
var get_mix = d3.min(data['data'])
//for x
var max_x = Number(get_max[0].split('-')[0])
var min_x = Number(get_mix[0].split('-')[0])
//for y
var max_y = get_max[1]
var min_y = get_mix[1]
var xScale = d3.scaleLinear()
.domain([min_x, max_x])
.range([padding, w-padding])
var yScale = d3.scaleLinear()
.domain([min_y, max_y])
.range([h-padding, padding])
//the_chars
for(var i in the_data){
var get_year = Number(the_data[i][0].split('-')[0])
the_data[i][0] = get_year
}
svg.selectAll('rect')
.data(the_data)
.enter()
.append('rect')
.attr("x", (d) => { xScale(d[0]) })
.attr('y', (d)=>{ yScale(d[1]) })
.attr("width", 200)
.attr("height", 20)
//axis
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
//display axis
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append('g')
.attr('transform', 'translate(' + padding + ', 0)')
.call(yAxis)
})
Now, what I need to do to display the charts!
I mention that the script tags are embedded in the body
Problem: Arrow functions without a return value. Solution: Instead use an explicit or an implicit return.
.attr("x", (d) => { xScale(d[0]) }) // returns undefined
.attr("x", (d) => xScale(d[0])) // implicit return
.attr("x", (d) => { return xScale(d[0]) }) // explicit return
Problem: Fixed height value. Solution Evaluate the height of each based on the GDP value (d[1]) instead.
.attr('height', 20) // fixed height
.attr('height', d => yScale(min_y) - yScale(d[1]))
// subtract from min range to account for padding and inverted y coordinates in SVG
Full solution in this codepen
I'm trying to make a bar chart but I can't figure out a way to make the bar start from the 0 point of y axis and not from the very bottom of the svg. 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 = 900;
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
var arr = [];
d3.json(url, function(data) {
for (let i = 0; i < data.data.length; i++) arr[i] = data.data[i];
const yScale = d3.scaleLinear()
.domain([0, d3.max(arr, (d) => d[1])])
.range([height - padding, padding]);
const yAxis = d3.axisLeft(yScale);
svg.append('g')
.attr('transform', 'translate(' + padding + ', 0)')
.call(yAxis)
svg.selectAll('rect')
.data(arr)
.enter()
.append('rect')
.attr('fill', 'blue')
.attr('height', d => d[1] + padding)
.attr('width', 2.909090909090909)
.attr('x', (d, i) => padding + (3.2 * i))
.attr('y', d => yScale(d[1]))
.append('title')
.text(d => d[1])
});
<script src="https://d3js.org/d3.v4.min.js"></script>
You are incorrectly calculating the height of the rectangle, and not using your scale. It's also trickier since your use of padding is not the typical D3 convention.
svg.selectAll('rect')
.data(arr)
.enter()
.append('rect')
.attr('fill', 'blue')
.attr('height', d => height - padding - yScale(d[1]))
Have spent the last 2 days looking through stackoverflow and online examples as to why my charts aren't displaying properly.
I'm sure I'm missing something in terms of the scaling portion of the code. If I copy the dark part at the bottom of the x-Axis on the chart to notepad it gives me all of the x-axis elements.
Can anyone point me in the right direction?
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded',function(){
req.open("GET",'https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json',true);
req.send();
req.onload=function(){
json=JSON.parse(req.responseText);
document.getElementsByClassName('title')[0].innerHTML=json.name;
dataset=json.data;
const w = 500;
const h = 300;
const padding = 10;
// create an array with all date names
const dates = dataset.map(function(d) {
return d[0];
});
const xScale = d3.scaleBand()
.rangeRound([padding, w-padding])
.padding([.02])
.domain(dates);
console.log("Scale Bandwidth: " + xScale.bandwidth());
const yScale = d3.scaleLinear()
.rangeRound([h-padding, padding])
.domain(0,d3.max(dataset, (d)=>d[1]));
console.log("Dataset Max Height: " + d3.max(dataset, (d)=>d[1]));
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
const svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width",(d,i)=>xScale.bandwidth())
.attr("height",(d,i)=>(h-yScale(d[1])))
.attr("x", (d,i)=>xScale(d[0]))
.attr("y", (d,i)=>yScale(d[1]))
.attr("fill", "navy")
.attr("class", "bar");
};
});
</script>
<h1 class="title">Title Will Go Here</h1>
</body>
D3 now uses Promises instead of asynchronous callbacks to load data. Promises simplify the structure of asynchronous code, especially in modern browsers that support async and await.
Changes in D3 5.0
Also, you are right in that your yScale is broken. Linear scales need a range and a domain, each being passed a 2 value array.
const yScale = d3.scaleLinear()
.range([h - padding, padding])
.domain([0, d3.max(dataset, (d) => d[1])]);
document.addEventListener('DOMContentLoaded', async function() {
const res = await d3.json("https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json");
//console.log(res.data)
const dataset = res.data
const w = 500;
const h = 300;
const padding = 10;
// create an array with all date names
const dates = dataset.map(function(d) {
return d[0];
});
const max = d3.max(dataset, function(d) { return d[1]} )
const xScale = d3.scaleBand()
.rangeRound([0, w])
.padding([.02])
.domain(dates);
console.log("Scale Bandwidth: " + xScale.bandwidth());
const yScale = d3.scaleLinear()
.range([h - padding, padding])
.domain([0, d3.max(dataset, (d) => d[1])]);
console.log("Dataset Max Height: " + d3.max(dataset, (d) => d[1]));
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
const svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.append("g")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("width", (d, i) => xScale.bandwidth())
.attr("height", (d, i) => (h - yScale(d[1])) )
.attr("x", (d, i) => xScale(d[0]))
.attr("y", (d, i) => yScale(d[1]))
.attr("fill", "navy")
.attr("class", "bar");
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>
Codepen
I have a series of paired xy coordinates that create 58 lines. I want to plot them on a Cartesian graph, values are between -5 and 5 on both axis, essentially making a scatter plot of lines. I have made something similar in matplotlib using the quiver function, but I want to be able to do this in D3. I would also like to be able to label each line, or each line that meets a length threshold. The code I have come up with below. Thanks.
var lisa = [["Eloy",0.0169808,-0.695317,-0.0510301,-0.6995938],
["Florence",-0.3465685,-0.6790588,-0.5869514,-0.6762134],
["Phoenix",0.677068,-0.5754814,-0.6052215,-0.6158059],
["Tucson",-0.663848,0.4111043,-0.6722116,0.011639]]
var w = 200;
var h = 200;
//create the svg element and set the height and width parameters
var svg = d3.select("div").select("div")
.append("svg")
.attr("height",h)
.attr("width", w)
.style("border", "1px solid black");
//Create the scale for the scatter plot
var xScale = d3.scale.linear()
.domain([d3.min(dataset, function(d) { return d[0];}),d3.max(dataset, function(d) { return d[0];})])
.range([-1,1]);
var yScale = d3.scale.linear()
.domain([d3.min(dataset, function(d) { return d[1];}),d3.max(dataset, function(d) { return d[1];})])
.range([-1,1]);
//This is the function that creates the SVG lines
var line = svg.selectAll("line")
.data(lisa)
.enter()
.append("line");
//This gets the cooresponding x,y cordinates from the dataset
line.attr("x1", function(d) {
return xScale(d[0]);
})
.attr("y1", function(d) {
return yScale(d[1]);
})
.attr("x2", function(d) {
return xScale(d[2]);
})
.attr("y2", function(d) {
return yScale(d[3]);
})
.attr("stroke", "black");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Your code has some problems:
First, your range right now ([-1, 1]) makes no sense. This should be the domain instead (I changed the ranges to [0, w] and [0, h]).
In your real code, the domain should be [-5, 5] and the range should be the limits of the plot, something like [leftLimit, rightLimit] and [topLimit, bottomLimit] (have in mind that, in an SVG, the 0 position for the y axis is the top, not the bottom).
Second, given this array:
["Tucson",-0.663848,0.4111043,-0.6722116,0.011639]
your x and y positions should be the indices 1,2,3 and 4, not 0, 1, 2 and 3.
Besides that changes, I added the labels:
var text = svg.selectAll(".text")
.data(dataset)
.enter()
.append("text");
text.attr("font-size", 10)
.attr("x", function(d) {
return xScale(d[1]);
})
.attr("y", function(d) {
return yScale(d[2]);
})
.text(d => d[0]);
Here is the demo with the corrections:
var dataset = [["Eloy",0.0169808,-0.695317,-0.0510301,-0.6995938],
["Florence",-0.3465685,-0.6790588,-0.5869514,-0.6762134],
["Phoenix",0.677068,-0.5754814,-0.6052215,-0.6158059],
["Tucson",-0.663848,0.4111043,-0.6722116,0.011639]];
var color = d3.scale.category10();
var w = 400;
var h = 300;
//create the svg element and set the height and width parameters
var svg = d3.select("body")
.append("svg")
.attr("height",h)
.attr("width", w)
.style("border", "1px solid black");
//Create the scale for the scatter plot
var xScale = d3.scale.linear()
.domain([-1,1])
.range([0,w]);
var yScale = d3.scale.linear()
.domain([-1,1])
.range([0,h]);
//This is the function that creates the SVG lines
var line = svg.selectAll("line")
.data(dataset)
.enter()
.append("line");
//This gets the cooresponding x,y cordinates from the dataset
line.attr("x1", function(d) {
return xScale(d[1]);
})
.attr("y1", function(d) {
return yScale(d[2]);
})
.attr("x2", function(d) {
return xScale(d[3]);
})
.attr("y2", function(d) {
return yScale(d[4]);
})
.attr("stroke-width", 2)
.attr("stroke", (d,i)=>color(i));
var text = svg.selectAll(".text")
.data(dataset)
.enter()
.append("text");
text.attr("font-size", 10)
.attr("x", function(d) {
return xScale(d[1])+2;
})
.attr("y", function(d) {
return yScale(d[2]) + 4;
})
.text(d=>d[0]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>