d3: how to drag line elements independently of background - d3.js

I have developed an applet that shows a d3 diagonal tree. The graph is navigatable by dragging the background.
It is based on the code found at the following link:
https://bl.ocks.org/adamfeuer/042bfa0dde0059e2b288
I am trying to have vertical lines across the page to further annotate the tree/ graph (based on the following link: https://bl.ocks.org/dimitardanailov/99950eee511375b97de749b597147d19).
See below:
See here: https://jsfiddle.net/chrisclarkson100/opfq6ve8/28/
I append the lines to the graph as follows:
var data_line = [
{
'x1': 300,
'y1': 700,
'x2': 300,
'y2': 700
},
////....
];
// Generating the svg lines attributes
var lineAttributes = {
....
'x1': function(d) {
return d.x1;
},
'y1': function(d) {
return screen.availHeight;
},
'x2': function(d) {
return d.x2;
},
'y2': function(d) {
return 0;
}
};
var drag_line = d3.behavior.drag()
.origin(function(d) { return d; })
.on('drag', dragged_line);
// Pointer to the d3 lines
var svg = d3.select('body').select('svg');
var lines = svg
.selectAll('line')
.data(data_line)
.enter().append('g')
.attr('class', 'link');
links_lines=lines.append('line')
.attr(lineAttributes)
.call(drag_line);
lines.append('text')
.attr('class','link_text')
.attr("x", d => d.x1)
.attr("y", d => 350)
.style('fill', 'darkOrange')
.style("font-size", "30px")
function dragged_line() {
var x = d3.event.dx;
var y = d3.event.dy;
var line = d3.select(this);
// Update the line properties
var attributes = {
x1: parseInt(line.attr('x1')) + x,
y1: parseInt(line.attr('y1')) + y,
x2: parseInt(line.attr('x2')) + x,
y2: parseInt(line.attr('y2')) + y,
};
line.attr(attributes);
}
The lines display as I wanted and are draggable. However, when I drag them, the background/ tree network moves with them... I want the different draggable elements to be independent of eachother.
Can anybody spot how I'm failing to make the dragging interactivity of each element independent of eachother?

Here's a JSFiddle that seems to do what you want– you can move the vertical lines without moving the tree; and move the tree without moving the vertical lines:
https://jsfiddle.net/adamfeuer/gd4ouvez/125/
(That JSFiddle uses a much smaller dataset of tree nodes; the one you linked to was too big to easily iterate and debug.)
The issue with the code you posted is that the zoom (pan) function for the tree is active at the same time the zoom() for the lines is active, so the tree and the active line drag at the same time.
I added a simple mechanism to separate the two – a boolean called lineDragActive. The code then checks for that in the tree zoom(), sets it true when a line drag starts, and false when the line drag ends:
// Define the zoom function for the zoomable tree
// flag indicates if line dragging is active...
// if so, we don't want to drag the tree
var lineDragActive = false;
function zoom() {
if (lineDragActive == false) {
// not line dragging, so we can zaoom (drag) the tree
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
}
[...]
function drag_linestarted() {
// tell others not to zoom while we are zooming (dragging)
lineDragActive = true;
d3.select(this).classed(activeClassName, true);
}
function drag_lineended() {
// tell others that zooming (dragging) is allowed
lineDragActive = false;
d3.select(this).classed(activeClassName, false);
label = baseSvg.selectAll('.link_text').attr("transform", "translate(" + String(Number(this.x1.baseVal.value) - 400) + "," + 0 + ")");
}

Related

How to limit draggable area in d3?

I have a draggable element in d3, but I want to limit the draggable area to the borders of the svg, so that it never goes out of view. Is this possible? Here's a jsfiddle with no limit on where the element can be dragged:
https://jsfiddle.net/asicfr/34TKg/
var drag1 = d3.behavior.drag()
.origin(function() {
var t = d3.select(this);
return {x: t.attr("x") + d3.transform(t.attr("transform")).translate[0],
y: t.attr("y") + d3.transform(t.attr("transform")).translate[1]};
})
.on("drag", function(d,i) {
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d3.event.x,d3.event.y ] + ")"
})
});
group.call(drag1);
Nice clean JSFiddle - makes it easy to help you!
You can do so manually given that you know the dimensions of the container and the element you want to drag.
In essence you can check whether the potential new location of your dragged object will going beyond the bounds of the container. If so, then set the element's position to values that will make it be fully visible inside the contairer.
I have made a JSFiddle showcasing it: https://jsfiddle.net/0sga1ypc/
Most of the my updates are made in the callback function to the drag event:
.on("drag", function(d,i) {
var left = d3.event.x
if (left + groupWidth + strokeWidth > svgWidth) {
left = svgWidth - groupWidth - strokeWidth
} else if (d3.event.x < 0) {
left = 0
}
var top = d3.event.y
if (top + groupHeight + strokeWidth > svgHeight) {
top = svgHeight - groupHeight - strokeWidth
} else if (d3.event.y < 0) {
top = 0
}
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ left,top ] + ")"
})
});
The only other changes I have made is to save relevant dimension values into variables, so that they are not writte out multiple times.
Hope this helps!

