Solution:
I spent a little time looking into this, and came up with a decent solution. Lars is right, d3.js doesn't really allow for what I wanted, but with a little layering, it worked out. Basically, I created a new SVG to contain only the axis, overlaid it on the actual graph and tied the two zoom scales together. Here is the code:
var x = d3.scale.linear()
.nice()
.domain([-1, 1])
.range([0, w]);
var xScale = d3.scale.linear()
.nice()
.domain([0, 1])
.range([0, 230]);
var xAxis = d3.svg.axis()
.scale(xScale)
.ticks(3)
.tickSize(7);
var y = d3.scale.linear()
.nice()
.domain([1, -1])
.range([h, 0]);
/* Control panning and zooming */
var zoom = function() {
if( d3.event ) {
zm.scale(d3.event.scale).translate(d3.event.translate);
za.scale(d3.event.scale); // don't translate so the axis is fixed
}
/* Do other zoom/pan related translations for chart */
// Update x-axis on pan and zoom
vis2.select(".xaxis")
.transition()
.ease("sin-in-out")
.call(xAxis);
vis.selectAll("line.link")
.attr("x1", function(d) { return x(parseFloat(d.source.x)); })
.attr("y1", function(d) { return y(parseFloat(d.source.y)); })
.attr("x2", function(d) { return x(parseFloat(d.target.x)); })
.attr("y2", function(d) { return y(parseFloat(d.target.y)); });
};
var zm = d3.behavior.zoom().x(x).y(y).scaleExtent([-Infinity, Infinity]).on("zoom", zoom);
var za = d3.behavior.zoom().x(xScale).scaleExtent([-Infinity, Infinity]).on("zoom", zoom);
force = d3.layout.force()
.gravity(0)
.charge(-5)
.alpha(-20)
.size([w, h]);
nodes = force.nodes();
links = force.links();
vis = d3.select(CHART).append("svg:svg")
.attr("width", w)
.attr("height", h)
.call(zm);
vis.append("rect")
.attr("class", "overlay")
.attr("width", "100%")
.attr("height", "100%");
vis2 = vis.append("svg:svg")
.attr("width", 300)
.attr("height", 30)
.call(za);
vis2.append("svg:g")
.attr("class", "xaxis")
.attr("transform", "translate(20,10)")
.call(xAxis);
vis2.append("rect")
.attr("class", "overlay")
.attr("width", "100%")
.attr("height", "100%");
force.on("tick", zoom);
The result (with some additional CSS, of course) looks something like this, where the scale adjusts automatically as the user zooms in and out:
http://i.imgur.com/TVYVp4M.png
Original Question:
I have a chart in D3js that displays a scale along the X-axis and appropriately updates as I zoom in and out. However, rather than having the entire bottom of the chart be a scale, I'd like to display the scale as more of a legend, like a distance scale on a map (ex. http://2012books.lardbucket.org/books/essentials-of-geographic-information-systems/section_06/91302d9e3ef560ae47c25d02a32a629a.jpg). Is this possible?
Snippet of relevant code:
var x = d3.scale.linear()
.nice()
.domain([-1, 1])
.range([0, w]);
var xAxis = d3.svg.axis()
.scale(x);
var zoom = function() {
vis.selectAll("g.node")
.attr("transform", function(d) {
return "translate(" + x(parseFloat(d.x)) + "," + y(parseFloat(d.y)) + ")";
});
// Update x-axis on pan and zoom
vis.select(".xaxis")
.transition()
.ease("sin-in-out")
.call(xAxis);
};
Related
I am trying to create a barplot using javascript. I have created a barplot, but want to add two axis. Currently stuck on the x-axis.
I am unable to move my x-axis to the bottom of my barplot. I am Using d3 to tailor the svg. I am currently able to showcase it at the top, but want to show it at the bottom.
Any input would be useful!
My attempts thus far have been to use transform, but when I execute this my axis disappears.
Googled several other solutions, none of them being successful.
Code:
<script>
d3.json("data_week3.json", function(data){
var data_renewables = [];
var data_nations = [];
for (i = 0; i < data.length; i++)
{
data_renewables.push(data[i].Renewable);
data_nations.push(data[i].Nation)
}
var width = 1000,
height = 500;
var y = d3.scale.linear()
.domain([0, d3.max(data_renewables)])
.range([height, 0]);
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", height);
var barWidth = width / data_renewables.length;
var bar = chart.selectAll("g")
.data(data_renewables)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });
bar.append("rect")
.attr("y", function(d) { return y(d); })
.attr("height", function(d) { return height - y(d); })
.attr("width", barWidth - 1);
bar.append("text")
.attr("x", barWidth / 2)
.attr("y", function(d) { return y(d); + 3; })
.attr("dy", ".75em")
.text(function(d) { return d; });
var axisScale = d3.scale.linear()
.domain([0, 30])
.range([0, 1000]);
var xAxis = d3.svg.axis()
.scale(axisScale)
.orient("bottom");
chart.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
})
</script>
You're giving the chart a height of height and then your transform is moving the top of the x axis by a value of height so it will always be cut off. I suggest you look at the margin convention: https://bl.ocks.org/mbostock/3019563
I've created a simple bar chart in D3, but the the bars are being created from top to bottom, instead of bottom to top. Below is the xScale, yScale and bar generation code:
var xscale = d3.scale.linear()
.domain([0, data.length])
.range([0, 240]);
var yscale = d3.scale.linear()
.domain([0, 100])
.range([0, 240]);
var bar = canvas.append('g')
.attr("id", "bar-group")
.attr("transform", "translate(10,20)")
.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr("class", "bar")
.attr("height", function(d, i) {
return yscale(d);
})
.attr("width", 15)
.attr("x", function(i) {
return yscale(i);
})
.attr("y", 0)
.style("fill", function(i) {
return colors(i);
});
Tried to swap yScale ranges but no success. Here is the fiddle.
In the SVG coordinates system, the origin (0,0) is at the top left corner, not the bottom left corner.
Besides that, your SVG has only 150px heigh. Thus, change your scale:
var yscale = d3.scale.linear()
.domain([0, 100])
.range([0, 150]);
And the math of your bars:
.attr("height", function(d, i) {
return yscale(d);
})
.attr("y", function(d){
return 150 - yscale(d)
})
Here is your updated fiddle: https://jsfiddle.net/bga2q72f/
PS: don't use the same scale for the x and y positions. It's quite confusing.
I am using d3 to build a piano roll editor (which looks kind of like this). I need the rectangles to always be snapped onto the grid so when I pan or zoom the shapes will stay relative to the grid lines. It doesn't matter if the vertical grid lines redraw as I move in and out, but the number of horizontal grid lines should always stay the same, and the rectangle shapes are always locked on. An example of it not quite working can be seen here: http://jsfiddle.net/jgab3103/e05qj4hy/
I can see lots of d3 zoom type of examples around the place but I can't find anything to address this kind of issue. I think I am just not understanding how to scale shapes properly when working with the the zoom function. Also, in trying to get this to work I am noticing the panning and zooming seems to have become a bit unreliable, not sure why.
Anyway, if anyone had any ideas on how to solve this, it would be greatly appreciated. The code which is on the jsfiddle is below:
UPDATE: Just to (hopefully!) clarify - both horizontal and vertical axis need to zoom. The constraint is that the number of horizontal grid lines needs to stay the same and the shapes must be locked on to the grid lines so the dimensions never change. If a rectangle starts with a width and height of 1, this always needs to be retained when zooming.
//Data for note shapes
var noteData = [
{frequency: 3, duration:1, startPoint: 1},
{frequency: 6, duration:1, startPoint: 2},
{frequency: 5, duration:1, startPoint: 3},
{frequency: 4, duration:1, startPoint: 4}
];
margin = {
top: 20,
right: 20,
bottom: 20,
left: 45
};
width = 400 - margin.left - margin.right;
height = 200 - margin.top - margin.bottom;
//SCALES
var xScale = d3.scale.linear()
.domain([0,width])
.range([0, width])
var yScale = d3.scale.linear()
.domain([0,width])
.range([0, height]);
var heightScale = d3.scale.linear()
.domain([0,100])
.range([0,height]);
//Set up zoom
var zoom = d3.behavior.zoom()
.x(xScale)
.y(yScale)
.scaleExtent([1,100])
.scale([50])
.on("zoom", zoomed);
// Create SVG space and centre it
svg = d3.select('#chart')
.append("svg:svg")
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append("svg:g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
// Append a rect on top
var rect = svg.append("svg:rect")
.attr("width", width)
.attr("height", height)
.attr("class", "plot");
var noteRange = d3.range(0,88);
var measureRange = d3.range(0,16);
var make_x_axis = function () {
return d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(10);
};
var make_y_axis = function () {
return d3.svg.axis()
.scale(yScale)
.orient("left")
.tickValues(noteRange);
};
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(10);
//.tickValues([2,5,7,9]);
svg.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis);
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.tickValues(noteRange);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "x grid")
.attr("transform", "translate(0," + height + ")")
.call(make_x_axis()
.tickSize(-height, 0, 0)
.tickFormat(""));
svg.append("g")
.attr("class", "y grid")
.call(make_y_axis()
.tickSize(-width, 0, 0)
.tickFormat(""));
var clip = svg.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
var chartBody = svg.append("g")
.attr("clip-path", "url(#clip)");
var rectGroup = svg.append("g")
var notes = rectGroup
.selectAll("rect")
.data(noteData)
.enter()
.append("rect")
.attr("x",function(d){
return xScale(d.startPoint)
})
.attr("y",function(d){
return yScale(d.frequency)
})
.attr("width",function(d) {
return 50;
})
.attr('class', 'rect')
.attr("height", function(d) {
return 23;
})
function zoomed() {
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
svg.select(".x.grid")
.call(make_x_axis()
.tickSize(-height, 0, 0)
.tickFormat(""));
svg.select(".y.grid")
.call(make_y_axis()
.tickSize(-width, 0, 0)
.tickFormat(""));
rectGroup.selectAll("rect")
.attr('class', 'rect')
.attr("x",function(d){
return xScale(d.startPoint);
})
.attr("y",function(d){
return yScale(d.frequency);
})
.attr('width', function(d) {
return 50;
})
.attr("height", function(d) {
return 23;
})
}
If you don't want yScale to be updated by the zoom behavior, just remove the line .y(yScale) and you should be good to go.
The zoom behavior will be constructed simply:
var zoom = d3.behavior.zoom()
.x(xScale)
.scaleExtent([1,100])
.scale([50])
.on("zoom", zoomed);
and it will only update the xScale.
I currently have a working scatter plot that I make using this
var data = (an array of arrays with two integers in each array)
var margin = {top: 20, right: 15, bottom: 60, left: 60}
, width = 300 - margin.left - margin.right
, height = 250 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[0]; })])
.range([ 0, width ]);
var y = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[1]; })])
.range([ height, 0 ]);
var chart = d3.select('.scatterGraph')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.ticks(5);
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(data)
.enter().append("svg:circle")
.attr("cx", function (d,i) { return x(d[0]); } )
.attr("cy", function (d) { return y(d[1]); } )
.attr("r", 2);
I was wondering how I could add a line graph (or alternatively another scatter plot) to this graph. I'm very new to d3 so I'm currently lost on how to do it. For example I would just want to add a line described by a function y = 2t where t is the x axis of the scatterplot. Thanks!
If it is as simple as a line described by a function y=2t you can just append a line element to your chart in this case main like this, assuming that your width is at least greater than twice your height
main.append("line").attr("x1", 0).attr("x2", height/2)
.attr("y1", height).attr("y2", 0);
But if you have a line that connected through multiple points, you will need to add a path element to your svg, and use d3.svg.line() function to generate its d attribute. So something like this,
var lineFunction = d3.svg.line().x(function (d) { x(d[0])}; )
.y(function (d) { y(d[1])}; );
var gLine = main.append("g");
gLine.append("path").attr("d", lineFunction(data));
For another scatter plot, you can reuse
var g = main.append("svg:g");
g.selectAll("scatter-dots")
.data(data2)
.enter().append("svg:circle")
.attr("cx", function (d,i) { return x(d[0]); } )
.attr("cy", function (d) { return y(d[1]); } )
.attr("r", 2);
but with a different set of data, and different accessor functions or scales if needed.
I have a vertical bar chart that is grouped in pairs. I was trying to play around with how to flip it horizontally. In my case, the keywords would appear on the y axis, and the scale would appear on the x-axis.
I tried switching various x/y variables, but that of course just produced funky results. Which areas of my code do I need to focus on in order to switch it from vertical bars to horizontal ones?
My JSFiddle: Full Code
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([0, w], 0.05);
// ternary operator to determine if global or local has a larger scale
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function (d) {
return (d.local > d.global) ? d.local : d.global;
})])
.range([h, 0]);
var xAxis = d3.svg.axis()
.scale(xScale)
.tickFormat(function (d) {
return dataset[d].keyword;
})
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(5);
var commaFormat = d3.format(',');
//SVG element
var svg = d3.select("#searchVolume")
.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 + ")");
// Graph Bars
var sets = svg.selectAll(".set")
.data(dataset)
.enter()
.append("g")
.attr("class", "set")
.attr("transform", function (d, i) {
return "translate(" + xScale(i) + ",0)";
});
sets.append("rect")
.attr("class", "local")
.attr("width", xScale.rangeBand() / 2)
.attr("y", function (d) {
return yScale(d.local);
})
.attr("x", xScale.rangeBand() / 2)
.attr("height", function (d) {
return h - yScale(d.local);
})
.attr("fill", colors[0][1])
;
sets.append("rect")
.attr("class", "global")
.attr("width", xScale.rangeBand() / 2)
.attr("y", function (d) {
return yScale(d.global);
})
.attr("height", function (d) {
return h - yScale(d.global);
})
.attr("fill", colors[1][1])
;
sets.append("rect")
.attr("class", "global")
.attr("width", xScale.rangeBand() / 2)
.attr("y", function (d) {
return yScale(d.global);
})
.attr("height", function (d) {
return h - yScale(d.global);
})
.attr("fill", colors[1][1])
;
I just did the same thing last night, and I basically ended up rewriting the code as it was quicker than fixing all the bugs but here's the tips I can give you.
The biggest issues with flipping the x and y axis will be with things like return h - yScale(d.global) because height is calculated from the "top" of the page not the bottom.
Another key thing to remember is that when you set .attr("x", ..) make sure you set it to 0 (plus any padding for the left side) so = .attr("x", 0)"
I used this tutorial to help me think about my own code in terms of horizontal bars instead - it really helped
http://hdnrnzk.me/2012/07/04/creating-a-bar-graph-using-d3js/
here's my own code making it horizontal if it helps:
var w = 600;
var h = 600;
var padding = 30;
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d){
return d.values[0]; })]) //note I'm using an array here to grab the value hence the [0]
.range([padding, w - (padding*2)]);
var yScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([padding, h- padding], 0.05);
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h)
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", 0 + padding)
.attr("y", function(d, i){
return yScale(i);
})
.attr("width", function(d) {
return xScale(d.values[0]);
})
.attr("height", yScale.rangeBand())
An alternative is to rotate the chart (see this). This is a bit hacky as then you need to maintain the swapped axes in your head (the height is actually the width etc), but it is arguably simpler if you already have a working vertical chart.
An example of rotating the chart is below. You might need to rotate the text as well to make it nice.
_chart.select('g').attr("transform","rotate(90 200 200)");
Here is the procedure I use in this case:
1) Inverse all Xs and Ys
2) Remember that the 0 for y is on top, thus you will have to inverse lots of values as previous values for y will be inversed (you don't want your x axis to go from left to right) and the new y axis will be inversed too.
3) Make sure the bars display correctly
4) Adapt legends if there are problems
This question may help in the sense that it shows how to go from horizontal bar charts to vertical: d3.js histogram with positive and negative values