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.
Related
I'm creating a timeline line chart showing 5 or 6 different lines and want to be able to zoom in and scroll (once zoomed). I used some examples that use area charts but for some reason my line chart jumps to the right when I zoom the first time and I lose some of the data off to the right (I can no longer scroll to see it or see it when zoomed fully out). Also the lines appear over the y axis when I zoom or scroll.
I've copied it to JSFiddle (see here) with a dataset from 1 of the lines in my chart. Why is the line jumping to the right as soon as you use the zoom function? How can I stop the line from appearing over the y-axis?
Here is the JS of my version if you'd prefer to read it here:
function drawTimeline() {
var margin = {top: 10, right: 0, bottom: 50, left: 60}
var width = d3.select('#timeline').node().getBoundingClientRect().width/3*2;
var height = 300;
var svg = d3.select("#timeline").append("svg")
.attr("width", width)
.attr("height", height+margin.top+margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.attr("width", width - margin.left - margin.right)
.call(d3.zoom()
// .extent()
.scaleExtent([1, 10])
.translateExtent([[0, -Infinity], [width - margin.left - margin.right, Infinity]])
.on("zoom", zoom)
);
var view = svg.append("rect")
.attr("class", "view")
.attr("width", width - margin.left - margin.right)
.attr("height", height)
.attr("fill", "white");
var x = d3.scaleTime()
.domain([
d3.min(poll_data[0].avgpolls, function(p) { return p.date; }),
d3.max(poll_data[0].avgpolls, function(p) { return p.date; })
])
.range([0, width - margin.left - margin.right]);
var y = d3.scaleLinear()
.domain([50, 0])
.range([0, height]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var gX = svg.append("g")
.attr("class", "axis xaxis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
var gY = svg.append("g")
.attr("class", "axis yaxis")
.call(yAxis);
//All lines are drawn in the same way (x and y points)
var line = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.poll); });
//selectAll allows us to create and manipulate multiple groups at once
var party = svg.selectAll(".party")
.data(poll_data)
.enter()
.append("g")
.attr("class", "party");
//Add path to every country group at once
var pollPaths = party.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.avgpolls); })
.attr("fill", "none")
.attr("stroke-width", 2)
.style("stroke", function(d) { return returnPartyColour(d.party); });
function zoom() {
console.log("zooming: " + d3.event.transform);
var new_x = d3.event.transform.rescaleX(x);
gX.call(d3.axisBottom(new_x));
//view.attr("transform", d3.event.transform);
//Redraw lines
//pollPaths.select(".line").attr("d", function(d) { return line(d.avgpolls); });
var newline = d3.line()
.x(function(d) { return new_x(d.date); })
.y(function(d) { return y(d.poll); });
pollPaths.attr("d", function(d) { return line(d.avgpolls); });
}
}
The data is formatted like this in my version but not the JSFiddle:
poll_data = [
{
'party' : 'Party 1',
'avgpolls' : [
{'date' : new Date(year, month, day), 'poll' : 0, },
],
]
Thanks
Two things required to fix these issues.
The reason the line was jumping on zoom was because the zoom extent was not set. This was set and the value of translateExtent updated:
d3.zoom()
.scaleExtent([1, 10])
.translateExtent([[0, 0], [width - margin.left - margin.right, Infinity]])
.extent([[0, 0], [width - margin.left - margin.right, height]])
.on("zoom", zoom)
To prevent the paths from overflowing a clipping path is required. After creating the svg, before other elements are added, I added a clip-path as follows:
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
//Same dimensions as the area for the lines to appear
.attr("width", width - margin.left - margin.right)
.attr("height", height);
Then this had to be added to each path.
var pollPaths = party.append("path")
...
.attr("clip-path", "url(#clip)");
The original JSFiddle has been updated to reflect these changes.
I have tried to make a set of x and y axis with this code and I'm stumped as to why they are not showing up. I want it to display a set of axis with ranges of -10 to 10, and the red line to sit above these axes. I am not sure why specifically the axis aren't working at this stage, the latter portion of my code has the axis code in it.
//variable declaration
var width = 500;
var height = 300;
var margin = 10;
//svg setup
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append('g')
.attr("transform", "translate(" + margin + "," + margin + ")" )
//line setup
svg.append('line')
.attr({
x1: 0,
y1: 0,
x2: 250,
y2: 250
});
//scales and axis setup
var scaleX = d3.scale.linear()
.range([-10, 10])
.domain([0, width])
var scaleY = d3.scale.linear()
.range([-10, 10])
.domain([height, 0])
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom");
var axisY = d3.svg.axis()
.scale(scaleY)
.orient("left");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + "," + height/2 + ")")
.call(axisX);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + width/2 + "," + "0)")
.call(axisY);
There were some mistakes,like mapping goes from domain to range not range to domain and use of Svg instead of svg and svg.scale instead of d3.scale etc
see for yourself
https://jsfiddle.net/dango_x_daikazoku/33g9pLqe/4/
var width = 500;
var height = 300;
var margin = 30;
//svg setup
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append('g')
.attr("transform", "translate(" + margin + "," + margin + ")");
//line setup
//scales and axis setup
var scaleX = d3.scale.linear()
.domain([-10, 10])
.range([0, width])
var scaleY = d3.scale.linear()
.domain([-10, 10])
.range([height, 0])
var axisY = d3.svg.axis()
.scale(scaleY)
.orient("left");
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom");
svg.append("g")
.attr('class', 'y-axis axis')
.call(axisY);
svg.append("g")
.attr('class', 'x-axis axis')
.call(axisX)
.attr('transform', 'translate( 0, 250)')
I have a current zoom function I just learned to use in D3. However when I use it, it only moves my and zooms the axis of the graph not the objects on it.
I'm very knew to D3 and would like some help please.
My source code of the javascript is posted below:
//Setting generic width and height values for our SVG.
var margin = {top: 60, right: 0, bottom: 60, left: 40},
width = 1024 - 70 - margin.left - margin.right,
height = 668 - margin.top - margin.bottom;
//Other variable declarations.
//Creating scales used to scale everything to the size of the SVG.
var xScale = d3.scale.linear()
.domain([0, 1024])
.range([0, width]);
var yScale = d3.scale.linear()
.domain([1, 768])
.range([height, 0]);
//Creates an xAxis variable that can be used in our SVG.
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
//Zoom command ...
var zoom = d3.behavior.zoom()
.x(xScale)
.y(yScale)
.scaleExtent([1, 10])
.on("zoom", zoomed);
// The mark '#' indicates an ID. IF '#' isn't included argument expected is a tag such as "svg" or "p" etc..
var SVG = d3.select("#mainSVG")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
//Create background. The mouse must be over an object on the graph for the zoom to work. The rectangle will cover the entire graph.
var rect = SVG.append("rect")
.attr("width", width)
.attr("height", height);
//This selects 4 circles (non-existent, there requires data-binding) and appends them all below enter.
//The amount of numbers in data is the amount of circles to be appended in the enter() section.
var circle = SVG
.selectAll("circle")
.data([40,100,400,1900])
.enter()
.append("circle")
.attr("cx",function(d){return xScale(d)})
.attr("cy",function(d){return xScale(d)})
.attr("r",20);
//This appends a circles to our SVG.
var circle = SVG
.append("circle")
.attr("cx",function(d){ return xScale(d)})
.attr("cy",300)
.attr("r",20);
//Showing the axis that we created earlier in the script for both X and Y.
var xAxisGroup = SVG.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
var yAxisGroup = SVG.append("g")
.attr("class", "y axis")
.call(yAxis);
function zoomed() {
SVG.select(".x.axis").call(xAxis);
SVG.select(".y.axis").call(yAxis);
}
You also need to redraw all the elements with the changed axes on zoom -- D3 won't do this for you automatically:
function zoomed() {
SVG.select(".x.axis").call(xAxis);
SVG.select(".y.axis").call(yAxis);
SVG.selectAll("circle")
.attr("cx",function(d){return xScale(d)})
.attr("cy",function(d){return xScale(d)});
}
I'm trying out d3js and I have a problem with getting my first basic column(vertical bar) chart work. The only thing I find a bit difficult to understand is the scaling thing. I want to make the x and y axis ticks with labels but I have the following problems:
First of all here is my data:
{
"regions":
["Federal","Tigray","Afar","Amhara","Oromia","Gambella","Addis Ababa","Dire Dawa","Harar","Benishangul-Gumuz","Somali","SNNPR "],
"institutions":
[0,0,34,421,738,0,218,22,22,109,0,456]
}
On the y-axis the values are there but the order is reversed. Here is the code:
var y = d3.scale.linear().domain([0, d3.max(data.institutions)]).range([0, height]);
then I use this scale to create a y-axis:
var yAxis = d3.svg.axis().scale(y).orient("left");
and add this axis to the svg element
svgContainer.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Institutions");
the problem here is that the y-axis start from 0 at the top and with 700 at the bottom which is OK but it should be in reverse order.
The other problem I have it the x-axis. I want to have an ordinal scale since the values I want to put are in the regions names I have above. So here's what I've done.
var x = d3.scale.ordinal()
.domain(data.regions.map(function(d) { return d.substring(0, 2); }))
.rangeRoundBands([0, width], .1);
then the axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
and finally add it to the svg element
svgContainer.append("g")
.attr("class", "x axis")
.attr("transform", "translate( 0," + height + ")")
.call(xAxis);
Here the problem is the ticks as well as the labels appear but they are not spaced out evenly and do not correspond with the center of the rectangles I'm drawing. Here is the complete code so you can see what's happening.
$(document).ready(function(){
d3.json("institutions.json", draw);
});
function draw(data){
var margin = {"top": 10, "right": 10, "bottom": 30, "left": 50}, width = 700, height = 300;
var x = d3.scale.ordinal()
.domain(data.regions.map(function(d) { return d.substring(0, 2); }))
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.domain([0, d3.max(data.institutions)])
.range([0, height]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svgContainer = d3.select("div.container").append("svg")
.attr("class", "chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.right+ ")");
svgContainer.append("g")
.attr("class", "x axis")
.attr("transform", "translate( 0," + height + ")")
.call(xAxis);
svgContainer.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Institutions");
svgContainer.selectAll(".bar")
.data(data.institutions)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d, i) {return i* 41;})
.attr("y", function(d){return height - y(d);})
.attr("width", x.rangeBand())
.attr("height", function(d){return y(d);});
}
I put the code to Fiddle: http://jsfiddle.net/GmhCr/4/
Feel free to edit it! I already fixed both problems.
To fix the upside-down y-axis just swap the values of the range function arguments:
var y = d3.scale.linear().domain([0, d3.max(data.institutions)]).range([height, 0]);
Do not forget to adjust the code for the bars if you change the scale!
The source of the mismatch between bars and the x-axis can be found here:
var x = d3.scale.ordinal()
.domain(data.regions.map(function(d) {
return d.substring(0, 2);}))
.rangeRoundBands([0, width], .1);
svgContainer.selectAll(".bar")
.data(data.institutions)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d, i) {return i* 41;})
.attr("y", function(d){return height - y(d);})
.attr("width", x.rangeBand())
.attr("height", function(d){return y(d);});
You specify the padding for rangeRoundBands at 0.1 but you ignore the padding when computing the x and width values for the bars. This for example is correct with a padding of 0:
var x = d3.scale.ordinal()
.domain(data.regions.map(function(d) {
return d.substring(0, 2);}))
.rangeRoundBands([0, width], 0);
svgContainer.selectAll(".bar").data(data.institutions).enter().append("rect")
.attr("class", "bar")
.attr("x", function(d, i) {
return i * x.rangeBand();
})
.attr("y", function(d) {
return y(d);
})
.attr("width", function(){
return x.rangeBand();
})
.attr("height", function(d) {
return height -y(d);
});
The padding determines how much of the domain is reserved for padding. When using a width of 700 and a padding of 0.1 exactly 70 pixels are used for padding. This means you have to add 70 / data["regions"].length pixels to every bar's x value to make this work with a padding.
I'm playing again with d3.js the javascript library. I am able to create a chart with 2 numeric axes but now I want to have one numeric axis and one axis with a date. Unfortunately I'm not able to do this.
First of all, thats the code that is not running:
d3.json('builds.json',
function(data){
var format = d3.time.format("%Y-%m-%d");
data.forEach(function(d) {
d.finished_at = format.parse(d.finished_at);
});
var margin = {top: 40, right: 40, bottom: 40, left: 40},
width = 960,
height = 500;
var x = d3.time.scale()
.domain(d3.extent(data, function(d) { return d.finished_at; }))
.range([0, width - margin.right - margin.left]);
var y = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d.result; }))
.range([height - margin.top - margin.bottom, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return x(d.finished_at); })
.attr("cy", function(d) { return y(d.result); })
.attr("r", 6);
svg.append("g") // Render the axis by calling a <g> selection.
.attr("transform", "translate(0," + y.range()[0] + ")") //setzt x-achse an null punkt von y-achse
.call(xAxis);
svg.append("g")
.call(yAxis);
});
The variable finished_at looks like this: "finished_at":"2011-11-20" and I tried to parse it.
But the output is the following:
Can anybody please help me?
The problem likely lies with your data. You will need to confirm that all of the finished_at values in your JSON are in the correct format.
Your code appears to work perfectly well with a small working JSON dataset: http://bl.ocks.org/4162693