d3 - sunburst partition. Different sizes for each node

I'm a newbie to the d3 library and javascript in general.
I'm trying to achieve something like
this, where I have a sunburst partition but each node has a different height with respect to the radial center - but the padding to its parent/child stays the same.
I've tried looking around and couldn't come up with any solutions.
(trying to change the innerRadius/outerRadius parameters didn't seem to work :( ).
Here is my code:
var vis = d3.select("#chart").append("svg")
.style("margin", "auto")
.style("position", "relative")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.sort(function (a, b) { return d3.ascending(a.time, b.time); })
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.n_leaves+1; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
//read data from json file and visualize it
d3.text("5rrasx_out.json", function(text) {
var data = JSON.parse(text);
var json = buildHierarchy(data,'5rrasx');
createVisualization(json);
});
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json);
var dataSummary = [{label: 'pos', count: totalPos}, {label: 'neg', count: totalNeg}];
//set title
$("#title").text(json.title.replace(/\[.*\]/g,""));
//set chart
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("path")
.attr("class", "sunburst_node")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return (d.sentiment > 0) ? colors["pos"] : colors["neg"]; })
.style("opacity", 1)
.on("mouseover", mouseover)
.on("click", click);
};
Any help would be much appreciated.
Thanks!
I know this is not a proper answer to the question above, but in case someone needs a sunburst with different dimensions for each node, here I post how to do it in R using the ggsunburst package.
# install ggsunburst
if (!require("ggplot2")) install.packages("ggplot2")
if (!require("rPython")) install.packages("rPython")
install.packages("http://genome.crg.es/~didac/ggsunburst/ggsunburst_0.0.10.tar.gz", repos=NULL, type="source")
library(ggsunburst)
# one possible input for ggsunburst is newick format
# consider the following newick "(((A,B),C),D,E);"
# you can define the distance in node A with "A:0.5"
# you can define size in node E with "E[&&NHX:size=5]"
# adding both attributes to the newick
nw <- '(((A:0.5,B),C:3),D[&&NHX:size=5],E[&&NHX:size=5]);'
sb <- sunburst_data(nw)
sunburst(sb, rects.fill.aes = "name") + scale_fill_discrete(guide=F)
as you can see in the code, these attributes can be defined independently, and as you can see in the plot they affect the dimennsions of the correponding nodes:
node "A" is 0.5 times shorter than "B", which is defined by the attribute "distance"
E has an angle 5 times wider than C, which is defined by the attribute "size".
and here an attempt to resemble the example posted in the question with a newick tree
nw <- "(((.:0[&&NHX:support=1.0:dist=0.0:name=.:size=3],a3:1[&&NHX:color=2:support=1.0:dist=1.0:name=a3:size=1])1:1[&&NHX:color=-3:support=1.0:dist=1.0:name=a2])1:1[&&NHX:color=-1:support=1.0:dist=1.0:name=a1],b1:1.8[&&NHX:color=1:support=1.0:dist=1.8:name=b1:size=5],(((a4:1[&&NHX:color=1:support=1.0:dist=1.0:name=a4:size=1],b4:1.8[&&NHX:color=-1:support=1.0:dist=1.8:name=b4:size=1],c4:1.5[&&NHX:color=2:support=1.0:dist=1.5:name=c4:size=1],d4:0.8[&&NHX:color=-2:support=1.0:dist=0.8:name=d4:size=1])1:1[&&NHX:color=1:support=1.0:dist=1.0:name=b3:size=1])1:1[&&NHX:color=-3:support=1.0:dist=1.0:name=b2:size=1],(c3:1[&&NHX:color=1:support=1.0:dist=1.0:name=c3:size=1],(e4:1[&&NHX:color=-2:support=1.0:dist=1.0:name=e4:size=1])1:0.5[&&NHX:color=-1:support=1.0:dist=0.5:name=d3:size=1])1:0.5[&&NHX:color=1:support=1.0:dist=0.5:name=c2:size=1])1:1[&&NHX:color=-1:support=1.0:dist=1.0:name=c1:size=1],d1:0.8[&&NHX:color=3:support=1.0:dist=0.8:name=d1:size=20]);"
sb <- sunburst_data(nw, node_attributes = "color")
sunburst(sb, leaf_labels.size = 4, node_labels.size = 4, node_labels = T, node_labels.min = 1, rects.fill.aes = "color") +
scale_fill_gradient2(guide=F) + ylim(-8,NA)

