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>
Related
I'm new to D3 and need a simple scatterplot.
The problem is that the data is not showing up where I want it to show up. I made some test data giving values for x and y between 100 an 200 but the dots
always seem to be in the same place on the screen. What I change to domain or range they show up on the same place. I think It must be something fundamental but I cant find it. Please give me a clue.
This is the code from the test:
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="d3.v6.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script>
// set the dimensions and margins of the graph
const margin = {top: 10, right: 30, bottom: 30, left: 60},
width = 920 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
// append the svg object to the body of the page
const svg = 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})`);
let dataExample = [];
for (let i = 0; i < 10000; i++) {
const x = Math.floor(Math.random() * 100) + 100;
const y = Math.floor(Math.random() * 100) + 100;
dataExample.push([x, y]);
}
//Read the data (DataFile.csv is NOT used. Using data from dataExample
d3.csv("DataFile.csv").then( function(data) {
// Add X axis
const x = d3.scaleLinear()
.domain([0, 10000])
.range([ 0, width ]);
svg.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(x));
// Add Y axis
const y = d3.scaleLinear()
.domain([0, 10000])
.range([ height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
// Add dots
svg.append('g')
.selectAll("dot")
.data(dataExample)
.join("circle")
.attr("cx", function (d) { return d[0]; } )
.attr("cy", function (d) { return d[1]; } )
//console.log(dataExample)
.attr("r", 1.5)
.style("fill", "#69b3a2")
})
</script>
The axes are 0 to 10000 but the plotted data shows op between y=7200 to 8800 and x=800 and 2500.
You need to use your x and y scales when setting the "cx" and "cy" attributes of the circles. Right now you're setting these attributes to the values in your data, without using the scales. This code should look like this:
svg.append('g')
.selectAll("circle")
.data(dataExample)
.join("circle")
.attr("cx", function (d) { return x(d[0]); } )
.attr("cy", function (d) { return y(d[1]); } )
.attr("r", 1.5)
.style("fill", "#69b3a2");
The purpose of the x and y scales in the scatterplot are to map values in your data to positions in the scatterplot.
With this fix, you'll likely want to update the domains of the scales to better match the values in the data, such as by using d3.extent to get the min and max values.
I have a quick question. I am trying to determine the height in pixels of a chart bar. This is for a D3 implementation, and my chart has a logarithmic y-axis.
I know the Y value for the bar I am trying to plot.
I also know the height of the axis in pixels (600px).
I know the min and the max of the Y-axis
I have tried various computations but cannot seem to calculate the height of the bar so that it connects the Y value with the x-Axis.
The picture below should provide a visual illustration of what I am trying to do. Right now I can't seem to get it right ... I think this is essentially a problem in maths, not so much D3. Thank you!
*** EDIT ****
This is the y axis scale that I am using:
var y = d3.scale.log()
.range([height, 0])
.domain([d3.min(sampleData, function(d) {
return d.y;
}),
d3.max(sampleData, function(d) {
return d.y;
})
]);
I'm still not sure about your problem, because the actual height of the bar is being calculated by the very scale you use to append the rectangles. And, if you're in fact appending the rectangles, you're already setting the height attribute!
Let's see an example. This is a bar chart using your log scale (I'm using D3 v4 here, but the principle is the same) and this fake data:
var data = [2000, 5000, 3000, 8000, 1500];
As you can see, there is a minimum and a maximum. I put a padding of 20px in the scale:
var yScale = d3.scaleLog()
.range([height - padding, padding])
.domain([d3.min(data), d3.max(data)]);
So, our first value in the range is h - padding and our last value is just padding. Here is the chart:
var width = 300,
height = 400,
padding = 20;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = [2000, 5000, 3000, 8000, 1500];
var xScale = d3.scaleBand()
.range([50, width])
.domain(d3.range(data.length))
.padding(0.2);
var yScale = d3.scaleLog()
.range([height - padding, padding])
.domain([d3.min(data), d3.max(data)]);
var bars = svg.selectAll("foo")
.data(data)
.enter()
.append("rect")
.attr("x", (d, i) => xScale(i))
.attr("width", xScale.bandwidth())
.attr("height", d => height - padding - yScale(d))
.attr("y", d => yScale(d))
.attr("fill", "teal");
var gX = svg.append("g")
.attr("transform", "translate(0," + (height - padding) + ")")
.call(d3.axisBottom(xScale));
var gY = svg.append("g")
.attr("transform", "translate(50,0)")
.call(d3.axisLeft(yScale));
<script src="//d3js.org/d3.v4.min.js"></script>
Suppose you want to calculate the height of the first bar. We can see, by inspecting the DOM, that its height is 61.867984771728516 pixels:
var width = 300,
height = 400,
padding = 20;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = [2000, 5000, 3000, 8000, 1500];
var xScale = d3.scaleBand()
.range([50, width])
.domain(d3.range(data.length))
.padding(0.2);
var yScale = d3.scaleLog()
.range([height - padding, padding])
.domain([d3.min(data), d3.max(data)]);
var bars = svg.selectAll("foo")
.data(data)
.enter()
.append("rect")
.attr("x", (d, i) => xScale(i))
.attr("width", xScale.bandwidth())
.attr("height", d => height - padding - yScale(d))
.attr("y", d => yScale(d))
.attr("fill", "teal");
var gX = svg.append("g")
.attr("transform", "translate(0," + (height - padding) + ")")
.call(d3.axisBottom(xScale));
var gY = svg.append("g")
.attr("transform", "translate(50,0)")
.call(d3.axisLeft(yScale));
console.log(d3.select("rect").node().height.animVal.value)
<script src="//d3js.org/d3.v4.min.js"></script>
But this is simply the first value in the range (height - padding) minus yScale(2000):
var width = 300,
height = 400,
padding = 20;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = [2000, 5000, 3000, 8000, 1500];
var xScale = d3.scaleBand()
.range([50, width])
.domain(d3.range(data.length))
.padding(0.2);
var yScale = d3.scaleLog()
.range([height - padding, padding])
.domain([d3.min(data), d3.max(data)]);
var bars = svg.selectAll("foo")
.data(data)
.enter()
.append("rect")
.attr("x", (d, i) => xScale(i))
.attr("width", xScale.bandwidth())
.attr("height", d => height - padding - yScale(d))
.attr("y", d => yScale(d))
.attr("fill", "teal");
var gX = svg.append("g")
.attr("transform", "translate(0," + (height - padding) + ")")
.call(d3.axisBottom(xScale));
var gY = svg.append("g")
.attr("transform", "translate(50,0)")
.call(d3.axisLeft(yScale));
console.log(height - padding - yScale(2000))
<script src="//d3js.org/d3.v4.min.js"></script>
Which, by the way, is the value used to set the height of the bars:
.attr("height", d => height - padding - yScale(d))
I have a bar chart see plunker the problem is that I would like to move the y-axis ticks to be at the middle left side of the rects but they appear on the top and end. and I cannot seem to move them without destroying the chart.
my code
var info = [{
name: "Walnuts",
value: 546546
}, {
name: "Almonds",
value: 456455
}
];
/* Set chart dimensions */
var width = 960,
height = 500,
margin = {
top: 10,
right: 10,
bottom: 20,
left: 60
};
//subtract margins
width = width - margin.left - margin.right;
height = height - margin.top - margin.bottom;
//sort data from highest to lowest
info = info.sort(function(a, b) {
return b.value - a.value;
});
//Sets the y scale from 0 to the maximum data element
var max_n = 0;
var category = []
for (var d in info) {
max_n = Math.max(info[d].value, max_n);
category.push(info[d].name)
}
var dx = width / max_n;
var dy = height / info.length;
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
var svg = d3.select("#chart")
.append("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr('preserveAspectRatio', 'xMidYMin')
.attr("viewBox", '0 0 ' + parseInt(width + margin.left + margin.right) + ' ' + parseInt(height + margin.top + margin.bottom))
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.selectAll(".bar")
.data(info)
.enter()
.append("rect")
.attr("class", function(d, i) {
return "bar" + d.name;
})
.attr("x", function(d, i) {
return 0;
})
.attr("y", function(d, i) {
return dy * i;
})
.attr("width", function(d, i) {
return dx * d.value
})
.attr("height", dy)
.attr("fill", function(d, i) {
if (d.name == 'Walnuts') {
return 'red'
} else {
return 'green'
}
});
var y_xis = svg.append('g')
.attr('id', 'yaxis')
.call(yAxis);
You are using range in y axis like this:
var y = d3.scale.ordinal()
.domain(category)
.range([0, height]);
You should be using 'rangeRoundBands' since the y scale is ordinal
var y = d3.scale.ordinal()
.domain(category)
.rangeRoundBands([0, height], .1);
working code here
For d3 versions like v4/v5.
Defining height as the graph/plot height, and max as the maximum value of y.
import { parseSvg } from 'd3-interpolate/src/transform/parse'
const yScale = d3
.scaleLinear()
.domain([0, max])
.rangeRound([height, 0])
const yAxis = d3.axisLeft(yScale)
svg
.append('g')
.call(yAxis)
.selectAll('.tick')
.each(function(data) {
const tick = d3.select(this)
const { translateX, translateY } = parseSvg(tick.attr('transform'))
tick.attr(
'transform',
translate(translateX, translateY + height / (2 * max))
)
})
Recently I needed something very very similar and I solved this with a call with selecting all text elements in the selection and moving their dy upwards. I will give an example with OP's code:
var y_xis = svg.append('g')
.attr('id','yaxis')
.call(yAxis)
.call(selection => selection
.selectAll('text')
.attr('dy', '-110') // this moves the text labels upwards
.attr('x', '110')); // this does the same job but horizontally
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.
Hope someone can help, I have a slight problem in that the horizontal axis label 100 gets cut off the end of the stacked horizontal barchart. I can't seem to figure out what is wrong in the code. Thanks in advance for your help. Please see code below.
<!DOCTYPE html>
-->
<html>
<head>
<title>Horizontal stacked bar</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<script type="text/javascript" src="d3/d3.js"> </script>
<style>
.axis{
font-size: 14px;
}
#h{
}
</style>
</head>
<body>
<script>
var margin = {
top: 12,
left: 15,
right: 15,
bottom: 14
};
var w = 500 - margin.left - margin.right;
var h = 300 - margin.top - margin.bottom;
var dataset = [
[
{x:0,y:20}
],
[
{x:0,y:30}
],
[
{x:0,y:50}
]
];
//Set up stack method
var stack = d3.layout.stack();
//Data, stacked
stack(dataset);
//Set up scales
var xScale = d3.scale.linear()
.domain([0,d3.max(dataset, function(d) {return d3.max(d, function(d)
{return d.y0 + d.y;}); }) ])
// note use of margin + right to get axis to scale width
.range([0, w + margin.right]);
var yScale = d3.scale.ordinal()
.domain(d3.range(dataset[0].length))
.rangeRoundBands([0,w ], 0.05);
//Easy colors accessible via a 10-step ordinal scale
var colors = d3.scale.category10();
//or make your own colour palet
var color = d3.scale.ordinal()
.range(["#1459D9", "#148DD9", "#87ceeb", "#daa520"]);
// good site for colour codes http://www.colorpicker.com/113EF2
//Create SVG element
var svg = d3.select("body")
.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 + ")")
;
// Add a group for each row of data
var groups = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
.style("fill", function(d,i){return color(i);})
;
// Add a rect for each data value
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d) {return xScale(d.y0) ;}) //+99 will move axis right
.attr("y", 180)
.attr("height", 90)
.attr("width", yScale.rangeBand());
//Add an axis
var xAxis = d3.svg.axis()
.scale(xScale);
svg.append("g")
.call(xAxis)
;
</script>
</body>
</html>
You are really better off using the xScale for both dimensions, x and y. After all, your y is really a width. Here is what I mean:
...
//Set up scales
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function (d) {
return d3.max(d, function (d) {
return d.y0 + d.y;
});
})])
.range([0, w]); // no need to tamper with margins since w already accounts for that
...
// Add a rect for each data value
var rects = groups.selectAll("rect")
.data(function (d) {return d;})
.enter()
.append("rect")
.attr("x", function (d) {
return xScale(d.y0); // use x scale
})
.attr("y", 50)
.attr("height", 50)
.attr("width", function (d) {
return xScale(d.y); // use x scale
})
...
And here is the updated FIDDLE. You can go ahead and make changes to the right margin value and any of your data y values (I placed comments in the code to that effect) and you can see that this solution scales well.