D3js: finding path's bounding box (without getBBox() )? - node.js

The following code works on Chromium :
var node = window.d3.selectAll('#L1 > *:nth-child(2)');
var bbox = node.node().getBBox();
console.log(bbox) // {height: 44, width: 44, y: -13, x: 144}
but not with nodejs + jsdom:
"TypeError: Object [ PATH ] has no method 'getBBox' "
M. Bostock pointed out that JSDOM doesn't support getBBox()
What D3js replacement to use to get the bounding box of #L1 > *:nth-child(2) ?
Past efforts lead me there : getBBox() based fiddle

Path's bounding box
Digging straight into the element's path data d="..." should work. An svg line is basically a set of x,y points. Assuming absolute coordinates without translation nor big bezier curves, which is the case of my D3js-generated svg lines, I'am finding in this data the min and max values for both x and y.
To do so, I get the d="..." svg line or multilines code. For simplicity sake, I rudely removes possible relative jumps such h30 or v20 since I never saw any in my D3js output, then clean out letters (aka svg commands : M,L,H,V,C,S,Q,T,A,Z), simplify the spaces and line jumps, then split by the remaining spaces. I get a clean arrays of coordinates.
Important to note, my selector directly target a single non-translated path.
var getBBox = function(selector){
var xmin, xmax, ymin, ymax,p;
// clean up path
var t = d3.select(selector).attr("d"); // get svg line's code
console.log(t)
t = t.replace(/[a-z].*/g," ") // remove relative coords, could rather tag it for later processing to absolute!
.replace(/[\sA-Z]+/gi," ").trim().split(" "); // remove letters and simplify spaces.
console.log(t)
for(var i in t){ // set valid initial values
if(t[i].length>1){
p = t[i].split(",");
xmin = xmax = p[0]; ymin = ymax = p[1]; }
}
for(var i in t){ // update xmin,xmax,ymin,ymax
p = t[i].split(",");
if(!p[1]){ p[0]=xmin; p[1] = ymin;} // ignore relative jumps such h20 v-10
xmin = Math.min(xmin, p[0]);
xmax = Math.max(xmax, p[0]);
ymin = Math.min(ymin, p[1]);
ymax = Math.max(ymax, p[1]);
} return [[xmin,ymax],[xmax,ymin]]; // [[left, bottom], [right, top]] as for https://github.com/mbostock/d3/wiki/Geo-Paths#bounds
}
var bb = getBBox("path");
JSfiddle DEMO
Groups bounding boxes
For groups of multiple paths, you may want to traverse the svg DOM to loop upon each single path of the group in order to update xmin, ymin, xmax, ymax.
Translated elements
To handle translated elements, adapt further.
Alternatives
Other better approaches may exist. Remember to check if getBBox() and getBoundingClientRect() are available in your context, since they are native and very convenient.

The reason why getBBox/getBoundingClientRect/getClientRect does not work in NodeJS+JSDOM is that calculating these values of an SVG (or HTML) element involves massive amounts of computation.
First, all CSS code in <style> elements must be parsed (which is already not trivial). Then the CSS selectors, cascading and inheritance rules must be applied to know what size, position or line width an element has. And even after you know all style property values, you need to do some non-trivial maths to calculate the bounding boxes: definition of different SVG transform functions, compositions of these, bounding boxes of SVG primitives and Bezier curves. Browsers support all of these (they have to, in order to draw the element), but JSDOM is simply not meant for all of these.
But fortunately, canvg is a JavaScript implementation of most of SVG, which uses a <canvas> element to draw the image. It does support most of the above, and although it does not have an interface for giving you those data, fortunately it has very nice (and MIT licensed) code, so hopefully you can copy and reuse parts of it. As of now, the code is written in a single file, and it has CSS parsing, applying cascading rules, path data parsing, definitions of SVG transforms, applying transformations, and bezier curve bounding box calculation. That is, almost everything you need to calculate bounding boxes :) It does not, however, support CSS selectors, but it can reuse another library. But unfortunately, as far as I can tell, canvg is not ready for running in NodeJS, you probably need some tweaks.
There is, however canvgc, an SVG to JS compiler, which contains an older version of canvg, and it is capable of running in NodeJS. So it is easier to start with that.

Related

noUISlider - Any way to place labels within the connect segments