d3.js sunburst with dynamically built updatable data structure

I am attempting to create a fully dynamic Sunburst graph using d3.js.
The examples and tutorials I have located tend to use existing/fully-populated data structures which may have the ability to modify the value of existing arcs but does not allow the ability to add child arcs as needed.
Likewise the tutorials I have located which allow new datasets simply replace the existing structure and begin drawing from scratch.
This is not the behavior I am trying to implement.
What I need is a dynamically built graph based on incoming data as it is provided.
I am able to append children to the end of the data set, transition and render the results without issue. The problem occurs any time I insert a child somewhere within the existing structure, d3’s selectAll() does not function as expected. It includes the new arc (which has yet to be drawn) resulting in any remaining arcs being rendered incorrectly. Then when transitioning the arcs it seems to get the arcs Dom ID and data it supposedly represents gets mixed up. The new arc is not visible and an empty space exists where new arc should be placed.
To be clear my intent is:
Add to the existing data structure allowing new children to be added when new information is provided
To transition existing arcs opening space for the new arcs before they are created and drawn
Broken down into four steps of the jsfiddle example:
Initialization of the graph (draws an invisible “root” arc)
{ name:"a_0", children: [] }
Adding First Child data and it’s children to root
{ name:"a_0", children:[
{ name:"a_1", children:[ { name:"a_2", children:[ { name:"a_3" } ] } ] }
] }
Adding Second Child and underlying children to root
{ name:"a_0", children:[
{ name:"a_1", children:[ { name:"a_2", children:[ { name:"a_3" } ] } ] },
{ name:"a_4", children:[ { name:"a_5", children:[ { name:"a_6" } ] } ] }
] }
Inserting another child within the existing arc a_2
{ name:"a_0", children:[
{ name:"a_1", children:[
{ name:"a_2", children:[
{ name:"a_3" },
{ name:"a_7" }
] }
] },
{ name:"a_4", children:[
{ name:"a_5", children:[
{ name:"a_6" }
] }
] }
] }
Step 1 works just fine
Step 2 draws the arcs properly
Step 3 transitions the existing arcs and adds the new arcs to the graph
Step 4 results some unexpected behavior.
During the transition of existing and entering of new arcs some of the arcs "jump around" losing the proper association with their respective data
The end result appears to be:
a_0 - is correct
a_1 & a_2 - look correct
a_3 - has shrunk to accommodate the new sibling a_7 - expected behavior
a_4 - disappears
a_5 - jumps down where a_4 should be
a_6 - (looks like) it is duplicated and exists once where it should be and where a_5 should be
a_7 - not displayed, location where it should be is empty space and appears to be associated with a_6 data
What the end result looks like and what is really going on are not the same.
In the attempt to update the graph the selectAll() for the existing arcs includes (a_0, a_1, a_2, a_3, a_4, a_5, a_7). Where the existing a_6 is not included in the selectAll() but a_7 (which has not been drawn) is.
The enter() function appears to operate on the existing a_6 which is then treated as a new arc
It looked like I was on the right track getting all the way to a_6, but I have not figured out the reason for the behavior when adding a_7.
The jsFidde executes the steps as described above including:
Unique colors for each arc
A table displaying the name of each arc,
If the arc is being handled by d3js' selectAll() (i.e. "existing") or enter() (i.e. "new"),
The d3 Index as it is currently being assigned when drawing existing or new arcs.
Expected target position where each arc should appear after any transitioning,
Arctween information as an Arc is being transitioned from its former location to the new location and
Questions:
What is going on that would cause this behavior in Step 4?
Is there a way to ensure the integrity between each arc and the data it represents?
Is there a way to insert children into the existing structure or update the graph in this dynamic manor?
Working example on jsfiddle https://jsfiddle.net/mfitzgerald/j2eowwya/
var dataObj = { name:"a_0", color: "none" };
var height = 300;
var width = 500;
var radius = Math.min(width, height) / 2;
var graph = d3.select("#graph")
.attr('height', height)
.attr('width', width)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d, i) { return 1; });
var arc = d3.svg.arc()
.startAngle(function(d) { if (isNaN(d.x)) { d.x = 0; } d.x0 = d.x; return d.x; })
.endAngle(function(d) { if (isNaN(d.dx)) { d.dx = 0; } d.dx0 = d.dx; return d.x + d.dx; })
.innerRadius(function(d) { if (isNaN(d.y)) { d.y = 0; } d.y0 = d.y; return Math.sqrt(d.y); })
.outerRadius(function(d) { if (isNaN(d.dy)) { d.dy = 0; } d.dy0 = d.dy; return Math.sqrt(d.y + d.dy); });
var arcTween = function(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0, y: a.y0, dy: a.dy0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
a.y0 = b.y;
a.dy0 = b.dy;
displayStats("arctween", b);
return arc(b);
};
}
// Root Arc
graph.datum(dataObj).selectAll('path.arc')
.data(partition.nodes)
.enter()
.append('path')
.attr('class', function(d) { return "arc " + d.name; })
.attr("d", arc)
.attr("id", function(d, i) { return "path_"+i; })
.attr("name", function(d) { return d.name; })
.style("fill", "none");
function updateGraph() {
console.log("Update Graph");
console.log(dataObj);
var update = graph.datum(dataObj).selectAll('path.arc')
.data(partition.nodes);
// Move existing Arcs
update.each(function(d, i) {
displayStats("target", d, i, "existing");
var domId = $(this).attr("id");
console.log("["+i+"] Exist Arc name:"+d.name+", dom_id:"+domId);
})
.transition()
.delay(function(d, i) { return i * 250; })
.duration(1500)
.attrTween("d", arcTween);
// Add New Arcs
update.enter().append('path')
.attr('class', function(d, i) { return "arc "+d.name; })
.attr("d", arc)
.attr("id", function(d, i) {
var domId = "path_"+i;
console.log("["+i+"] NEW Arc name:"+d.name+", dom_id:"+domId);
displayStats("target", d, i, "new");
return domId;
})
.style("stroke", "#fff")
.style("fill", function(d) { return d.color; })
.style("opacity", 0)
.transition()
.delay(function(d, i) { return i * 250; })
.duration(1500)
.style("opacity", .5)
.attrTween("d", arcTween);
}
#Gordon has answered the question. The issue was resolved by adding a key function when joining with .data() in the updateGraph code.
Forked example on jsfiddle
var update = graph.datum(dataObj).selectAll('path.arc')
.data(partition.nodes, function(d) { return d.name; } );
I believe the answers to the questions are:
The .data() function uses an indexed array which only uniquely identifies each arc given any new arcs are appended to the end of the array. Once one is inserted this would cause the data, graphed arcs and associated DOM ids to be misaligned.
Using the key function, as suggested by Gordon, allows unique identification of specific nodes keeping the Data and Graph in sync as expected.
Update
An additional modification would need to be made as the DOM id was set by the array index of the data element there would still be an invalid association with the DOM and the underlying graph/data.
This would result in 2 a_4 DOM id's. Instead of using the array index using the Node Name as the DOM id should keep this association correct.

