unexpected behaviour of d3.js attrTween - d3.js

I'm a newby in D3.js. I adapted this example from Mike Bostock, into this fiddle, which provides an example of Point-Along-Path Interpolation based on attrTween and getPointAtLength.
In the original example a single circle shape is introduced, which follows a simple path. In the adapted example, instead of introducing a single circle, a bunch of circles are generated which follow a more complex shape, a toy example path made in Inkscape.
The animation runs fine for a number of iterations, but after a short while the circles appear to get caught up in a loop and ultimately the page freezes up. However, if only a single circle is generated e.g. var RadiusData = [20]; (see code below), the animation keeps looping just fine.
What could be causing this? Is there an easy way to avoid this behavior?
var w = $(window).width(),
h = $(window).height();
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + 0.25*w + "," + 0 + ")");
//some toy path data
var dInkScape ="m 360.1639,630.31678 c 1.0609,13.05167 -195.29107,-273.68628 -203.49722,-275.81173 -22.23818,-5.75983 -24.83733,-34.59299 -15.23811,-51.66666 17.17076,-30.54078 59.06286,-32.72422 85.71428,-14.04764 39.11203,27.40863 40.85844,83.86959 12.85717,119.7619 C 202.67874,456.39146 131.20349,457.65152 86.190506,420.21936 29.546262,373.1148 28.796105,286.43841 75.714265,232.36222 132.53844,166.8687 234.51201,166.64035 297.61902,223.07645 c 74.36943,66.50798 74.06939,183.83474 8.09531,255.95237 C 229.54464,562.29148 96.8291,561.45911 15.714334,485.93366 -76.453418,400.11684 -75.086213,251.98848 9.9999617,161.88605 105.45379,60.804734 269.012,62.70845 368.09519,157.36214 478.09632,262.44568 489.74023,530.06221 385.51394,638.12097 z";
var path = svg.append("svg:path")
.attr("d", dInkScape);
//some random data for the circle radii
var RadiusData = [20,50,25,5,40,22,50,66,72,23];
//introduce a circle for each element, set radius and give it some random color
var circle = svg.selectAll("circle")
.data(RadiusData).enter()
.append("svg:circle")
.attr("r", function(d){return d;})
.style("fill",function(d,i) {return "hsl(" + 120 + 100 *Math.random() + ",100%,25%)";})
.attr("transform", "translate(0," + -h / 3 + ")");
//with a 1 second delay introduce a new circle
function transition() {
circle.transition()
.duration(5000)
.delay(function(d,i){return 1000*i;})
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
}
transition();
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}

The problem is that you are re-calling the transition function at the end of each circle's movement, but that function creates a new transition for every circle:
//with a 1 second delay introduce a new circle
function transition() {
circle.transition() //add a transition to every circle
.duration(5000)
.delay(function(d,i){return 1000*i;})
.attrTween("transform", translateAlong(path.node()))
.each("end", transition);
//re-run this function after *each* circle's transition completes
}
One option for fixing it is to have the each function only re-start the transition for the first element, i.e. when i==0 or !i is true:
function transition() {
circle.transition()
.duration(5000)
.delay(function(d,i){return 1000*i;})
.attrTween("transform", translateAlong(path.node()))
.each("end", function(d,i){if (!i) transition(); });
}
http://jsfiddle.net/A88W3/8/
The other option, as #mbostock just suggested, is to make your function only apply to a single element:
function transitionThis(d,i) { //index is given to the function
d3.select(this).transition()
.duration(5000)
.delay(1000*i) //not a function anymore
.attrTween("transform", translateAlong(path.node()))
.each("end", transitionThis); //repeat for this element
}
circle.each(transitionThis); //start transitions for each
http://jsfiddle.net/A88W3/9/
Or, if you only wanted to apply the delay once, to stagger the start times, but then to have all the circles move evenly without stopping at the beginning of the path:
function transitionThis(d,i) {
d3.select(this).transition()
.duration(5000) //no delay once started
.ease("linear") //move steadily at all points on path
.attrTween("transform", translateAlong(path.node()))
.each("end", transitionThis); //repeat for this element
}
circle.transition().duration(0)
.delay(function(d,i){return 1000*i;}) //stagger starts
.each(transitionThis); //start transitions for each
http://jsfiddle.net/A88W3/10/
One other thing: at least during debugging, it's always a good idea to code-in a way to stop any infinite loops. I did so in the above fiddles by adding a click function to the svg as a whole that creates a new transition on the circles, interrupting the infinite looping versions:
svg.on("click", function() { //Stop the infinite transitions!
circle.transition(); //create a new empty transition
});