I have a noUiSlider with several handles to allow specifying several contiguous date periods (example = Feb to Apr, May to July, and Aug to Sept). Ideally I would like to have labels that appear centered on the connect divisions to describe what each period relates to (ex. "Current Period", "Next Period"). I was thinking I could do this by setting a centered background image on the noUi-connect divisions.
However, the noUi-connect divisions use transform (translate/scale) styling which results in my background images being scaled which I do not want.
I also thought maybe I could revise the javascript to generate an outer division around each nonUi-connect division, and I would apply the background onto the outer division instead - but I was unable to get the background from the outer division to appear.
Any other ways I could accomplish this? The only other thing I can think of it to have floating divisions defined outside of the noUiSlider object which I would need to reposition whenever I detect changes in the handle positions.
You can add an element outside of the connects and absolutely position it.
A quick version for a slider with two handles (showing the value for the first handle):
var origin = slider.querySelector('.noUi-connects');
var node = document.createElement('div');
node.style.textAlign = 'center';
node.style.position = 'absolute';
node.style.zIndex = '10';
node.style.fontSize = '10px';
origin.appendChild(node);
slider.noUiSlider.on('update', function(values, handle, unencoded, tap, positions) {
node.style.left = positions[0] + '%';
node.style.right = (100 - positions[1]) + '%';
node.innerText = values[0];
});
Just realized another approach is to set the innerHtml of the specific noUi-connect divisions to my label values. Simpler than playing with background images.
But the transform styling still affects the labels, so the end result is not better. Maybe I can load the innerHtml with an inner division that somehow ignores the transform settings but I haven't figure out how to do that yet. transform: none does not make any difference.

d3.js geo - rendering svg path

I'd like to create choropleth map of Czech Republic. Inspired by this article http://bl.ocks.org/mbostock/4060606, I have created this
http://jsfiddle.net/1duds8tz/2/
var width = 960;
var height = 500;
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height);
var offset = [width / 2, height / 2];
var projection = d3.geo.mercator().scale(6000).center([15.474, 49.822]).translate(offset);
var path = d3.geo.path().projection(projection);
queue().defer(d3.json, "..map.geojson").await(ready);
function ready(error, reg) {
var group = svg.selectAll("g").data(reg.features).enter().append("g");
group.append("path").attr("d", path).attr("fill", "none").attr("stroke", "#222");
}
When I tried to fill svg path with some color, I ended on this
http://jsfiddle.net/1duds8tz/3/
group.append("path").attr("d", path).attr("fill", "red").attr("stroke", "#222");
There are odd values in path d attribute.
My GeoJSON data must be somehow faulty but I can't figure what is wrong.
Everything looks right here: https://gist.github.com/anonymous/4e51227dd83be8c2311d
Your geoJSON is corrupted and as a result your polygons are being drawn as the interiors of an infinitely bounded polygon. That's why when you attempt to give a fill to the path, it goes beyond the extent of the screen but still displays the border just fine. I tried to reverse the winding order of your coordinates array, and that seemed to fix all of them except for "Brno-venkov", which might be the source of your problems (especially given its administrative shape).
I'd suggest going back to where you created the original GeoJSON and try to re-export it with simplification. If you want to reverse the coordinates on your GeoJSON to correct the winding order, that's pretty simple:
geodata = d3.selectAll("path").data();
for (x in geodata) {geodata[x].geometry.coordinates[0] = geodata[x].geometry.coordinates[0].reverse()}
But this won't fix the problem polygon, nor will not reversing its coordinates.
In case you are familiar with svg manipulation you can try geojson2svg. This allows you manipulate svg in standard way but you have to code a little more. In case your application requires d3 for many other purpose then d3 is best solution.
I've got exactly the same problem with Mapzen's .geojson files.
.reverse()-ing isn't good enough, if you can't make sure all your data has the same winding order.
I solved it with this one:
https://www.npmjs.com/package/geojson-rewind
You'll need to have npm & require available
Install it, and save it to your project
npm i -g geojson-rewind
Import it, to make it useable
var rewind = require('geojson-rewind');
Use it on the data, in this case:
req = rewind(req);
Tip: If you are working with static data, you can do this only once on the console, and you're good to go.

how to get text bounding box in famo.us