d3 circles shifting position on click-to-zoom

I'm trying to implement zooming on a d3 graphic with a bunch of data as circles. The data is made up of 1 large circle and many smaller circles plotted inside it. I want to click on the smaller circles and zoom to them. I'm using a variation of the zoom from the zoomable circle packing demo. I don't want to use the demo code directly but I've got it mostly working.
Initially all the circles are in their correct positions. However when I click on the smaller circles, they shift position right before the zoom. When I click on the white circle to zoom back, you can see they are now permanently shifted. And when it does zoom, the circles don't zoom into the center of the viewport, like they do in the demo.
I've noticed that when I comment out the transform line
node.attr("transform", function(d) { return "translate(" + (xscale(d.cx) - v[0]) * k + "," + (yscale(d.cy) - v[1]) * k + ")"; });
the circles now remain in their correct positions. Their sizes scale up as they should, but now they just merge into one another as they get bigger, because I'm no longer translating them. So the problem must be something in my transform attribute, but I can't figure out what it is. Or maybe it's something with my initial view? When I uncomment the zoomTo(view) the circles immediately move to the incorrect positions.
How do I get their positions to remain in the right positions? And how do I get the circles to zoom to the center of the viewpoint? I thought I followed the demo code pretty closely, but it's not quite working right. Any ideas?
I'd also like the axes to zoom as well but I haven't gotten that far into my problem yet.
Here's my jsfiddle.
And my full javascript code
function loadPlateDesign(){
var width = 400;
var height = 400;
var padding = 55;
var plateid = 7443;
var plateCen = {'ra': 230.99167, 'dec': 42.68736 };
var data = [{'name':7443,'color': 'white', 'cx': 0.0, 'cy': 0.0, 'r': 200},
{'color': 'red', 'cx': 8.23066, 'cy': -134.645, 'ra':231.1,'dec':42.1,'name': '1901', 'r': 10.0,
'children':[{'color': 'red', 'cx': 8.23066, 'cy': -134.645, 'ra':231.1,'dec':42.1,'name': 'a', 'r': 2.0}]},
{'color': 'blue', 'cx': -167.524, 'cy': -90.009, 'name': '711', 'r': 5.0}];
var xscale = d3.scale.linear().domain([330.,-330.]).range([0,400]);
var yscale = d3.scale.linear().domain([330.,-330.]).range([0,400]);
// initial focus and view
var focus = {'name':7443,'color': 'white', 'cx': 0.0, 'cy': 0.0, 'r': 200};
var view = [xscale(0.0),yscale(0.0),200*2];
// make the main svg element
var svg = d3.select('#platedesign').append('svg')
.attr('width',width+padding)
.attr('height',height+padding);
// add the plate and ifu data
var ifus=svg.selectAll('circle').data(data).enter().append('circle')
.attr('id',function(d){return d.name;})
.attr('cx',function(d,i){return xscale(d.cx);})
.attr('cy',function(d,i){return yscale(d.cy);})
.attr('r',function(d,i){return d.r;})
.style('fill',function(d,i){return d.color;})
.style('stroke','black')
.on('click',function(d){
if (focus != d) zoom(d), d3.event.stopPropagation();
});
// add the axes
var rascale = d3.scale.linear().domain([plateCen.ra+1.5,plateCen.ra-1.5]).range([0,400]);
var decscale = d3.scale.linear().domain([plateCen.dec+1.5,plateCen.dec-1.5]).range([0,400]);
xaxis = d3.svg.axis().scale(rascale).orient('bottom');
yaxis = d3.svg.axis().scale(decscale).orient('right').ticks(5);
svg.append('g').attr('class','x axis')
.attr('transform','translate(0,'+(height+5)+')')
.call(xaxis)
.append('text')
.attr('x',width/2)
.attr('y',35)
.style('text-anchor','middle')
.text('RA');
svg.append('g').attr('class','y axis')
.attr('transform','translate('+(width+5)+',0)')
.call(yaxis)
.append('text')
.attr('transform','rotate(90)')
.attr('x',height/2)
.attr('y',-35)
.style('text-anchor','middle')
.text('Dec');
var node = svg.selectAll("circle");
//zoomTo(view);
function zoom(d){
console.log('zooming to', d.name);
var focus0 = focus; focus=d;
var newview = [xscale(d.cx), yscale(d.cy), d.r*2+20];
var transition = d3.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween('zoom', function(d){
var i = d3.interpolateZoom(view, newview);
return function(t) {zoomTo(i(t)); };
});
}
function zoomTo(v) {
var k = height / v[2]; view = v;
console.log(height, v);
node.attr("transform", function(d) { return "translate(" + (xscale(d.cx) - v[0]) * k + "," + (yscale(d.cy) - v[1]) * k + ")"; });
ifus.attr("r", function(d) { return d.r * k; });
}
}
Looks like you are mixing positioning methods. You set an initial cx and cy but then you zoom on a transform. Re-factoring a bit to get all the positioning done with transform fixes it up. I also found that you should initiate the view and do the zoom calculations on your d.cx and d.cy instead of on the xscale(d.cx).
function zoom(d){
console.log('zooming to', d.name);
var focus0 = focus; focus=d;
var newview = [d.cx, d.cy, d.r*2+20];
var transition = d3.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween('zoom', function(d){
var i = d3.interpolateZoom(view, newview);
return function(t) {zoomTo(i(t)); };
});
}
function zoomTo(v) {
var k = height / v[2]; view = v;
console.log(height, v);
node.attr("transform", function(d) { return "translate(" + xscale((d.cx - v[0]) * k) + "," + yscale((d.cy - v[1]) * k) + ")"; });
ifus.attr("r", function(d) { return d.r * k; });
}
Updated fiddle.

