I am building a user interface using Raphael JS. currently I have a .js script that draws out everything using Raphael JS 2.1 exactly as needed. However, because the drawing is driven by dynamic data it is highly likely that objects will overlap. Adding the d3.js Force Layout to the objects would cause them to scatter automatically so there is no overlap of various ux components. However I have not been able to apply the d3.js Force Layout to Raphael drawn SVG objects.
I've created a basic example using JSFiddle here. I used the d3.js collision detection example as a "template".
I've fixed up your example and posted the result at http://jsfiddle.net/gn6tZ/6/. You had a minor typo in your collide function (- y instead of - r) and when you want to update the nodes after the force layout runs you need to supply the update function with the new data.
var nodes = circleHolder.nodes();
force.on("tick", function(e){
var q = d3.geom.quadtree( nodes ),
i = 0,
n = nodes.length;
while( ++i < n ) {
q.visit(collide( nodes[i]));
}
d3.selectAll('circle')
.data(nodes)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
d3
One of the examples: Force-Directed Graph
Related
I want to use image-maps inside fancybox 3. Goal is to display mountain panoramas, where the user could point on a summit and get name and data. The usual recommendation is to use a SVG based image map for this like in this pen. Due to the size of the images the fancybox zoom functionality is important.
While fancybox will display SVGs as an image like in this pen it is not possible to use the <image> tag with an external source inside the SVG file. Even worse: SVG files used as source of an <img> tag would not show the map functionality (see this question).
I tried to replace the <img> tag in fancybox with an <object> tag using the SVG file as data attribute. This shows the image with the map functionality correctly but fancybox won't zoom it any more.
So eventually the question boils down to how I can make an <object> (or an inline SVG or an iframe) zoomable just like an image in fancybox 3.
I'm open to other solutions as well. I only want to use fancybox to keep the appearance and usage the same as other image galleries on the same page. I'd even use an old style <map>, where I would change the coords using jquery to have it responsive. I tried that, attaching the map manually in developer tools as well as programmatically in afterLoad event handler, but apparently this doesn't work in fancybox 3 either.
The areas are polygons, so using positioned div's as overlays is no solution.
Edit: I just discovered that I can replace <img> with a <canvas> in fancybox without loosing the zoom and drag functionality. So in theory it would be possible to use canvas paths and isPointInPath() methode. Unfortunately I need more than one path, which requires the Path2D object, which is not available in IE...
Since all options discussed in the question turned out to be not feasible and I found the pnpoly point in polygon algorithm, I did the whole thing on my own. I put the coordinates as percentages (in order to be size-independent) in an array of javascript objects like so:
var maps = {
alpen : [
{type:'poly',name:'Finsteraarhorn (4274m)',vertx:[56.48,56.08,56.06,56.46], verty:[28.5,28.75,40.25,40.25]},
{type:'rect',name:'Fiescherhörner (4049m)',coords:[58.08,29.5,59.26,43.5]},
{type:'poly',name:'Eiger (3970m)',vertx:[61.95,61.31,61.31,60.5,60.5], verty:[43,35.25,30.25,30.25,45.5]}
]
}; // maps
Since the pnpoly function requires the vertices for x and y separately I provide the coordinates this way already.
The Id of the map is stored in a data attribute in the source link:
<a href="/img/bilder/Alpen.jpg" data-type='image' data-Id='alpen' data-fancybox="img" data-caption="<h5>panorama of the alps from the black forest Belchen at sunset</h5>">
<img src="/_pano/bilder/Alpen.jpg">
</a>
CSS for the tooltip:
.my-tooltip {
color: #ccc;
background: rgba(30,30,30,.6);
position: absolute;
padding: 5px;
text-align: left;
border-radius: 5px;
font-size: 12px;
}
pnpoly and pnrect are provided as simple functions, the handling of that all is done in the afterShow event handler:
// PNPoly algorithm checkes whether point in polygon
function pnpoly(vertx, verty, testx, testy) {
var i, j, c = false;
var nvert = vertx.length;
for(i=0, j=nvert-1; i<nvert; j=i++) {
if (((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i])) {
c = !c;
}
}
return c;
}
// checks whether point in rectangle
function pnrect(coords,testx,testy) {
return ((testx >= coords[0]) && (testx <= coords[2]) && (testy >= coords[1]) && (testy <= coords[3]));
}
$("[data-fancybox]").fancybox({
afterShow: function( instance, slide ) {
var map = maps[$(slide.opts.\$orig).data('id')]; // Get map name from source link data-ID
if (map && map.length) { // if map present
$(".fancybox-image")
.after("<span class='my-tooltip' style='display: none'></span>") // append tooltip after image
.mousemove(function(event) { // create mousemove event handler
var offset = $(this).offset(); // get image offset, since mouse coords are global
var perX = ((event.pageX - offset.left)*100)/$(this).width(); // calculate mouse coords in image as percentages
var perY = ((event.pageY - offset.top)*100)/$(this).height();
var found = false;
var i;
for (i = 0; i < map.length; i++) { // loop over map entries
if (found = (map[i].type == 'poly') // depending on area type
?pnpoly(map[i].vertx, map[i].verty, perX, perY) // look whether coords are in polygon
:pnrect(map[i].coords, perX, perY)) // or coords are in rectangle
break; // if found stop looping
} // for (i = 0; i < map.length; i++)
if (found) {
$(".my-tooltip")
.css({bottom: 'calc(15px + '+ (100 - perY) + '%'}) // tooltip 15px above mouse coursor
.css((perX < 50) // depending on which side we are
?{right:'', left: perX + '%'} // tooltip left of mouse cursor
:{right: (100 - perX) + '%', left:''}) // or tooltip right of mouse cursor
.text(map[i].name) // set tooltip text
.show(); // show tooltip
} else {
$(".my-tooltip").hide(); // if nothing found: hide.
}
});
} else { // if (map && map.length) // if no map present
$(".fancybox-image").off('mousemove'); // remove event mousemove handler
$(".my-tooltip").remove(); // remove tooltip
} // else if (map && map.length)
} // function( instance, slide )
});
Things left to do: Find a solution for touch devices, f.e. provide a button to show all tooltips (probably rotated 90°).
As soon as the page is online I'll provide a link here to see it working...
I'm trying to generate an SVG gradient (for a stroke) with D3 (the rest of the project uses D3, so using D3 for this seemed to make sense...)
Here is the code that generates the gradient:
function generateBlindGradient(svg, color, side) {
// can't have a hash mark in the id or bad things will happen
idColor = color.replace('#', '');
side = side || 'right';
// this is a sneaky d3 way to select the element if present
// or create the element if it isn't
var defs = svg.selectAll('defs').data([0]);
defs.enter().append('svg:defs');
var id = 'gradient-' + idColor + '-' + side;
var gradient = defs.selectAll('lineargradient#'+id).data([0]);
gradient.enter().append('svg:lineargradient')
.attr('id', id);
var colors = [
{ offset : '50%', color : '#DFE2E6' },
{ offset : side === 'left' ? '100%' : '0%', color : color }
];
var stops = gradient.selectAll('stop').data(colors);
stops.enter().append('svg:stop');
stops.attr('stop-color', function(d) { return d.color; })
.attr('offset', function(d) { return d.offset; });
return id;
}
This works... almost right. It generates gradients like this:
<lineargradient id="gradient-a8d4a1-left">
<stop stop-color="#DFE2E6" offset="50%"></stop>
<stop stop-color="#a8d4a1" offset="100%"></stop>
</lineargradient>
That gradient does not work (as either a fill or a stroke)--the element it's applied to gets no stroke or fill.
If I use the web inspector to "edit the HTML" of the lineargradient element, even if I don't change anything, the gradients suddenly work — so I'm guessing there's something weird going on within Chrome's SVG parsing or d3's element generation.
I think it might be down to a confusion between lineargradient and linearGradient—d3 seems to have some issues with camelCased elements and when I have it create linearGradient elements, it doesn't select them (and I get lots and lots of copies). Also, while in Chrome's inspector, these elements show up as lineargradient; when I edit as HTML, they are linearGradient. I'm not sure what's going on here or how to fix it.
SVG is case sensitive so its linearGradient rather than lineargradient for creation.
I think Chrome has a selector bug that you can't select camel cased elements though.
The common workaround seems to be to assign a class to all your linearGradient elements and select by class rather than by tag name.
Scratching my head on this one.
We have a list of text on the left side of the page. Each item in the list has a data-id attribute that makes it easy to match up corresponding schools in our SVG map. This SVG map is a map of the US, and has school locations fed in from a CSV excel sheet and stored in "schools" for access.
circles.selectAll("circles")
.data(schools)
.enter().append("svg:a")
.attr("xlink:href", function(d) { return d.url; })
.append("svg:circle")
.attr("school", function(d, i) { return d.name; })
.attr("id", function(d, i) { return d.id; })
.attr("cx", function(d,i) { return d.longitude; })
.attr("cy", function(d,i) { return d.latitude; })
.attr("r", function(d,i) { return 6; })
.attr("i", function(d,i) { return i; })
.attr("class", "icon")
So when a user hovers over this list of text I previously mentioned, I use this function:
mapSearch = function(id) {
d3.selectAll("circle")
.filter(function(d) {
if (d.id == id) {
return show_bubble_2(d);
}
})
}
Which calls:
show_bubble_2 = function(school_data) {
var school_info = school_data,
latitude = school_info.latitude,
longitude = school_info.longitude;
bubble.css({
"left": (longitude - 75)+"px",
"top": (latitude - 67)+"px"
});
bubble.html("<h1>" + school_info.name + "</h1>" + "<p>" + school_info.city + ", " + school_info.state + "</p>")
.attr("class", function(d) { return school_info.letter; });
bubble.addClass("active");
}
This works unless we start resizing the map to fit different screen sizes, or unless we do special zoom functions on the map. Then the bubbles closer to the west coast are where they're supposed to be but the ones on the east coast are way off. In short, it's a complete nightmare and not at all scalable.
My question: How do I just append this DIV to the corresponding circle ID instead of using an absolute positioned DIV so that no matter what size the map is, the bubble will always pop up right on top of that circle.
I have tried appending inside the if (d.id == id) { } but it always returns errors and so far I haven't figured it out. I'll keep trying something along those lines because I feel like that's the way to do it. If you have a better solution or could point me in the right direction, I would really appreciate it.
Thanks, and have a good one!
You can find the position of the circle even if there is a transform applied by using Element.getBoundingClientRect.
You could use your filtered selection, get the .node() and find its bounding rect. Then by adjusting for the scroll position, you can find the values of top and left to give to your bubble.
This means that the position of the bubble is based on the actual position at which the circle appears on the page, rather than being based on its data, which would require you to take the transforms into account. Try something like this:
mapSearch = function(id) {
// get the selection for the single element that matches id
var c = d3.selectAll("circle")
.filter(function(d) {
if (d.id == id) {
return show_bubble_2(d);
}
});
// get the bounding rect for that selection's node (the actual circle element)
var bcr = c.node().getBoundingClientRect();
// calculate the top/left based on bcr and scroll position
var bubbleTop = bcr.top + document.body.scrollTop + 'px',
bubbleLeft = bcr.left + document.body.scrollLeft + 'px';
// set the top and left positions
bubble.css({
'top': bubbleTop,
'left': bubbleLeft
});
}
Of course, if you are zooming or panning and want the bubble to remain on the circle, you will need to recalculate these values inside your zoom and pan functions, but the process would be the same.
HERE is a demo using circles that are randomly placed within a g element that has a translation and scale applied. Click on an item in the list to place the bubble on the corresponding circle.
A <div> is HTML. A <circle> is SVG. You can't (easily) put HTML elements inside SVG. You'd have to use <foreignobject> elements to do that. (See this question for details.) Alternatively, you could use native SVG elements such as <tspan> instead of <div>
I'm working with RaphaelJS.
I've noticed that you can add dynamic data to the elements, for example:
to assign a value:
el.data("key",value);
to get a value:
el.data("key")
How can I copy this behaviour using JQuery or Javascript?
Hi I'm trying a wild guess here: as I understand correctly you'd like to communicate from Rapahel to jQuery.
The best I've come up so far is to keep references of both and use them accordingly as Raphael seems not to insert data into dom directly (apart from ID attribute):
$(document).ready(function () {
var paper = Raphael("div", 400, 150);
var circle = paper.circle(80, 80, 30).data('title', 'Red dot').attr({
fill: '#f00'
});
circle.node.id = 'my_circle';
$el_circle = $(circle[0]); // get DOM element out of Rapahel's object
$el_circle.on('click', function () {
// use Raphael reference:
alert("My is title: " + circle.data('title'));
});
});
http://jsfiddle.net/saxxi/Z35NV/
References.
the possibilities: raphael.js - custom attributes
the explanation: Where does Raphael.js store an element's data that is set with the element.data() method?
Combining Raphael and jQuery to achieve browser compatibility
How do I display nodes as different symbols in d3.js's force-directed library? I wanted to implement something similar to what I wrote below:
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append(function(d){return d.shape;})
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
Each node would have an encoded shape ("rect", "circle", etc.). However, I get the error:
Uncaught TypeError: Object function (d){return "circle";} has no method 'indexOf'
The other question I have related to that is this: how would I toggle between applying different attributes for each shape? Circles need an "r" attribute refined, but rects require "height" and "width". Thanks!
Use d3.svg.symbol, as in the force-directed symbols example.