I am attempting to draw an SVG bezier curve that starts at the end of a text string that is in a Surface. I can set the size of the Surface to [true, true], which is supposed to make the size equal the text bounding box. But if I later try "mySurface.size[0]" in order to get the width, it returns "true"! I need to get a number for the width and height of that bounding box, in order to calculate the end point of my bezier curve! The equivalent DOM approach would just be to use the .getBBox() function.. how do I do this in Famo.us?
this is maybe because the surface hasn't rendered yet. there are a few similar questions here, one of them from me:
how can we get the size of a surface within famo.us?
you could also try deferring or using a setTimeout or Engine.nextTick() to check the size on the next loop through.
if you find an elegant solution let us know as this is a big problem in many places using famous - having to do multiple highjinks where you can't really position a scene on the initial setup - you have to let it render and then adjust...
You can use the 'getSize' function and specify 'true' to get the real size of the surface:
var realSize = surface.getSize(true);
#ljzerenHein, thanks for the hint.. unfortunately, surface.getSize(true) returns null!
#dcsan, thanks for the link. I believe you may be right, however the solution linked to ends up being much too involved for me.
After much searching, hacking, and experimenting, I've settled on the following approach:
-] use the DOM to get untransformed bounding boxes for text strings
-] format the text strings in SVG form
-] make it so the strings are invisible (set fill and stroke to none)
-] reuse the same "div" element for all the strings that I want to measure
-] once I have the untransformed bounding box, then set the famous surface size to that and then apply modifiers.
-] if I need the bounding box after all transforms have been applied, then get the total accumulated transforms for the surface and multiply that with the original untransformed bounding box
Here's the code to create the DOM element, insert SVG text, then get the bounding box:
//Do this part once, of creating a DOM element and adding it to the document
var el1 = document.createElement("div");
document.body.appendChild(el1); //only need to append once -- just set innerHTML after
//now set the SVG text string -- from this point down can be repeated for multiple
// strings without removing or re-adding the element, nor fiddling with the DOM
var text1_1_1_SVG = '<svg> <text x="0" y="0" style="font-family: Arial; font-size: 12;fill:none;stroke:none" id="svgText1">' + myFamousSurface.content + '</text> </svg>';
//note the id is inside the text element! Also the fill and stroke are null so nothing paints
el1.innerHTML = text1_1_1_SVG;
//now get the element -- this seems to be what triggers the bounding box calc
var test = document.getElementById("svgText1"); //this is ID in the text element
//get the box, take the values out of it, and display them
var rect = test.getBoundingClientRect();
var str = "";
for (i in rect) { //a trick for getting all the attributes of the object
str += i + " = " + rect[i] + " ";
}
console.log("svgText1: " + str);
FYI, all of the SVGTextElement methods seem to be callable upon gotElem.
SVGTextElement docs here: http://msdn.microsoft.com/en-us/library/ie/ff972126(v=vs.85).aspx
#seanhalle
I'm pretty sure .getSize(true) is returning null because the element has not yet been added to the DOM. Keep in mind that famo.us is synchronized with animation-frames, and updates to the DOM happen don't happen instantly. Accesssing the DOM directly (aka pinging) is strongly disadviced because you will loose the performance benefits that famo.us promises.
What I would do is create a custom view to wrap your surface inside and implement a render-method in it. In the render-method, use getSize(true) to get the size. If it returns null,
you know it has not yet been committed to the DOM.
view in action as jsfiddle
define('MyView', function (require, exports, module) {
var View = require('famous/core/View');
var Surface = require('famous/core/Surface');
function MyView() {
View.apply(this, arguments);
this.surface = new Surface();
this.add(this.surface);
}
MyView.prototype = Object.create(View.prototype);
MyView.prototype.constructor = MyView;
MyView.prototype.render = function() {
var size = this.getSize(true);
if (size) {
if (!this.hasSize) {
this.hasSize = true;
console.log('now we have a size: ' + size);
}
this.surface.setContent('Size: ' + JSON.stringify(size));
} else {
console.log('no size yet');
}
return this._node.render();
};
module.exports = MyView;
});

How to right/end align text along an textPath inside an arc using d3.js?