How do I adjust zoom size for a point in D3?

This could be a classic case of "you're doing it wrong", but all of my searching to date hasn't warranted any help.
Here's my scenario:
I'm using an albersUSA map projection in conjunction with the national and county GeoJson files to draw everything.
I also have a self created "cities" file that contains major cities for each state. The coordinates are accurate and everything looks good.
When a user clicks on a given state, I hide all state shapes and then calculate the transform needed to get the county shapes for that state to fit within my viewport. I then apply that transform to all the necessary county shapes in order to get the "zoomed" view. My code is as follows:
function CalculateTransform(objectPath)
{
var results = '';
// Define bounds/points of viewport
var mapDimensions = getMapViewportDimensions();
var baseWidth = mapDimensions[0];
var baseHeight = mapDimensions[1];
var centerX = baseWidth / 2;
var centerY = baseHeight / 2;
// Get bounding box of object path and calculate centroid and zoom factor
// based on viewport.
var bbox = objectPath.getBBox();
var centroid = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
var zoomScaleFactor = baseHeight / bbox.height;
var zoomX = -centroid[0];
var zoomY = -centroid[1];
// If the width of the state is greater than the height, scale by
// that property instead so that state will still fit in viewport.
if (bbox.width > bbox.height) {
zoomScaleFactor = baseHeight / bbox.width;
}
// Calculate how far to move the object path from it's current position to
// the center of the viewport.
var augmentX = -(centroid[0] - centerX);
var augmentY = -(centroid[1] - centerY);
// Our transform logic consists of:
// 1. Move the state to the center of the screen.
// 2. Move the state based on our anticipated scale.
// 3. Scale the state.
// 4. Move the state back to accomodate for the scaling.
var transform = "translate(" + (augmentX) + "," + (augmentY) + ")" +
"translate(" + (-zoomX) + "," + (-zoomY) + ")" +
"scale(" + zoomScaleFactor + ")" +
"translate(" + (zoomX) + "," + (zoomY) + ")";
return results;
}
...and the binding function
// Load county data for the state specified.
d3.json(jsonUrl, function (json) {
if (json === undefined || json == null || json.features.length == 0)
{
logging.error("Failed to retrieve county structure data.");
showMapErrorMessage("Unable to retrieve county structure data.");
return false;
}
else
{
counties.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("id", function (d, i) {
return "county_" + d.properties.GEO_ID
})
.attr("data-id", function (d, i) { return d.properties.GEO_ID })
.attr("data-name", function (d, i) { return countyLookup[d.properties.GEO_ID] })
.attr("data-stateid", function (d, i) { return d.properties.STATE })
.attr("d", path);
// Show all counties for state specified and apply zoom transform.
d3.selectAll(countySelector).attr("visibility", "visible");
d3.selectAll(countySelector).attr("transform", stateTransform);
// Show all cities for the state specified and apply zoom transform
d3.selectAll(citySelector).attr("visibility", "visible");
d3.selectAll(citySelector).attr("transform", stateTransform);
}
});
This works fine here, except for really small states, the zoom factor is much larger, and the circles get distored.
Is there a way to force the size of the points to be a fixed size (say a 15px radius) even after the transform occurs?
For things you don't want to scale, just make them divided by 'scale' . In my case,
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll(".mapmarker")
.attr("r",6/d3.event.scale)
.attr("stroke-width",1/d3.event.scale);
});
This is happening because you are setting a scale transform instead of scaling the positions. You can see the difference here Basically it is the difference between:
// Thick lines because they are scaled too
var bottom = svg.append('g').attr('transform', 'scale('+scale+','+scale+')');
bottom.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
and
// line thicknesses are nice and thin
var top = svg.append('g');
top.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x * scale; })
.attr('cy', function(d) { return d.y * scale; });
With mapping probably you best solution is to compute your offset and scale as you do and then add them into your projection function - you want to directly modify the post-projection x and y values. If you update your projection function properly you should not have to do anything else to apply the appropriate zoom to your map.

Resources