Combining translate and rotate with D3 - d3.js

Quite possibly this repeats some of this SO question, but the code is overly-complicated and the OP hasn't added solution code. And this related question is no longer replicable.
I'm trying to figure out how to combine rotations and translations in the right order. It's possible to rotate around the origin as in this example. But when we follow this with a translation the original rotation is undone.
Is it possible to structure this for correct sequential application?
jsfidle code:
HTML:
<script src="http://d3.geotheory.co.uk/d3-transform.js"></script>
SVG:
var svg = d3.select("body").append("svg")
.attr("width", 400)
.attr("height", 300);
//Draw the Rectangle
var rect = svg.append("rect")
.attr("x", 0).attr("y", 0)
.attr("width", 50).attr("height", 100)
.style("fill", "purple");
var rotate = d3.svg.transform().rotate(-45);
var translate = d3.svg.transform().translate(200, 100);
rect.attr('transform', rotate);
var rect2 = rect.attr('transform', rotate);
rect2.attr('transform', translate);

If you're looking to do this in D3 version 4.x+ you can do it like so:
.attr('transform', 'translate(200,100)rotate(-45)')
https://bl.ocks.org/mbostock/1345853

You're creating two different transformations. Assigning one doesn't add to the other. That is, in doing
rect2.attr('transform', translate);
you're undoing the first one, as it is overwritten.
To have both, add them both to one transition, e.g.
var rotateTranslate = d3.svg.transform().rotate(-45).translate(200, 100);
rect2.attr('transform', rotateTranslate);
To do this dynamically, you'll need to do something like this.
.attr("transform", function() {
return d3.svg.transform()
.translate(200, 100)
.rotate(-45)
.translate(-d3.select(this).attr("width")/2, -d3.select(this).attr("height")/2)();
}
Complete jsfiddle here.

Related

d3 v6 pointer function not adjusting for scale and translate

I am upgrading my app from d3 v5 to v6 and am having an issue migrating the d3.mouse functionality. In my app I apply a transform to the top level svg group and use the zoom functionality to zoom and pan (scale and translate). When I double click on the screen I take the mouse position and draw a square.
Now I am replacing the d3.mouse function with d3.pointer. In my double click event I get the mouse position by calling d3.pointer(event). However this function is not producing a position that is relative to where my top level svg group is positioned and scaled. When I remove the translate and scale from the top level group, the position matches up.
In the older version of d3 I could call d3.mouse(this.state.svg.node()) and it would produce the exact position I clicked corrected for pan and scale. Is this available in version 6? If not, is there a clean way I can adjust for this? The new event object is coming through with a host of different position properties: pagex, offsetx, screenx, x. None of these is producing the position I clicked on. Is there a clean way to acheive this?
You could specify a container element which would factor in a zoom transform in v5 and earlier:
d3.mouse(container)
Returns the x and y coordinates of the current event relative to the specified container. The container may be an HTML or SVG container element, such as a G element or an SVG element. The coordinates are returned as a two-element array of numbers [x, y]. (source)
In d3v6 you can specify this by using the second parameter of d3.pointer:
d3.pointer(event[, target])
Returns a two-element array of numbers [x, y] representing the coordinates of the specified event relative to the specified target. event can be a MouseEvent, a PointerEvent, a Touch, or a custom event holding a UIEvent as event.sourceEvent.
...
If the target is an SVG element, the event’s coordinates are transformed using the inverse of the screen coordinate transformation matrix. If the target is an HTML element, the event’s coordinates are translated relative to the top-left corner of the target’s bounding client rectangle. (source)
So as far as I'm aware, you should be use:
d3.pointer(event,this.state.svg.node());
Instead of
d3.mouse(this.state.svg.node());
Here's a d3v6 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function(event) {
g.attr("transform", event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function(event) {
var xy = d3.pointer(event,g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
Adapting this v5 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function() {
g.attr("transform", d3.event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function() {
var xy = d3.mouse(g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

What is the d3.js v4.0 equivalent for d3.scale.category10()?

I'm trying to learn d3 with the Interactive Web Visualization book, but a lot has changed with version 4.0. One thing I really can't figure out is if there is an equivalent for d3.scale.category10() to get an easy mapping to colors. Is there something like that in the new version or do we need to use math.random and code up something ourselves?
Instead of
d3.scale.category10()
use
d3.scaleOrdinal(d3.schemeCategory10);
Create a color scale like this:
var color = d3.scaleOrdinal(d3.schemeCategory10);
use the color like this in the code same as in V3:
svg.append("rect")
.attr("x", 10)
.attr("y", 10)
.attr("width", 100)
.attr("height", 100)
.style("fill", color(3))
read here
Reference here
working code here
A straight-forward solution is to use the following color scales in version-4 of d3.js :
var colorScale_1 = d3.schemeCategory10;
var colorScale_2 = schemeCategory20;
var colorScale_3 = d3.schemeCategory20b;
var colorScale_4 = d3.schemeCategory20c;
colorScale_1, colorScale_2, colorScale_3, colorScale_4 are the arrays of different colors. So, you can use their different indices to fill the shape. For example
svg.append("rect")
.attr("x", 10)
.attr("y", 10)
.attr("width", 100)
.attr("height", 100)
.style("fill", colorScale_1[2])
For reference, take a look here: http://bl.ocks.org/emmasaunders/f4902478bcfa411c77a412c02087bed4
Hope that helps.

How to limit the text of polygons in Voronoi diagram with D3.js?

I've see the Example of D3.js-Voronoi Tessellation.But I want to put some text in each of polygons instead of a circle,Here is my js code:
var width = 600, height = 400;
var vertices = d3.range(20).map(function(d){
return [Math.random() * width, Math.random() * height]
});
var voronoi = d3.geom.voronoi();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
path = svg.append("g").selectAll("path");
svg.selectAll("info")
.data(vertices.slice(1))
.enter().append("text")
.attr("transform", function(d) {
return "translate(" + d + ")";
})
.text("someText")
.attr("shape-rendering","crispEdges")
.style("text-anchor","middle");
redraw();
function redraw(){
path = path
.data(voronoi(vertices), polygon);
path.exit().remove();
path.enter().append("path")
.attr("class", function(d, i) {return "q" + (i % 9) + "-9";})
.attr("d", polygon);
path.order();
}
function polygon(d){
return "M" + d.join("L") + "Z";
}
I have a JSFiddle for that basic example here:
my voronoi code
now, I want each of the polygons' text in the center of the polygon, and don't cross with the polygon's border. If the polygon have not enough space to contain the all text, just contain the first part of it!
Let me know if there is anything I can do to solve this issue, thank you!
PS:I'm so sorry to my English, yes, it's so poor! :)
Have a look at this example http://bl.ocks.org/mbostock/6909318 , you probably want to place the text at the polygon centroid and not the seed (point) used to determine the voronoi tessellation.
That should fix the majority of your layout issues.
Automatically scaling the text to fit is a little bit harder, if you are willing to scale and rotate the text you can use a technique similar to the following to determine the length of the line at that point:
https://mathoverflow.net/questions/116418/find-longest-segment-through-centroid-of-2d-convex-polygon
Then you need to determine the angle of the line. I have a plugin that should help with that:
http://bl.ocks.org/stephen101/7640188/3ffe0c5dbb040f785b91687640a893bae07e36c3
Lastly you need to scale and rotate the text to fit. To determine the width of the text use getBBox() on the text element:
var text = svg.append("svg:text")
.attr("x", 480)
.attr("y", 250)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("font", "300 128px Helvetica Neue")
.text("Hello, getBBox!");
var bbox = text.node().getBBox();
Then you use the angle you calculated earlier to scale and rotate your text:
text.attr("transform", "rotate(40) scale(7)")
I would love to give a complete example but this is quite a bit of work to get it right.
There are other options to achieve the same effect but none of them are simple (ie you could anneal the layout similar to the way d3 does the Sankey layout)

D3JS scaling and transition performance

I have some code to scale and translate a map in D3 but the performance is quite terrible. When zooming and panning, it's taking nearly 3 seconds for a refresh. I thought the map would look nicer including line boundaries for all the counties, but at 6MB+ I suspect this may be where the bottleneck is coming from. Is there another way I should be handling the transforms or maybe a way to optimize the map data? Is D3 really not suited to this level of detail? Very new to D3.
I'm using shape files from here, converted from DBF to Geojson using QGIS:
https://www.census.gov/cgi-bin/geo/shapefiles2010/main
<!doctype html>
<html>
<head>
<title>d3 map</title>
<script src="http://d3js.org/d3.v3.min.js">
</script>
</head>
<body>
<script>
var width = 800;
var height = 600;
var projection = d3.geo.mercator();
var path = d3.geo.path().projection (projection);
var canvas = d3.select ("body")
.append ("svg")
.attr ("width", width)
.attr ("height", height)
var zoomVar = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([height, 60 * height])
.on("zoom", onPostZoom);
var hotbox = canvas.append("g").call(zoomVar);
hotbox.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("fill", "white")
.attr("height", height);
d3.json ("cali.geojson", function (data)
{
hotbox.append("g")
.attr("id", "geometry")
.selectAll("path")
.data(data.features)
.enter()
.append("path")
.attr("d", path)
.attr("fill", "steelblue")
.on("click", onClick);
})
function onClick (d)
{
var centroid = path.centroid(d), translate = projection.translate();
projection.translate(
[translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2 ]);
zoomVar.translate(projection.translate());
hotbox.selectAll("path").transition()
.duration(700)
.attr("d", path);
}
function onPostZoom()
{
projection.translate(d3.event.translate).scale(d3.event.scale);
hotbox.selectAll("path").attr("d", path);
}
</script>
</body>
</html>
As Lars said, you should definitely simplify your data to the appropriate resolution. Choose the maximum resolution based on how far you want to zoom in. I recommend topojson -s to simplify, as you’ll also get the benefits of the smaller TopoJSON format.
The other big thing is to avoid reprojection if you’re just panning and zooming. Reprojection is a comparatively expensive trigonometric operation, and so is serializing very large path strings in SVG. You can avoid this for panning and zooming (translating and scaling) by simply setting the transform attribute on the path elements or a containing G element. See these examples:
Zoom to Bounding Box
Raster & Vector Zoom
You should also consider using projected TopoJSON (alternate example) which bakes the projection into the TopoJSON file. This makes the client is even faster: it never has to project!
The problem you're experiencing isn't really because of D3, but because of the browser. The main bottleneck is rendering all the visual elements, not computing their positions etc.
The only way to avoid this is to have less data. One way to start would be to simplify the boundaries in QGIS, using e.g. the dpsimplify plugin.

D3: How to access an attribute of a previous item

I am using D3 to plot a rectangle for each object in an array, the height of the rectangle being dependant on the 'Size' property of the object. These rectangles are stacked on top of each other. I currently set the y position by summing the 'Size' of each subsequent rect that gets plotted - but this seems wrong - and I was wondering if there was a better way to do this, such as accessing the 'y' attribute of the previous item (and how?) or another way...
This is what the essence of my code looks like. There is a link to the fiddle below.
var cumY = 0;
var blocks1 = sampleSVG.selectAll("rect")
.data(fpp)
.enter()
.append("rect")
.sort(SortBySize)
.style("stroke", "gray")
.style("opacity", blockOpacity)
.style("fill", function (d) {return d.Colour})
.attr("width", 80)
.attr("height", function (d) {return d.Size})
.attr("x", 5)
.attr("y", function (d, i) {
var thisY = cumY;
cumY += d.Size;
// perhaps I could just return something like d.Size + previousItem.GetAttribute("y") ???
return thisY;
});
http://jsfiddle.net/ninjaPixel/bvER3/
This is tricky do! You're right that keeping track of the cumulative height 'seems wrong' - it works now but it isn't very idiomatic d3 and will get pretty messy once you start trying to do something more complicated.
I would try using d3's built in stack-layout which was created solve this problem. You might want to start working off of this example and posting an updated fiddle if you get stuck. Good luck!

Resources