Here's the fiddle: http://jsfiddle.net/DevChefOwen/CZ6Dp/
var text = g.append("text")
.style("font-size",30)
.style("fill","#000")
.attr("dy",0)
.append("textPath")
.attr("xlink:href","#yyy")
.style("text-anchor","left") // using "end", the entire text disappears
.text("some text");
I've tried a number of different things to no avail. The left align is the easy part. If you did a middle, though, you see only "text" instead of "some text", implying that "some" is just hidden because it went "out of span" for the given arc.
If, however, I added:
.attr("startOffset","39%")
(as in here: http://jsfiddle.net/DevChefOwen/2H99c/)
It would look right aligned, but outside of programmatically trying to get the width/height of the text element and look for sharp changes in width/height (which seems wrong and likely error-prone), I can't seem to find a way to right align the text.
I've also tried using an SVG path (essentially a curved arc line) and the same disappearing act happens with the text when "text-anchor" is set to "left".
Thanks ahead for your time!
The question is somewhat confusing matters. The issue isn't aligning text at the end of the path -- that's easy to do with "text-anchor"="end" and "startOffset"="100%".
However, using those settings with the path created by the d3 arc function, you end up with the text cornering around the end of the inside curve and the left straight edge, to the end of the path as defined by the arc function:
http://jsfiddle.net/CZ6Dp/8/
The real issue is that the path that you want the text to be aligned along (the outside arc of the shape) is only one segment of the path that defines the shape.
(By the way, "left" and "right" are not valid values for the "text-anchor" property, and will just be ignored).
The answer by #defghi1977 gives one way to approach the problem, by figuring out the length of the path segment that you do want to use and adjusting the start offset accordingly.
Another way to approach the problem is to create a separate path (not drawn on screen) that represents only the part of the path that you want to be used for positioning text.
There are a number of possible ways to create a path that only represents the outside arc (some example code here). #defghi1977's approach of grabbing it from the existing path with regular expressions is probably the most efficent for your situation. But instead of just creating a temporary element to calculate a length, I actually have to add the new path to the DOM so it can be used as the reference path for the <textPath> element. (Which I suppose is the downside to this approach -- twice as many DOM elements!)
var path = g.append("svg:path")
.attr("d", arct)
.style("fill","#ccc")
.attr("transform", "translate("+cfg.w/2+","+cfg.h/2+")")
.each(function(d,i) {
var justArc = /(^.+?)L/;
//grab everything up to the first Line statement
var thisSelected = d3.select(this);
var arcD = justArc.exec( thisSelected.attr("d") )[1];
defs.append("path")
.attr("id", "yyy") //normally the id would be based on the data or index
.attr("d", arcD)
.attr("transform", thisSelected.attr("transform") );
//if you can avoid using transforms directly on the path element,
//you'll save yourself having to repeat them for the text paths...
});
var text = g.append("text")
.style("font-size",30)
.style("fill","#000")
.attr("dy",0)
.append("textPath")
.attr("xlink:href","#yyy")
.style("text-anchor","end")
.attr("startOffset","100%")
.text("some text");
http://jsfiddle.net/CZ6Dp/9/
Again, factoring in the extra DOM load #defghi1977's method is probably slightly preferrable, although this version has the benefit of not being dependent on browser support for getTotalLength. But as far as I know that method is fairly well implemented.
So just consider this an alternate approach for completeness' sake.
This path is constructed by 4(or 5) path segments.
So, this probrem will be solved to get first arc path length.
But I don't know how to get sub path length by using d3.js, thus I use svgdom directly.
I tried to fix your code. If this code is not what you hope, I'm sorry.
path-anchor attribute to end.
define function to get startOffset value.
var path = g.append("svg:path")
.attr("id","yyy")
.attr("d", arct)
.style("fill","#ccc")
.attr("transform", "translate("+cfg.w/2+","+cfg.h/2+")");
var text = g.append("text")
.style("font-size",30)
.style("fill","#000")
.attr("dy",0)
.append("textPath")
.attr("xlink:href","#yyy")
//.style("text-anchor","left") // using "end", the entire text disappears
.attr("text-anchor", "end")
.text("some text")
.attr("startOffset",function(){
var d = document.getElementById("yyy").getAttribute("d");
var tmp = document.createElementNS("http://www.w3.org/2000/svg" ,"path");
//get the arc segment of path
var arc = d.match(/(^.+?)L/)[1];
tmp.setAttribute("d", arc);
//return offset position
return tmp.getTotalLength();
});
I think the confusion comes from the meaning of text-anchor - it's not "relative to where on the parent will I justify" but rather "what part of me should I align to the start".
You're right to try to use startOffset to move the origin. Since the outer radius of your path is longer than the inner radius, the correct start offset is a little more than half of the path (around 53%).
Just a little more twiddling with your settings and you should have it. Here's a fiddle with my interpretation of what you're looking for.

convert kml polygons to svg paths

I have a KML file with placemarkers, each of which has polygon coordinates. I'd like to change the fill color of the polygons on the fly, depending on values I read in from a json file. My understanding is that Google caches kml files, so you can't easily change the fill color of polygons on the fly.
So I'm trying to convert my kml polygons into svg paths so that I can use Raphael to place my polygons on my Google map. Then I can change fill colors with javascript.
But how do I convert polygon coordinates to svg paths, does anyone know?
A sample set of coordinates would be:
<Polygon><outerBoundaryIs><LinearRing><coordinates>-80.098181,40.42127 -80.096479,40.421262 -80.096464,40.421409 -80.096448,40.421551 -80.096444,40.421583 -80.096434,40.421666 -80.096406,40.421931 -80.096389,40.422087 -80.096353,40.422423 -80.09583,40.426101 -80.095525,40.428234 -80.095315,40.429714 -80.095276,40.429989 -80.092585,40.428593 -80.092273,40.428431 -80.09069,40.430519 -80.090384,40.430924 -80.08989,40.430618 -80.089699,40.4305 -80.089499,40.430359 -80.088738,40.429886 -80.088418,40.429688 -80.088254,40.429585 -80.087931,40.429384 -80.087086,40.428859 -80.086867,40.428722 -80.086658,40.428592 -80.086493,40.42849 -80.08617,40.428282 -80.086177,40.428265 -80.08621,40.428188 -80.0864,40.427742 -80.086397,40.42749 -80.086394,40.427125 -80.08631,40.426427 -80.086335,40.425887 -80.086235,40.425409 -80.085776,40.425327 -80.085442,40.42527 -80.084993,40.424585 -80.085076,40.42448 -80.085542,40.423842 -80.085679,40.423125 -80.085659,40.423011 -80.085626,40.422827 -80.085191,40.421758 -80.08467,40.420859 -80.084258,40.420336 -80.083828,40.4201 -80.083078,40.420005 -80.082504,40.420072 -80.081444,40.420196 -80.080888,40.420181 -80.080775,40.420178 -80.080604,40.420173 -80.080122,40.420161 -80.079753,40.420151 -80.07947,40.420144 -80.079287,40.420139 -80.078239,40.420296 -80.077661,40.420418 -80.076213,40.420726 -80.075673,40.420766 -80.075298,40.420719 -80.075127,40.420625 -80.074909,40.420307 -80.075028,40.419779 -80.07539,40.419028 -80.07583,40.41836 -80.076065,40.418108 -80.076528,40.417616 -80.077217,40.417124 -80.077503,40.417002 -80.077725,40.416907 -80.078391,40.416622 -80.078614,40.416528 -80.078657,40.41651 -80.078693,40.416491 -80.078755,40.416457 -80.079174,40.416233 -80.079205,40.416217 -80.079292,40.416128 -80.079439,40.415977 -80.079456,40.41596 -80.079614,40.415473 -80.079604,40.415377 -80.079584,40.415179 -80.07958,40.415168 -80.079539,40.415042 -80.079499,40.414915 -80.079407,40.414785 -80.079742,40.414965 -80.08086,40.415568 -80.081458,40.415889 -80.081535,40.41593 -80.082292,40.416343 -80.08238,40.416392 -80.082597,40.41651 -80.082624,40.416525 -80.082787,40.416614 -80.083508,40.417007 -80.083934,40.417239 -80.084422,40.417505 -80.084622,40.417432 -80.084852,40.417348 -80.085329,40.417179 -80.085419,40.417141 -80.085948,40.41695 -80.086252,40.417184 -80.088463,40.418885 -80.088964,40.418631 -80.089036,40.418595 -80.089145,40.418539 -80.089173,40.418525 -80.089346,40.418438 -80.089376,40.418457 -80.089398,40.418471 -80.089964,40.418827 -80.090299,40.419055 -80.091306,40.419739 -80.091642,40.419967 -80.091803,40.420088 -80.091966,40.420043 -80.092445,40.419915 -80.092561,40.419885 -80.096792,40.418748 -80.098853,40.41815 -80.098868,40.418242 -80.09943,40.421273 -80.098181,40.42127</coordinates></LinearRing></outerBoundaryIs></Polygon>
Disclaimer: I'm a bit rusty on geodetics, but think your #1 problem here is the coordinate system - lat, lng are projection-dependent data points, whereas your screen is a flat pixel one. What you need to do is convert these to northing and easting points first.
There is a projection conversion library out there called proj.4 - use this to convert your coordinates. If needed, there is a javascript port of it that you can easily adapt for your use at https://trac.osgeo.org/proj/
Generic conversion process would go like this (LAT, LNG are actual coordinates.)
var source = new Proj4js.Proj('WGS84');
var dest = new Proj4js.Proj('GOOGLE');
var p = new Proj4js.Point( LATITUDE, LONGITUDE ); // replace with actual coords!
var pdest = Proj4js.transform(source, dest, p);
At this point, pdest.x and pdest.y will contain your SVG-compatible coordinates.
After your coordinates are converted to pixel units, simply plot them - and mind the range and units. Northing and easting coordinates will be in 800,000 range, so you will likely be applying some transformations, such as translation and scaling.
End result should look like this (being rusty, I may have messed up and flipped lat/lng around, etc.)
Here's a working jsfiddle with a conversion and plot: http://jsfiddle.net/LPzKV/1/

Resources