The problem is you’re starting a new transition on all on the circles whenever any one of the circles finishes a transition, leading to an explosion of overlapping transitions if your initial selection has more than one element.
The transition.each callback is called for each element in the selection. You probably want to say d3.select(this) to create a transition for an individual element, as in the chained transitions example.

Related

d3js flicker on transform with text inside g

i have the following https://jsfiddle.net/zzxpw3o0/
function dragstart(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("fixed", d.fixed = true);
}
when dragging the circle i get strange flicker
If i remove the g wrapping the circle and the text all is good.
https://jsfiddle.net/e2t2z8uj/
Wondering if i can fix the flicker. I have around 1k circles with text label , so i don't want to create 2k svg:g for circle + text.
Just move your call to the force.drag function from the circles to the group, like so:
var nodes = svg.selectAll("g")
.data(d3.values(nodes))
.enter()
.append("g")
.call(force.drag); // <= move that line here
nodes.append("circle")
.attr("r", 10)
.style("fill", function (d, i) { return colors(i); })
// .call(force.drag); // <= remove this line
The updated fiddle is here.

How do I properly add transitions to D3 Polygons?

Original Code can be found at: http://bl.ocks.org/Guerino1/451f4c47842967dd813c8a64b24f7686
Problem: Applying .transition() code to different polygon sets appears to yield different results.
The following portion of the code seems to work as expected. Applying a transition causes the chevrons to transition onto the svg canvas, from left to right...
svgChevronsCanvas.selectAll("a")
.data(dataSet)
.enter().append("a")
.attr("xlink:href", function(d) { return d.link; })
.append("svg:polygon")
.attr("id", function(d,i){ return ("chevron_" + selectString.replace(/ /g,'_').replace(/#/g,'') + "_index_" + i); })
.attr("originalcolor", polygonPrimaryColor)
//.style("stroke","blue")
//.style("stroke-width",1)
.style("fill", polygonPrimaryColor)
.attr("points", chevronOrigin)
.on('mouseover', chevronMouseOver)
.on("mouseout", chevronMouseOut)
.on("click", chevronMouseOut)
.transition() // <------------------- TRANSITION HERE
.duration(3000)
.attr("points", calculateChevron);
The following code, which attempts to follow the same pattern as above does not seem to work as expected. Given the transition, I would expect the textboxes (in light blue below the chevrons), which are also drawn using D3 polygons, to transition onto their svg canvas from left to right, just like the chevron polygons in the above code...
svgTextboxesCanvas.selectAll("a")
.data(dataSet)
.enter().append("a")
.attr("xlink:href", function(d) { return d.link; })
.append("svg:polygon")
.attr("id", function(d,i){ return ("textbox_" + selectString.replace(/ /g,'_').replace(/#/g,'') + "_index_" + i); })
.attr("originalcolor", textboxColor)
.style("stroke", textboxColor)
.style("stroke-width",1)
.style("fill", textboxColor)
.attr("points", textboxesOrigin)
.on('mouseover', textboxMouseOver)
.on("mouseout", textboxMouseOut)
.on("click", textboxMouseOut)
.transition()
.duration(3000)
.attr("points", calculateTextbox);
Question: How do I properly add transitions to the D3 polygons that are built to look like rectangles (below the chevrons), in the latter set of code, and make them transition into the page just like the dark blue chevrons above them?
In the original code:
Make
var chevronGapSpace = 5;//this is the distance between each rectangle.
var slantDepth = 0;//to make the polygon rectangle.
Next, to make rectangle transition inside function calculateChevron change the calculations accordingly:
function calculateChevron(d, i){
return [
[(svgMargin) + i*(chevronDistance),chevronTopOffset],
[(svgMargin + chevronWidth - slantDepth) + i*(chevronDistance),chevronTop],
[(svgMargin + chevronWidth - slantDepth) + i*(chevronDistance),chevronPointY],
[(svgMargin + chevronWidth - slantDepth) + i*(chevronDistance),chevronBottom],
[(svgMargin) + i*(chevronDistance),chevronBottom],
[(svgMargin + slantDepth) + i*(chevronDistance),chevronPointY]
];
};
working code here

Force-directed graph: How to add icon in the middle of an edge?

How would it be possible to place an icon in the middle of all edges of a force-directed graph?
This is how I imagine the graph to look like:
I have read this post. However, this does not seem to work in my case or I have not understood the mechanics.
How could this icon be equipped with a click event listener? Would the icon element - when clicked - "know" which path it belongs to?
Many thanks.
I have annotated the 3 problematic code snippets with [?] HELP - THIS DOES NOT WORK in the code below. The code is massively shortened and will not run as is.
<!DOCTYPE html>
<meta charset="utf-8">
<!--
Structure:
- #illustration_content (explicitly defined as div)
- svg
- g (The g element is a container used to group objects.)
- path (for links)
- text (for labels)
- circle (for nodes)
- marker (for arrow heads)
- icon (for icons)
-->
<head>
<!-- EDITED OUT-->
</head>
<body>
<!-- EDITED OUT-->
<script>
// Declare the object that will contain the nodes later on
var nodes = {};
// <-- EDITED OUT-->
// Set the size of the illustration (used multiple times --> hence defined here)
var width = 800, // make sure this fits to the width of the illustration div
height = 600;
// <-- EDITED OUT-->
var linkDistanceVariable = 50; // we will update this with increasing number of nodes
// Create the force layout for d3
var force = d3.layout.force()
.size([width, height])
.linkDistance(function() { return linkDistanceVariable; })
.friction(0.7) //at each tick of the simulation, the particle velocity is scaled by the specified friction.
.charge(-600) //A negative value results in node repulsion, while a positive value results in node attraction.
.alpha(0.2) // cooling parameter: If you do not stop the layout explicitly, it will still stop automatically after the layout's alpha decays below some threshold.
.on("tick", tick); // this calls the tick function (which is in the separate JS) - the function is executed for each step of the animation
// Append the d3 illustration to the illustration div container
var svg = d3.select("#illustration_content").append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black");
// Append stuff
var path = svg.append("g").selectAll("path"),
text = svg.append("g").selectAll("text"),
circle = svg.append("g").selectAll("circle"),
marker = svg.append("defs").selectAll("marker");
// [?] HELP - THIS DOES NOT WORK
var icon = svg.append("svg:g").selectAll("g").data(force.links()).enter().append("svg:g");
// Update function to update the visualisation with new data
function update(json){
// <-- EDITED OUT (~100 LOC)-->
// The idea here is to prevent the illustration from pulsing (i.e. updating when nothing has changed)
// This introduces unnecessary movements
// This will only update the screen if the new link array length differs from the previous one
// This should be improved (also other things might change like values of properties)
// Essentially, currently only a change in number of links will trigger an update (not a change in confidence etc.)
if (links_previously.length !== links.length){
console.info("Is the links array length different than previous one? -->" + (links_previously !== links));
// Stop the layout
force.stop();
// Set the new linkDistance val
linkDistanceVariable = 40 + 3 * Object.keys(nodes).length; // new
// Start the layout again
force.start();
force
.nodes(d3.values(nodes))
.links(links)
.start();
}
// Compute the data join. This returns the update selection.
marker = marker.data(["suit", "licensing", "resolved"]);
// Remove any outgoing/old markers.
marker.exit().remove();
// Compute new attributes for entering and updating markers.
marker.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 20) //this determines position of arrow head along path (x-axis)
.attr("refY", 0) // this determines y-position of arrow head on path (0 = centered)
.attr("markerWidth", 6) // width of arrow head?
.attr("markerHeight", 6) // width of arrow head?
.attr("orient", "auto")
.append("path") // use ".append("line") for lines instead of arrows
.attr("d", "M0,-5L10,0L0,5");
// -------------------------------
// Compute the data join. This returns the update selection.
path = path.data(force.links());
// Remove any outgoing/old paths.
path.exit().remove();
// Compute new attributes for entering and updating paths.
path.enter().append("path")
.attr("class", function(d) { return "link " + d.type; })
.attr("marker-end", function(d) { return "url(#" + d.type + ")"; })
.on("dblclick", pathMouseDoubleClick) // allow path to be clicked;
// -------------------------------
// [?] HELP - THIS DOES NOT WORK
// Idea: https://stackoverflow.com/questions/14567809/how-to-add-an-image-to-an-svg-container-using-d3-js
icon.append("image").attr("xlink:href","imgs/icon.png")
.attr("x", -20)
.attr("y", -2)
.attr("width", 20).attr("height", 20)
.attr("class", "type-icon");
// -------------------------------
// Compute the data join. This returns the update selection.
circle = circle.data(force.nodes());
// Add any incoming circles.
circle.enter().append("circle");
// Remove any outgoing/old circles.
circle.exit().remove();
// Compute new attributes for entering and updating circles.
circle
.attr("r", 10) // size of the circles
.on("dblclick", nodeMouseDoubleClick) // allow nodes to be clicked
.call(force.drag);
// -------------------------------
// Compute the data join. This returns the update selection.
text = text.data(force.nodes());
// Add any incoming texts.
text.enter().append("text");
// Remove any outgoing/old texts.
text.exit().remove();
// Compute new attributes for entering and updating texts.
text
.attr("x", 11) // Distance of the text from the nodes (x-axis)
.attr("y", "0.5em") // Distance of the text from the nodes (y-axis)
.text(function(d) { return d.name; });
// <-- EDITED OUT-->
} // end update function
// ------------------------------------- loadNewData() --------------------------------------
function loadNewData(){
d3.json("data/dataFromPython.json", function(error, json){
// <-- EDITED OUT-->
update(json);
// <-- EDITED OUT-->
});
};
// ------------------------------ search functionality -----------------------------------
// <-- EDITED OUT-->
// -------------------------------------- Hide or show node labels ------------------------------
// <-- EDITED OUT-->
// ------------------------------------- Interval timer ----------------------------------------
// Regularly update the data every x milliseconds (normal JS)
setInterval(function () {
loadNewData();
console.info("Interval timer has just called loadNewData()");
}, 3000);
// ------------------------------------- Tick function ----------------------------------------
// The tick function is executed for each tiny step of the animation
// Use elliptical arc path segments to doubly-encode directionality
function tick(){
path.attr("d", linkArc);
circle.attr("transform", transform);
text.attr("transform", transform);
// For icon in the middle of the path
// does not work, taken from here: https://stackoverflow.com/questions/14582812/display-an-svg-image-at-the-middle-of-an-svg-path
// [?] HELP - THIS DOES NOT WORK
icon.attr("transform", function(d) {
return "translate(" +((d.target.x+d.source.x)/2) + "," + ((d.target.y+d.source.y))/2 + ")";
});
}
// ------------------------------------- Link arc and transform function ----------------------------------------
function linkArc(d){
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = (d.straight == 0)?Math.sqrt(dx * dx + dy * dy):0;
return "M" + d.source.x + "," + d.source.y +
"A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}
function transform(d){
return "translate(" + d.x + "," + d.y + ")";
}
</script>
</body>
As noted in my comment, you were creating a zero length selection because you were binding the data before it had been added to the force layout. Also, you need to select dynamically, not just once.
If you treat the icon selection the same as the others like this...
// Append stuff
var path = svg.append("g"),
text = svg.append("g"),
circle = svg.append("g"),
marker = svg.append("defs"),
icon = svg.append("svg:g");
and then put something like this in your update function, after the new json is bound to the force layout...
function update(json) {
// ...
force
.nodes(d3.values(nodes))
.links(links)
.start();
// ...
// Compute the data join. This returns the update selection.
var markers = marker.selectAll("marker").data(["suit", "licensing", "resolved"]);
// ...
// Compute the data join. This returns the update selection.
var paths = path.selectAll("path").data(force.links());
// ...
var icons = icon.selectAll("g").data(force.links());
icons.enter().append("svg:g")
.append("image").attr("xlink:href", "imgs/icon.png")
.attr("x", -20)
.attr("y", -2)
.attr("width", 20).attr("height", 20)
.attr("class", "type-icon");
icons.exit().remove();
// ...
// -------------------------------
// Compute the data join. This returns the update selection.
var circles = circle.selectAll("circle").data(force.nodes());
// Add any incoming circles.
circles.enter().append("circle")
// Compute new attributes for entering circles.
.attr("r", 10) // size of the circles
.on("dblclick", nodeMouseDoubleClick) // allow nodes to be clicked
.call(force.drag);
// Remove any outgoing/old circles.
circles.exit().remove();
// -------------------------------
var texts = text.selectAll("text").data(force.nodes());
And then in your tick function...
function tick() {
paths.attr("d", linkArc);
circles.attr("transform", transform);
texts.attr("transform", transform);
// For icon in the middle of the path
// does not work, taken from here: http://stackoverflow.com/questions/14582812/display-an-svg-image-at-the-middle-of-an-svg-path
// [?] HELP - THIS DOES NOT WORK
icons.attr("transform", function(d) {
return "translate(" + ((d.target.x + d.source.x) / 2) + "," + ((d.target.y + d.source.y)) / 2 + ")";
});
}
Then it would be close to being ok.
I can't verify because you didn't provide a minimal working example.
Also, notice that the data bound to the icons (force.links()) is the same data that is bound to the paths so, if you put an event listener on it then you can use that information to help you find the link.

D3 how to make dynamic sequential transitions work using transform/translate and offset coordinates?

I am trying to create an interactive org chart such that when I click on a box that box is repositioned in the centre of the SVG container and all other elements transition as well but remain in the same relative position. So if you click the top box in the list, they all move down together. Then if you click one of the lower boxes they all move up together but always so the selected box is in the centre. If you click on a box which is already in the middle it should not move but at the moment they are flying all over the place.
I have got this working for the first click but on each subsequent click the boxes start flying all over the place. I am using the mouse listener to get the current position and calculate an offset to centre the selected box that I feed into transform/translate. I think this is where the strange behaviour is coming from because the offset is calculating correctly (viewed through console.log) but the applied transition is not equal to this calculation.
I have read many posts about transform/translate but they all seem to apply to a single transition, not multiple sequential transitions. I have tried using .attr(transform, null) before each new transition but this didn't work. I have also tried to dynamically extract the current x,y of the selected component and then update these attributes with the offset value but this didn't work either. Am really stuck with this and any help is greatly appreciated!
Thanks,
SD
<script type="text/javascript">
var cwidth = 1000;
var cheight = 500;
var bwidth = 100;
var bheight = 50;
// container definition
var svgContainer = d3.select("body").append("svg")
.attr("width",cwidth)
.attr("height",cheight)
.on("mousemove", mousemove);
// Background gray rectangle
svgContainer.append("svg:rect")
.attr("x",0)
.attr("y",0)
.attr("width",cwidth)
.attr("height",cheight)
.style("fill", "lightgrey");
// data
var secondData = [
{ "idx": 1, "name": "Commercial" },
{ "idx": 2, "name": "Finance" },
{ "idx": 3, "name": "Operations" },
{ "idx": 4, "name": "Business Services" }
];
var secondElements = secondData.length;
// group definition
var secondNodes = svgContainer.append("g")
.attr("class", "nodes")
.selectAll("rect")
.data(secondData)
.enter()
.append("g")
.attr("transform", function(d, i) {
d.x = 300;
d.y = ((cheight/secondElements)*d.idx)-bheight;
return "translate(" + d.x + "," + d.y + ")";
});
// Add elements to the previously added g element.
secondNodes.append("rect")
.attr("class", "node")
.attr("height", bheight)
.attr("width", bwidth)
.style("stroke", "gray")
.style("fill", "white")
.attr("y", function() {return -(bheight/2);})
.on("mouseover", function(){d3.select(this).style("fill", "aliceblue");})
.on("mouseout", function(){d3.select(this).style("fill", "white");})
.on("mousedown", center);
// Add a text element to the previously added g element.
secondNodes.append("text")
.attr("text-anchor", "left")
.attr("x", 15)
.attr("y",5)
.text(function(d) {return d.name;});
// gets current coordinates for transition
var current = [0,0];
var xshift = 0;
var yshift = 0;
// get offset to centre from current mouse location
function mousemove() {
//console.log(d3.mouse(this));
current = d3.mouse(this);
xshift = 500 - current[0];
yshift = 250 - current[1];
}
//applies transitions
function center(d) {
secondNodes.selectAll("rect")
.transition()
.delay(0)
.duration(500)
.attr("transform", "translate(" + xshift + "," + yshift + ")")
.each("end", function() {
secondNodes.selectAll("text")
.transition()
.delay(0)
.duration(0)
.attr("transform", null);
});
}
</script>
If you want everything to keep its relative position, it seems to me that something far easier to do would be to include everything in a g element that you can set the transform attribute one. That is, instead of moving many elements, you would have to move just the top-level container. The code you have for handling clicks would remain pretty much the same except that you only need to set the transform attribute on that one element.

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