I'm trying to create a responsive scatterplot with D3js using percentages for the width and height of the chart.
So, I have these variables declared up top:
var w = '100%';
var h = '100%';
My xScale and yScale work properly, along with the placement of my output circles in the SVG:
var xScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d["ttc"]; })])
.range([0, w]);
var yScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d["ctc"]; })])
.range([0, h]);
var svg = d3.select(el)
.append('svg')
.attr('height', h)
.attr('width', w);
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', function(d) { return xScale(d["ttc"]); })
.attr('cy', function(d) { return yScale(d["ctc"]); })
.attr('r', 10);
However, due to the nature of SVGs, the circles are displayed from the top left corner rather than the typical origin in the bottom left. In order to reverse this, usually I would switch the yScale range values, to be [h, 0] instead of [0, h]. When I do this, I get an error Error: Invalid value for <circle> attribute cy="NaN". What can I do to fix this and have the plot work with percentages?
You can declare width and height as numbers, like var h = 100; and then add percentages in the attr function:
.attr('cx', function (d) {
return xScale(d["ttc"]) + '%';
})
Then your svg could be styled with:
.attr('height', h + '%')
.attr('width', w + '%')
Or just use CSS for that. Here's a demo.
Related
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>
I have a scatter plot made in d3.v3 and no matter how large i increase the width and height variables it does not take up more screen space.
var w = 700;
var h = 700;
var dataset = [
"Over 50 pairs of coordinates that look like [0,1][0,43],
];
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
.attr("width",5)
.attr("height",5);
There are more than 50 coordinates in my dataset and i want to be able to display them well so that is why i want this to take up more screen space. Currently there is nothing in my html, and no css for this. How can i adjust this so that the scatter plot takes more screen space?
The code you show doesn't place data points with any consideration of width or height, it places data points based on the values in the data:
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
The x and y attributes, without any SVG transformation, expect pixel values. If a point has the datum [25,25] it will be placed 25 pixels from the top and left. Height and width of the svg do not matter - you are stating the pixel position based on the data, not based on the svg dimensions in combination with the data.
Scales are a fundamental part of d3 - you can scale the x and y values of your data points across a range of pixel values. To do this we need to know the domain of the input data - the extent of input values - and the range of the output values - the extent of output values.
There are a number of scales built into D3, including power, logarithmic and linear. I'll demonstrate a linear scale below, but the form is very similar for other continuous scales, such as those noted above.
var xScale = d3.scaleLinear() // create a new linear scale
.domain([0,50]) // map values from 0 to 50 to:
.range([0,width]) // to pixel values of 0 through width
var yScale = d3.scaleLinear() // create a new linear scale
.domain([0,50]) // map values from 0 to 50 to:
.range([height,0]) // to pixel values of height through 0
Since in SVG coordinate space y=0 is the top of the SVG, and normally we want data with y=0 to be displayed at the bottom of the graph, we use a range of [height,0] rather than [0,height]
With the scales set up, to return the pixel value for a given data value we use:
xScale(d[0]); // assuming d[0] holds the x value
or
yScale(d[1]); // assuming d[1] holds the y value
Together this gives us:
var w = 700;
var h = 700;
var dataset = d3.range(50).map(function(d) {
return [Math.random()*50,Math.random()*50];
})
var x = d3.scaleLinear()
.domain([0,50]) // input extent
.range([0,w]) // output extent
var y = d3.scaleLinear()
.domain([0,50])
.range([h,0])
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d) {
return x(d[0]);
})
.attr("y", function(d) {
return y(d[1]);
})
.attr("width",5)
.attr("height",5);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Of course we might not know the domain of the data if its dynamic, so d3 has a few built in helpers including d3.min, d3.max, d3.extent.
d3.min and d3.max iterate through an array to find the minimum and maximum values of some property of each item in the data array:
d3.min(dataArray, function(d) {
return d.property;
})
d3.max(dataArray, function(d) {
return d.property;
})
D3.extent does both at the same time returning an array containing min and max:
d3.extent(dataArray, function(d) {
return d.property;
})
We can plug those into the scales too:
var w = 700;
var h = 700;
var dataset = d3.range(50).map(function(d) {
return [Math.random()*50,Math.random()*50];
})
var x = d3.scaleLinear()
.domain([0,d3.max(dataset, function(d) { return d[0]; }) ]) // input extent
.range([0,w]) // output extent
var y = d3.scaleLinear()
.domain( d3.extent(dataset, function(d) { return d[1]; }) )
.range([h,0])
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d) {
return x(d[0]);
})
.attr("y", function(d) {
return y(d[1]);
})
.attr("width",5)
.attr("height",5);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
The below works perfectly fine for me. The things you might need to check is :
The coordinates range is more than 5. (or you might need to use scale and axis)
Is there a overriding styles to your g
var w = 700;
var h = 700;
var dataset = [[10,10],[10,30],[30,50],[30,70]];
//Create SVG element
var svg = d3.select("#scatterplot")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
.attr("width",5)
.attr("height",5);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id='scatterplot'></div>
To start, I am fairly new to D3.Js. I have spent the past week or so working on a D3.JS issue-specifically making a graph with a Y-axis label. However, I cannot get the graph exactly how I want. It is almost there but inverted or my data comes out wrong. Now I will briefly show some of my code and images of my main problem before showing all of the code. I have spent time looking at other Stack Overflow posts with a similar issue and I do what is on those posts and still have the same issue.
For example, I thought that this post would have the solution: reversed Y-axis D3
The data is the following:
[0,20,3,8] (It is actually an array of objects but I think this may be all that is needed.
So, to start, when the yScale is like this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
The bar chart looks like this:
As one can see the Y chart starts with zero at the top and 20 at the bottom-which at first I thought was an easy fix of flipping the values in the domain around to this:
var yScale = d3.scaleLinear()
.domain([0, maxPound]) //Value of maxpound is 20
.range([0, 350]);
I get this image:
In the second image the y-axis is right-20 is on top-Yay! But the graphs are wrong. 0 now returns a value of 350 pixels-the height of the SVG element. That is the value that 20 should be returning! If I try to switch the image range values, I get the same problem!
Now the code:
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {top: 5, right: 200, bottom: 70, left: 25}
var maxPound = d3.max(poundDataArray,
function(d) {return parseInt(d.Pounds)}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([maxPound, 0])
.range([0, h]);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select(".pounds")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i){
return i * (w / poundDataArray.length);
})
.attr('y', function(d) {
return 350 - yScale(d.Pounds);
})
.attr('width', (w / 4) - 25)
.attr('height', function(d){
return yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.call(yAxis);
Thank you for any help! I believe that the error may be in the y or height values and have spent time messing around there with no results.
That is not a D3 issue, but an SVG feature: in an SVG, the origin (0,0) is at the top left corner, not the bottom left, as in a common Cartesian plane. That's why using [0, h] as the range makes the axis seem to be inverted... actually, it is not inverted: that's the correct orientation in an SVG. By the way, HTML5 Canvas has the same coordinates system, and you would have the same issue using a canvas.
So, you have to flip the range, not the domain:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h, 0]);//the range goes from the bottom to the top now
Or, in your case, using the margins:
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
Besides that, the math for the y position and height is wrong. It should be:
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
Also, as a bonus tip, don't hardcode the x position and the width. Use a band scale instead.
Here is your code with those changes:
var poundDataArray = [{
Pounds: 10
}, {
Pounds: 20
}, {
Pounds: 5
}, {
Pounds: 8
}, {
Pounds: 14
}, {
Pounds: 1
}, {
Pounds: 12
}];
var w = 350;
var h = 350;
var barPadding = 1;
var margin = {
top: 5,
right: 20,
bottom: 70,
left: 25
}
var maxPound = d3.max(poundDataArray,
function(d) {
return parseInt(d.Pounds)
}
);
//Y-Axis Code
var yScale = d3.scaleLinear()
.domain([0, maxPound])
.range([h - margin.bottom, margin.top]);
var xScale = d3.scaleBand()
.domain(d3.range(poundDataArray.length))
.range([margin.left, w - margin.right])
.padding(.2);
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Creating SVG element
var svg = d3.select("body")
.append('svg')
.attr("width", w)
.attr('height', h)
.append("g")
.attr("transform", "translate(" + margin.left + "," +
margin.top + ")");
svg.selectAll("rect")
.data(poundDataArray)
.enter()
.append("rect")
.attr('x', function(d, i) {
return xScale(i);
})
.attr('y', function(d) {
return yScale(d.Pounds);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return h - margin.bottom - yScale(d.Pounds);
})
.attr('fill', 'steelblue');
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + margin.left + ",0)")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
I'm trying to learn how to use the d3.js hexbin plugin.
I started with the example: http://bl.ocks.org/mbostock/4248145 , and I'm adapting it.
I have a data set of points between [0,0] and [600,600]. I want to output them to a 300,300 graph.
My graph doesn't look right. It looks like the data isn't being scaled properly and the graph is only showing 1/4 of the data. Can someone tell me what's wrong? I've read a book about using d3, but I don't have very much experience using it.
Jsfiddle of my hexbin
var graph_width = 300;
var graph_height = 300;
var data_width = 600;
var data_height = 600;
var randomX = d3.random.normal(data_width / 2, 80),
randomY = d3.random.normal(data_height / 2, 80),
points = d3.range(2000).map(function() { return [randomX(), randomY()]; });
var color = d3.scale.linear()
.domain([0, 20])
.range(["white", "steelblue"])
.interpolate(d3.interpolateLab);
var hexbin = d3.hexbin()
.size([graph_width, graph_height])
.radius(20);
var x = d3.scale.linear()
.domain([0, data_width])
.range([0, graph_width]);
var y = d3.scale.linear()
.domain([0, data_height])
.range([0, graph_height]);
var svg = d3.select("body").append("svg")
.attr("width", graph_width)
.attr("height", graph_height)
.append("g");
svg.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("class", "mesh")
.attr("width", graph_width)
.attr("height", graph_height);
svg.append("g")
.attr("clip-path", "url(#clip)")
.selectAll(".hexagon")
.data(hexbin(points))
.enter().append("path")
.attr("class", "hexagon")
.attr("d", hexbin.hexagon())
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.style("fill", function(d) { return color(d.length); });
I think I understand. You have data values in in the range of 0 to 600 but want those mapped to x/y positions in the range of 0 to 300.
If that's it then scale the points:
var x = d3.scale.linear()
.domain([0, data_width])
.range([0, graph_width]);
var y = d3.scale.linear()
.domain([0, data_height])
.range([0, graph_height]);
var randomX = d3.random.normal(data_width / 2, 80),
randomY = d3.random.normal(data_height / 2, 80),
points = d3.range(2000).map(function() { return [x(randomX()), y(randomY())]; });
Updated fiddle.
I am trying to build a stacked bar chart in d3.
Here's my jsfiddle:
http://jsfiddle.net/maneesha/gwjkgruk/4/
I'm trying to fix it so that the y-axis starts at zero.
I thought this is where that was set up and this is how it should be:
yScale = d3.scale.linear()
.domain([0, yMax])
.range([0, height]),
but it doesn't work. Changing domain to this gets the scale right but messes up all my rects.
yScale = d3.scale.linear()
.domain([yMax, 0])
.range([0, height]),
What am I doing wrong?
Try to modify your scale like that:
yScale = d3.scale.linear()
.domain([0,yMax])
.range([height,0]),
and then when you fill the rect:
.attr('y', function (d) {
return height - yScale(d.y0);
})
.attr('height', function (d) {
return height - yScale(d.y);
})