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.
Related
When building an area chart in D3.js, when you have only a single value the chart does not render.
For demonstration purposes, I modified the following example: https://d3-graph-gallery.com/graph/area_basic.html to illustrate the problem.
<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 50},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var 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 + ")");
//Read the data
d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/3_TwoNumOrdered_comma.csv",
// When reading the csv, I must format variables:
function(d){
return { date : d3.timeParse("%Y-%m-%d")(d.date), value : d.value }
},
// Now I can use this dataset:
function(data) {
data = [data[0]]
// Add X axis --> it is a date format
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) { return d.date; }))
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.value; })])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));
// Add the area
svg.append("path")
.datum(data)
.attr("fill", "#cce5df")
.attr("stroke", "#69b3a2")
.attr("stroke-width", 1.5)
.attr("d", d3.area()
.x(function(d) { return x(d.date) })
.y0(y(0))
.y1(function(d) { return y(d.value) })
)
})
</script>
I would expect that chart to look something like:
If you inspect the element the path element you can see it is rendering, just 0 width/height:
I am trying to make simple chart right now importing data from a CSV. Everything on the chart is working great except for the labels. In element inspect I can see that they are being appended and that their x and y coordinates are even correct, but for some reason they are all trapped in the top left corner in the SVG itself.
I have tried changing the x placement function at first because I thought it just wasn't giving the labels a x position, but upon further inspection the labels have the correct metadata.
//Graph Dimensions
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 1000 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//Set Ranges
var x_scale = d3.scaleBand()
.range([0, width])
.padding(0.1);
var y_scale = d3.scaleLinear()
.range([height, 0]);
//Create SVG object
var svg = d3.select('#chart')
.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 + ")");
//Retrieve data
d3.csv('sales.csv').then(function(data){
//Set domains based on data
x_scale.domain(data.map(function(d) { return d.month; }));
y_scale.domain([0, d3.max(data, function(d) { return d.sales; })]);
//Create bars
svg.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x_scale(d.month); })
.attr("width", x_scale.bandwidth())
.attr("y", function(d) { return y_scale(d.sales); })
.attr("height", function(d) { return height - y_scale(d.sales); });
//Create labels
svg.selectAll('text')
.data(data)
.enter().append('text')
.attr('class', 'label')
.attr("x", function(d) { return x_scale(d.month); })
.attr("y", function(d) { return y_scale(d.sales); })
.attr( 'font-size', 14 )
.attr( 'fill', '#555555' )
.attr( 'text-anchor', 'middle' );
//Add Axes
svg.append("g") //X Axis
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x_scale));
svg.append("g") //Y Axis
.call(d3.axisLeft(y_scale));
})
The only thing im looking for is the labels actually appearing. I can change their location later if needed.
A little background, I'm still fairly new to JS and development in general, so I may be missing something obvious. I got this chart working pretty well using d3, but I cannot get the positioning right no matter what I do. I've tried manipulating it with CSS and it just doesn't seem to behave in a logical way. I set display to block and margins to auto and it didn't affect it at all. The only way I can change the positioning is adjusting the margins in the d3 code, but that doesn't do very much for responsiveness. I've also tried using text-align and that didn't work either. What I'm trying to do is center it and have it scale larger as the screen size increases. This should all be easy to do in CSS in theory, but it just doesn't seem to work at all. Thanks for any help.
Here is the JS code:
// Set the dimensions of the canvas / graph
var margin = {top: 20, right: 0, bottom: 70, left: 70},
width = 300 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%-m/%-d/%Y").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(10);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
// chart area fill
var area = d3.svg.area()
.x(function(d) { return x(d.Date); })
.y0(height)
.y1(function(d) { return y(d.Forecast); });
// Define the line
var valueline = d3.svg.line()
.interpolate("cardinal")
.x(function(d) { return x(d.Date); })
.y(function(d) { return y(d.Orders); });
var valueline2 = d3.svg.line()
.interpolate("cardinal")
.x(function(d) { return x(d.Date); })
.y(function(d) { return y(d.Forecast); });
// Adds the svg canvas
var svg = d3.select("body")
.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 + ")");
// Get the data
d3.csv("csv/Forecast.csv", function(error, data) {
data.forEach(function(d) {
d.Date = parseDate(d.Date);
d.Orders = +d.Orders;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.Date; }));
y.domain([0, d3.max(data, function(d) { return d.Orders; })]);
// Area
svg.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline2(data))
.style("stroke", "#A7A9A6");
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
JSFIDDLES
Responsive & Centered:
https://jsfiddle.net/sladav/6fyjhmne/3/
Not Responsive: https://jsfiddle.net/sladav/7tp5vdkr/
For responsiveness, take advantage of the SVG viewBox:
Some links:
https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox
Using ViewBox to resize svg depending on the window size
Setting up viewBox:
var margin = {top: 100, right: 150, bottom: 100, left: 150}
var outerWidth = 1600,
outerHeight = 900;
var width = outerWidth - margin.right - margin.left,
height = outerHeight - margin.top - margin.bottom;
d3.select(".plot-div").append("svg")
.attr("class", "plot-svg")
.attr("width", "100%")
.attr("viewBox", "0 0 " + outerWidth + " " + outerHeight)
.append("g")
.attr("class", "plot-space")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")"
);
Points of note in the above:
SVG is put in a div -- I'll adjust the size and position of the div rather than the svg
SVG width is set as % of parent/div, not absolute.
Everything you draw in SVG now is with respect to outerWidth x outerHeight and unitless parameters are rescaled by viewBox.
Taking a look at the rect in my JSFIDDLE example...
d3.select(".plot-svg").append("rect")
.attr("x", 0)
.attr("y", 3*outerHeight/4)
.attr("width", 800)
.attr("height", outerHeight/4)
.attr("fill", "grey")
Resize your window and the rectangle will always fill 1/2 the svg because 800/1600 (note: 1600 is outerWidth).
For adjusting position/centering:
Manipulate the div containing your SVG/chart to position it how you want to. In my example it takes up 50% of the page and is centered because of margin: auto.
.plot-div{
width: 50%;
display: block;
margin: auto;
}
Whatever methods you use to scale/position your div, your chart should follow suit.
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 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)});
}