D3 Panning Chart with Dates - javascript

I'm trying to get a multi-line date based chart to pan nicely across the X date axis and I simply cannot figure out what the problem is.
I have the zoom behaviour set up in the code but it's just not performing as expected. If you click on a point in a line and scroll it appears to be scrolling the axis, if it click on the labels in the axis it also scrolls but the actual visualisation of data doesn't scroll.
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 1])
.on("zoom", function () {
svg.select(".x.axis").call(xAxis);
svg.select(".lines").call(xAxis);
});
svg.call(zoom);
Also if you click directly on the back ground the mouse event doesn't seem to make it's way to the control at all.
I read a few examples on this and each seem to take a vastly different approach which I've tried but none have worked for my chart.
There are possibly a number of issues that exist as barriers to getting this working so I thought the best way to illustrate the problem was in a JsFiddle.
D3 Chart Panning Fiddle
What I'm trying to achieve is when there is a lot of data to visualise the chart can adapt to the data set and allow the data to extend beyond the bounds of the chart.

Currently clicking on the background does not allow panning because you have applied zoom behavior to the g element not to the svg.
var svg = d3.select('#host')
.data(plotData)
.append("svg")
.attr("id", "history-chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom);
Right now on zoom you have updated x and y axes but not the visualization. So you have update the lines and circles also like this.
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 1])
.on("zoom", function () {
svg.select(".x.axis").call(xAxis);
svg.select(".lines").call(xAxis);
svg.selectAll("path.lines")
.attr("d", function(d) { return line(d.values); });
svg.selectAll("circle")
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.value); });
});
Since you are panning the map you will have to use clip path for restricting visualization from moving outside the chart
var clip = svg.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
Apply clip path to the g elment which contains lines and cicrles.
var attribute = svg.selectAll(".attribute")
.data(plotData)
.enter().append("svg:g")
.attr("clip-path", "url(#clip)")
.attr("class", "attribute");

Related

d3 how to tie text to top right corner of view port while zooming and panning

I am creating a mapping application in d3 and want to tie some text to the top right corner of my view port. Additionally, I want the text to remain in the top right corner while I zoom and pan across the application.I think I can solve my problem by figuring out how to get the coordinates of the top right corner of my view. Knowing this information would allow me to then set the coordinates of my text element. I've tried manually setting the dimensions of the containing svg element and then moving the text to that location but interestingly this didn't work. I was hoping to be able to find the coordinates programatically rather than setting coordinates manually. How can I do this in d3/javascript?
EDIT:
My code is a modification of this code by Andy Barefoot: https://codepen.io/nb123456/pen/zLdqvM
My own zooming and panning code has essentially remained the same as the above example:
function zoomed() {
t = d3
.event
.transform
;
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
;
}
I'm trying to append the text at the very bottom of the code:
countriesGroup.append("text")
.attr("transform", "translate(" How do I get top right coordinates? ")")
.style("fill", "#ff0000")
.attr("font-size", "50px")
.text("This is a test");
My idea is to be able to get the top right coordinates of the view port through the code rather than setting it manually and then have the coordinates of the text update as the user zooms or pans.
To keep something in place while zooming and panning you could invert the zoom:
point == invertZoom(applyZoom(point))
This isn't particularly efficient, as we are using two operations to get to the original number. The zoom is applied here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")");
While the inversion would need to look something like:
text.attr("x", d3.zoom.transform.invert(point)[0])
.attr("y", d3.zoom.transform.invert(point)[1])
.attr("font-size", baseFontSize / d3.zoom.transform.k);
Where point and base font size are the original anchor point and font size. This means storing that data somewhere. In the example below I assign it as a datum to the text element:
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g")
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
var text = g.append("text")
.datum({x: width-10, y: 20, fontSize: 12})
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.style("text-anchor","end")
.attr("font-size",function(d) { return d.fontSize; })
.text("This is a test");
function zoomed() {
g.attr("transform", d3.event.transform);
var d = text.datum();
var p = d3.event.transform.invert([d.x,d.y]);
var x1 = p[0];
var y1 = p[1];
text.attr("x",x1)
.attr("y",y1)
.attr("font-size", d.fontSize / d3.event.transform.k)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Better Solution
The above is the solution to the approach you seem to be looking for. But the end result is best achieved by a different method. As I mention in my comment, the above approach goes through extra steps that can be avoided. There can also be some size/clarity changes in the text when zooming (quickly) using the above method
As noted above, you are applying the zoom here:
countriesGroup
.attr("transform","translate(" + [t.x, t.y] + ")scale(" + t.k + ")")
The zoom transform is applied only to countriesGroup, if your label happens to be in a different g (and not a child of countriesGroup), it won't be scaled or panned.
We wouldn't need to apply and invert the zoom, and we wouldn't need to update the position or font size of the text at all.
var width = 500;
var height = 200;
var data = d3.range(100).map(function() {
return {x:Math.random()*width,y:Math.random()*height}
})
var zoom = d3.zoom()
.on("zoom",zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
.call(zoom);
var g = svg.append("g");
var g2 = svg.append("g"); // order does matter in layering
var circles = g.selectAll()
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.attr("fill","steelblue")
// position once and leave it alone:
var text = g2.append("text")
.attr("x", width - 10)
.attr("y", 20 )
.style("text-anchor","end")
.attr("font-size", 12)
.text("This is a test");
function zoomed() {
// apply the zoom to the g that has zoomable content:
g.attr("transform", d3.event.transform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Zoom/Pan D3.js Polar Scatter Plot

I created a polar scatter plot using D3.js (based on this post) .
I would like to add the functionality to zoom and pan. I've seen examples for rectangular plots, but nothing for zooming/panning on circular plots.
I am just a beginner with using D3 so I'm a little lost. Can anyone help/offer suggestions?
I'm not entirely sure what your goal is, but I tried something below.
First you should add zoom behaviour. I used the r scale for both your x and y directions like:
var zoomBeh = d3.behavior.zoom()
.x(r)
.y(r)
.on("zoom", zoom);
And call the zoom behaviour into your svg:
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.call(zoomBeh);
Finally you should make a zoom function.
function zoom() {
var t = svg.transition().duration(750);
svg.selectAll(".point").transition(t)
.attr("transform", function(d) {
var coors = line([d]).slice(1).slice(0, -1);
return "translate(" + coors + ")"
})
}
Here is an updated fiddle. It's a little bit staggering, I'm not sure why yet.

How better organize grid lines?

There are grid lines from points.
Is there another solution with better performance, because if I add many svg elements(etc. rects, circles, paths) and increase the dimension of the grid I will see the freeze effect when I use zoom, move element...
The size of the grid is changed.
Also, how can I create endless grid lines, instead limited (gridCountX, gridCountY)?
Thanks
var svg = d3.select("body").append("svg");
var svgG = svg.append("g");
var gridLines = svgG.append("g").classed("grid-lines-container", true).data(["gridLines"]);
var gridCountX = _.range(100);
var gridCountY = _.range(100);
var size = 10;
gridLines.selectAll("g").data(gridCountY)
.enter()
.append("g")
.each(function(d) {
d3.select(this).selectAll("circle").data(gridCountX).enter()
.append("circle")
.attr("cx", function(_d) {return _d*size;})
.attr("cy", function(_d) {return d*size;})
.attr("r", 0.5)
.attr("style", function() {
return "stroke: black;";
});
});
var zoomSvg = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", function(){
svgG.attr("transform", d3.event.transform);
});
svg.call(zoomSvg);
svg {
width: 100%;
height: 100%;
border: 1px solid #a1a1a1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
As you note, this approach is not really scalable and has a larger impact on performance. I have found the approach of utilizing d3 axes for grids to have minimal performance impact while also being relatively straightforward to incorporate with zoom such that you can have infinite zoom with the grid lines updating in a sensible manner due to the "magic" of automatic generation of sensible tick locations in d3.
To implement something similar in d3 v4, you can do something along these lines:
var svg = d3.select("svg"),
margin = {top: 20, right: 140, bottom: 50, left: 70},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")"),
innerSvg = g.append("svg").attr("width", width).attr("height", height);
// Calculate domain for x and y from data and store in x0, y0 (not shown here)
x.domain(x0);
y.domain(y0);
xGridAxis = d3.axisBottom(x).ticks(10);
yGridAxis = d3.axisLeft(y).ticks(10 * height / width);
// Create grouping and additional set of axes for displaying grid
innerSvg.append("g")
.attr("class", "grid x-grid")
.attr("transform", "translate (0," + height + ")")
.call(xGridAxis
.tickSize(-height, 0, 0)
.tickFormat("")
)
.selectAll(".tick");
innerSvg.append("g")
.attr("class", "grid y-grid")
.attr("transform", "translate (" + width + ", 0)")
.call(yGridAxis
.tickSize(width)
.tickFormat("")
);
// Add element to capture mouse events for drag and pan of plots
var zoom = d3.zoom()
.on("zoom", zoomed);
var scrollZoom = innerSvg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("pointer-events", "all") // Defaults to panning with mouse
.call(zoom);
// Mouse panning and scroll-zoom implementation using d3.zoom
// Modification of : http://bl.ocks.org/lorenzopub/013c0c41f9ffab4d27f860127f79c5f5
function zoomed() {
lastEventTransform = d3.event.transform;
// Rescale the grid using the new transform associated with zoom/pan action
svg.select(".x-grid").call(xGridAxis.scale(lastEventTransform.rescaleX(x)));
svg.select(".y-grid").call(yGridAxis.scale(lastEventTransform.rescaleY(y)));
// Calculate transformed x and y locations which are used to redraw all plot elements
var xt = lastEventTransform.rescaleX(x),
yt = lastEventTransform.rescaleY(y);
// Code below just shows how you might do it. Will need to tweak based on your plot
var line = d3.line()
.x(function(d) { return xt(d.x); })
.y(function(d) { return yt(d.y); });
innerSvg.selectAll(".line")
.attr("d", function(d) { return line(d.values); });
innerSvg.selectAll(".dot")
.attr("cx", function(d) {return xt(d.x); })
.attr("cy", function(d) {return yt(d.y); });
}
Here is a worked out example in d3 v4 that inspired my version above:
http://bl.ocks.org/lorenzopub/013c0c41f9ffab4d27f860127f79c5f5

d3.js Bubble Force Chart application

http://jsfiddle.net/pPMqQ/146/
I'm developing a bubble chart application that is a derivative of a force chart to provide a little bit of movement.
Here is some of the code. As I create the svg - I also set up the viewBox - which I think could be used to adjust the size of the chart - but I've not been able to adjust the ratios properly to get the correct scaling.
I've also added here the code that adds the nodes - as the node size is calculated - its using a scale variable to affect the size of the orbs.
var svg = d3.select(selector)
.append("svg")
.attr("class", "bubblechart")
.attr("width", parseInt(w + margin.left + margin.right,10))
.attr("height", parseInt(h + margin.top + margin.bottom,10))
.attr('viewBox', "0 0 "+parseInt(w + margin.left + margin.right,10)+" "+parseInt(h + margin.top + margin.bottom,10))
.attr('perserveAspectRatio', "xMinYMid")
.append("g")
.attr("transform", "translate(0,0)");
methods.force = d3.layout.force()
.charge(1000)
.gravity(100)
.size([methods.width, methods.height])
var bubbleholder = svg.append("g")
.attr("class", "bubbleholder")
var bubbles = bubbleholder.append("g")
.attr("class", "bubbles")
var labelbubble = bubbleholder.append("g")
.attr("class", "labelbubble")
// Enter
nodes.enter()
.append("circle")
.attr("class", "node")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", 1)
.style("fill", function (d) { return methods.fill(d.label); })
.call(methods.force.drag);
// Update
nodes
.transition()
.delay(300)
.duration(1000)
.attr("r", function (d) { return d.radius*scale; })
// Exit
nodes.exit()
.transition()
.duration(250)
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", 1)
.remove();
I have the remaining issues
scaling is an issue
I've set the width/heights via data attributes - currently I have a scaling variable set to adjust the size of the orbs depending on the width of the chart. I would like to find a more scientific way of ensuring the chart is resized accordingly and also that the elements always remain central (don't become obscured).
ensuring the smaller elements are on top of the big elements
I've also noticed that small objects may randomly fall underneath larger orbs, is there a way to organize the rendering of the orbs dependant on size, so bigger elements always sit at the bottom layer.
I've solved the second problem by sorting the data via value.
http://jsfiddle.net/pPMqQ/149/
data = data.sort(function(a, b) {return b.value - a.value})
Currently I have a scale variable depending on the width of the chart - var scale = methods.width*.005;
but its not very scientific per say
If the chart is 150 width
http://jsfiddle.net/pPMqQ/150/
the chart renders - but the bubbles no longer fit in the space.
the chart at 250 px
http://jsfiddle.net/pPMqQ/151/

Having trouble panning and zooming on my d3 map

I have this map in d3
http://107.170.20.64/
that renders topojson with a custom projection and path, like this
var projection = d3.geo.mercator().translate([width / 2, height / 2]).scale(width * 185).center([-89.99, 29.975]);
var path = d3.geo.path().projection(projection);
I am trying to adapt it so that it pans and zooms using Bostock's tutorial. Here is the function that fires once the topojson loads (showing my adaptations of Bostock's method):
function ready(error, us) {
var zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.scaleExtent([1, 8])
.on("zoom", zoomed);
var features = svg.append("g");
features.append("g")
.attr("class", "precincts")
.selectAll("path")
.data(topojson.feature(us, us.objects.orleansgeojson).features)
.enter().append("path")
.attr("class", (function(d) {
return wards.get(d.id) + " precinct";
}))
.attr("title", (function(d) {
return votesone.get(d.id) + "-" + votestwo.get(d.id);
}))
.attr("id", function(d) {
return d.id;
})
.attr("d", path);
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height)
.call(zoom);
function zoomed() {
features.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
features.select(".precinct").style("stroke-width", 1.5 / d3.event.scale + "px");
}
Somehow, zoomed is never getting called. If I set a breakpoint on zoomed it never catches. I think that the final append to svg calls zoom which somehow sets up a d3 behavior that creates listeners for mouse events and calls the zoomed function. That's what I understand so far about what is going on (clarification or detailed answers would be great). Is the problem that the listeners are not getting set? If so, how do I debug why they are not getting set? The overlay shows up in my svg -- it just does not seem to be picking up mouse events.
The problem in your case is unrelated to the zoom behaviour. You're setting the z-index of the div containing the map to be -1. This means that it's behind the containing div, which gets all the mouse events. So the map is "obscured" by the element that contains it.
To fix, either set the z-index of the map div to be higher than -1, or set the z-index of all the containing elements (including the body) to be -1 or less.

Categories

Resources