Trying to figure out the best way of bin packing/ distributing a bunch of known width svg images in a horizontal row, where they can be stacked on top of each other if the width of the container is too tight.
Height of container should optimally be self-adjusted, and the gravity should be the vertical center point. Created a few images to illustrate the desired solution.
Are there any JS library out there that solves this problem, d3 perhaps? It feels like a bin packing problem, but perhaps with some added complexity for order and gravity. Not interested in canvas solutions.
If container is wide enough
Too tight, stack some elements
Even tighter, stack all
For a pure D3-based SVG solution, my proposal here is using a force simulation with collision detection. The collision detection in the D3 force simulation (d3.forceCollide) is a circular one, that is, it uses the elements' radii as arguments. So, since you have square/rectangular elements, I'm using this rectangular collision detection I found.
The idea is setting the x and y positions using the simulation based on your data and the available width, with the collision based on the elements' size. Then, in the resize event, you run the simulation again with the new width.
Have in mind that, contrary to most D3 force directed charts you'll find, we don't want to show the entire simulation developing, but only the final positions. So, you'll set the simulation and stop it immediately:
const simulation = d3.forceSimulation(data)
//etc...
.stop();
Then, you do:
simulation.tick(n);
Or, in the resize handler, re-heating it:
simulation.alpha(1).tick(n);
Where n is the number of iterations you want. The more the better, but also the more the slower...
Here is a very crude example, move the blue handle on the right-hand side to squeeze the rectangles:
const svg = d3.select("svg");
let width = parseInt(svg.style("width"));
const data = d3.range(15).map(d => ({
id: d,
size: 5 + (~~(Math.random() * 30))
}));
const collisionForce = rectCollide()
.size(function(d) {
return [d.size * 1.2, d.size * 1.2]
})
const simulation = d3.forceSimulation(data)
.force("x", d3.forceX(d => (width / data.length) * d.id).strength(0.8))
.force("y", d3.forceY(d => 100 - d.size / 2).strength(0.1))
.force("collision", collisionForce.strength(1))
.stop();
simulation.tick(100);
const rect = svg.selectAll("rect")
.data(data, d => "id" + d.id);
rect.enter()
.append("rect")
.style("fill", d => d3.schemePaired[d.id % 12])
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("width", d => d.size)
.attr("height", d => d.size);
const drag = d3.drag()
.on("drag", function() {
const width = Math.max(d3.mouse(this.parentNode)[0], 70);
simulation.nodes(data)
.force("x", d3.forceX(d => (width / data.length) * d.id).strength(0.8))
.stop();
simulation.alpha(1).tick(100);
const rect = svg.selectAll("rect")
.data(data, d => "id" + d.id);
rect.attr("x", d => d.x)
.attr("y", d => d.y);
d3.select("#outer").style("width", width + "px");
});
d3.select("#inner").call(drag);
function rectCollide() {
var nodes, sizes, masses
var size = constant([0, 0])
var strength = 1
var iterations = 1
function force() {
var node, size, mass, xi, yi
var i = -1
while (++i < iterations) {
iterate()
}
function iterate() {
var j = -1
var tree = d3.quadtree(nodes, xCenter, yCenter).visitAfter(prepare)
while (++j < nodes.length) {
node = nodes[j]
size = sizes[j]
mass = masses[j]
xi = xCenter(node)
yi = yCenter(node)
tree.visit(apply)
}
}
function apply(quad, x0, y0, x1, y1) {
var data = quad.data
var xSize = (size[0] + quad.size[0]) / 2
var ySize = (size[1] + quad.size[1]) / 2
if (data) {
if (data.index <= node.index) {
return
}
var x = xi - xCenter(data)
var y = yi - yCenter(data)
var xd = Math.abs(x) - xSize
var yd = Math.abs(y) - ySize
if (xd < 0 && yd < 0) {
var l = Math.sqrt(x * x + y * y)
var m = masses[data.index] / (mass + masses[data.index])
if (Math.abs(xd) < Math.abs(yd)) {
node.vx -= (x *= xd / l * strength) * m
data.vx += x * (1 - m)
} else {
node.vy -= (y *= yd / l * strength) * m
data.vy += y * (1 - m)
}
}
}
return x0 > xi + xSize || y0 > yi + ySize ||
x1 < xi - xSize || y1 < yi - ySize
}
function prepare(quad) {
if (quad.data) {
quad.size = sizes[quad.data.index]
} else {
quad.size = [0, 0]
var i = -1
while (++i < 4) {
if (quad[i] && quad[i].size) {
quad.size[0] = Math.max(quad.size[0], quad[i].size[0])
quad.size[1] = Math.max(quad.size[1], quad[i].size[1])
}
}
}
}
}
function xCenter(d) {
return d.x + d.vx + sizes[d.index][0] / 2
}
function yCenter(d) {
return d.y + d.vy + sizes[d.index][1] / 2
}
force.initialize = function(_) {
sizes = (nodes = _).map(size)
masses = sizes.map(function(d) {
return d[0] * d[1]
})
}
force.size = function(_) {
return (arguments.length ?
(size = typeof _ === 'function' ? _ : constant(_), force) :
size)
}
force.strength = function(_) {
return (arguments.length ? (strength = +_, force) : strength)
}
force.iterations = function(_) {
return (arguments.length ? (iterations = +_, force) : iterations)
}
return force
};
function constant(_) {
return function() {
return _
}
};
svg {
width: 100%;
height: 100%;
}
#outer {
position: relative;
width: 95%;
height: 200px;
}
#inner {
position: absolute;
width: 10px;
top: 0;
bottom: 0;
right: -10px;
background: blue;
opacity: .5;
cursor: pointer;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="outer">
<svg></svg>
<div id="inner"></div>
</div>
As I said, this is a very crude code, just as an example. You can tweak the strengths and improve other parts, for fitting your needs.
What you try to achieve is a masonry layout. This is typically used when you don't know the height and number of elements. A real masonry layout will work even with variable width elements.
Here is a JSFIDDLE example (resize to see how the elements packs themselves).
A lot of codes will require some js AND still won't be real masonry, as each column will have the same width (here, or here). Though you can actually achieve this result in pure css (no js) by using a multi-column layout, alongside with media queries.
However, as this is only suitable when all elements have the same width, and it seems it's not your case, I would advise you to pick one from the list below:
https://isotope.metafizzy.co/layout-modes/masonry.html
Supports a lot of features:
Fit-width
Horizontal/vertical ordering
Gutter
Fully-responsive
Has a vertical (default) masonry mode and also horizontal mode
https://masonry.desandro.com/v3/
https://vestride.github.io/Shuffle/
https://codepen.io/jh3y/pen/mPgyqw this one is pure css, but is
trickier to use
Here is the first one in action:
I might have forgotten some, if so, please propose in comment section, and if it is indeed a real masonry (support for variable width), in accordance to what the OP ask for, I will add it to the list.
Related
EDIT: For those interested in efficiency with large datasets the accepted answer is useful and I've needed to implement it anyhow. But the more direct cause of the problem as pointed in comments was that the db wasn't sending all the edges for large queries. So this was an X Y problem.
I am using d3.forceSimulation to create a network graph in the browser. The graph works as intended for a low number of edges (see fig 1.) For a large number of edges (approc > 500), most of the edges start failing to render (see fig 2.) This is of course the undesired behaviour.
So far I have tried increasing the size of the canvas, and also adjusting the update to only run on every 20th tick. There has been no improvement in edge rendering from either of these changes.
I am willing to sacrifice performance if required (eg, lower framerate). It is important that I am able to display at least 1000 nodes on the graph. I do not know what parameters I could change to achieve this as I'm not sure what exactly is causing the problem.
The simulation setup code is copied in below. I have also included the drawEdge function below, since I used a very manual process to make the graph directed (draw triangles), and in particular the arctan function has given me issues in the past. So perhaps there is a problem there.
4
Thank you.
Simulation setup:
simulation = d3.forceSimulation()
.force("x", d3.forceX(canvasWidth/2))
.force("y", d3.forceY(canvasHeight/2))
.force("collide", d3.forceCollide(nodeRadius+1))
.force("charge", d3.forceManyBody()
.strength(-90))
.force("link", d3.forceLink()
.id(function (d) { return d.id; }))
.on("tick", queue_update);
simulation.nodes(graph.nodes);
simulation.force("link")
.links(graph.edges);
drawLink function:
function drawLink(l) {
//Setup for line
ctx.beginPath();
ctx.strokeStyle=colors[l.source.court];
//Draw a line between the nodes
ctx.moveTo(l.source.x, l.source.y);
ctx.lineTo(l.target.x, l.target.y);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "#000";
//Setup for arrow
var line_angle = Math.atan2(l.source.y - l.target.y, l.source.x - l.target.x);
var x_move = Math.cos(line_angle);
var y_move = Math.sin(line_angle);
var on_line_x = l.target.x + x_move*11;
var on_line_y = l.target.y + y_move*11;
var on_line_x_2 = l.target.x + x_move*6;
var on_line_y_2 = l.target.y + y_move*6;
ctx.moveTo(on_line_x, on_line_y);
ctx.lineTo(on_line_x - y_move, on_line_y + x_move);
ctx.lineTo(on_line_x_2, on_line_y_2);
ctx.lineTo(on_line_x + y_move, on_line_y - x_move);
ctx.lineTo(on_line_x, on_line_y);
ctx.stroke();
}
Edit: Minimal example can be found here: https://drive.google.com/file/d/19efUYOaB6D04jVg4FfjxEQbw8Mcwa1pm/view?usp=sharing
There is a way to optimise rendering functions (nodes and links) as such: render only if they are visible in the viewport and let D3 do the charge / collide calculations on all nodes
First you will have to define the width/height of the viewport and the representative rectangle:
const WIDTH = 600;
const HEIGHT = 600;
const viewport = {left: 0, top: 0, right: WIDTH, bottom: HEIGHT}
The most basic thing to do to check if a link crosses the viewport is to check if the rectangle defined by the source and target intersects with the viewport:
function intersectRect(r1, r2) {
return !(r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top);
}
function drawLink(l) {
const lineRect = {
left: Math.min(l.source.x, l.target.x),
right: Math.max(l.source.x, l.target.x),
top: Math.min(l.source.y, l.target.y),
bottom: Math.max(l.source.y, l.target.y),
}
// draw only if they intersect
if (intersectRect(lineRect, viewport)) {
//Setup for line
ctx.beginPath();
ctx.strokeStyle = "#000";
//Draw a line between the nodes
ctx.moveTo(l.source.x, l.source.y);
ctx.lineTo(l.target.x, l.target.y);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "#000";
//Setup for arrow
var line_angle = Math.atan2(l.source.y - l.target.y, l.source.x - l.target.x);
var x_move = Math.cos(line_angle);
var y_move = Math.sin(line_angle);
var on_line_x = l.target.x + x_move*11;
var on_line_y = l.target.y + y_move*11;
var on_line_x_2 = l.target.x + x_move*6;
var on_line_y_2 = l.target.y + y_move*6;
ctx.moveTo(on_line_x, on_line_y);
ctx.lineTo(on_line_x - y_move, on_line_y + x_move);
ctx.lineTo(on_line_x_2, on_line_y_2);
ctx.lineTo(on_line_x + y_move, on_line_y - x_move);
ctx.lineTo(on_line_x, on_line_y);
ctx.stroke();
}
}
the same thing can be done for nodes when rendering
function drawNode(d) {
if (d.x > 0 && d.x< WIDTH && d.y> 0 && d.y< HEIGHT){
ctx.beginPath();
ctx.fillStyle = "#666";
fill_node(d)
}
}
function fill_node(d) {
if (d.x > 0 && d.x < WIDTH && d.y > 0 && d.y < HEIGHT){
ctx.moveTo(d.x, d.y);
ctx.arc(d.x, d.y, nodeRadius, 0, 2*Math.PI);
ctx.fill();
}
}
I would like to draw directed arcs between rectangles (nodes that are represented by rectangles) in such a way that the arrow-tip always hits the edge in a graceful way. I have seen plenty of SO posts on how to do this for circles (nodes represented by circles). Quite interestingly, most d3 examples deal with circles and squares (though squares to a lesser extent).
I have an example code here. Right now my best attempt can only draw from center-point to center-point. I can shift the end point (where the arrow should be), but upon experimenting with dragging the rectangles around, the arcs don't behave as intended.
Here's what I've got.
But I need something like this.
Any ideas on how I can easily do this in d3? Is there some built-in library/function that can help with this type of thing (like with the dragging capabilities)?
A simple algorithm to solve your problem is
when a node is dragged do the following for each of its incoming/outgoing edges
let a be the node dragged and b the node reached through the outgoing/incoming edge
let lineSegment be a line segment between the centers of a and b
compute the intersection point of a and lineSegment, this is done by iterating the 4 segments that make the box and checking the intersection of each of them with lineSegment, let ia be the intersection point of one of the segments of a and lineSegment, find ib in a similar fashion
Corner cases that I have considered but haven't solved
when a box's center is inside the other box there won't be 2 segment intersections
when both intersections points are the same! (solved this one in an edit)
when your graph is a multigraph edges would render on top of each other
plunkr demo
EDIT: added the check ia === ib to avoid creating an edge from the top left corner, you can see this on the plunkr demo
$(document).ready(function() {
var graph = {
nodes: [
{ id: 'n1', x: 10, y: 10, width: 200, height: 200 },
{ id: 'n2', x: 10, y: 270, width: 200, height: 250 },
{ id: 'n3', x: 400, y: 270, width: 200, height: 300 }
],
edges: [
{ start: 'n1', stop: 'n2' },
{ start: 'n2', stop: 'n3' }
],
node: function(id) {
if(!this.nmap) {
this.nmap = { };
for(var i=0; i < this.nodes.length; i++) {
var node = this.nodes[i];
this.nmap[node.id] = node;
}
}
return this.nmap[id];
},
mid: function(id) {
var node = this.node(id);
var x = node.width / 2.0 + node.x,
y = node.height / 2.0 + node.y;
return { x: x, y: y };
}
};
var arcs = d3.select('#mysvg')
.selectAll('line')
.data(graph.edges)
.enter()
.append('line')
.attr({
'data-start': function(d) { return d.start; },
'data-stop': function(d) { return d.stop; },
x1: function(d) { return graph.mid(d.start).x; },
y1: function(d) { return graph.mid(d.start).y; },
x2: function(d) { return graph.mid(d.stop).x; },
y2: function(d) { return graph.mid(d.stop).y },
style: 'stroke:rgb(255,0,0);stroke-width:2',
'marker-end': 'url(#arrow)'
});
var g = d3.select('#mysvg')
.selectAll('g')
.data(graph.nodes)
.enter()
.append('g')
.attr({
id: function(d) { return d.id; },
transform: function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
}
});
g.append('rect')
.attr({
id: function(d) { return d.id; },
x: 0,
y: 0,
style: 'stroke:#000000; fill:none;',
width: function(d) { return d.width; },
height: function(d) { return d.height; },
'pointer-events': 'visible'
});
function Point(x, y) {
if (!(this instanceof Point)) {
return new Point(x, y)
}
this.x = x
this.y = y
}
Point.add = function (a, b) {
return Point(a.x + b.x, a.y + b.y)
}
Point.sub = function (a, b) {
return Point(a.x - b.x, a.y - b.y)
}
Point.cross = function (a, b) {
return a.x * b.y - a.y * b.x;
}
Point.scale = function (a, k) {
return Point(a.x * k, a.y * k)
}
Point.unit = function (a) {
return Point.scale(a, 1 / Point.norm(a))
}
Point.norm = function (a) {
return Math.sqrt(a.x * a.x + a.y * a.y)
}
Point.neg = function (a) {
return Point(-a.x, -a.y)
}
function pointInSegment(s, p) {
var a = s[0]
var b = s[1]
return Math.abs(Point.cross(Point.sub(p, a), Point.sub(b, a))) < 1e-6 &&
Math.min(a.x, b.x) <= p.x && p.x <= Math.max(a.x, b.x) &&
Math.min(a.y, b.y) <= p.y && p.y <= Math.max(a.y, b.y)
}
function lineLineIntersection(s1, s2) {
var a = s1[0]
var b = s1[1]
var c = s2[0]
var d = s2[1]
var v1 = Point.sub(b, a)
var v2 = Point.sub(d, c)
//if (Math.abs(Point.cross(v1, v2)) < 1e-6) {
// // collinear
// return null
//}
var kNum = Point.cross(
Point.sub(c, a),
Point.sub(d, c)
)
var kDen = Point.cross(
Point.sub(b, a),
Point.sub(d, c)
)
var ip = Point.add(
a,
Point.scale(
Point.sub(b, a),
Math.abs(kNum / kDen)
)
)
return ip
}
function segmentSegmentIntersection(s1, s2) {
var ip = lineLineIntersection(s1, s2)
if (ip && pointInSegment(s1, ip) && pointInSegment(s2, ip)) {
return ip
}
}
function boxSegmentIntersection(box, lineSegment) {
var data = box.data()[0]
var topLeft = Point(data.x, data.y)
var topRight = Point(data.x + data.width, data.y)
var botLeft = Point(data.x, data.y + data.height)
var botRight = Point(data.x + data.width, data.y + data.height)
var boxSegments = [
// top
[topLeft, topRight],
// bot
[botLeft, botRight],
// left
[topLeft, botLeft],
// right
[topRight, botRight]
]
var ip
for (var i = 0; !ip && i < 4; i += 1) {
ip = segmentSegmentIntersection(boxSegments[i], lineSegment)
}
return ip
}
function boxCenter(a) {
var data = a.data()[0]
return Point(
data.x + data.width / 2,
data.y + data.height / 2
)
}
function buildSegmentThroughCenters(a, b) {
return [boxCenter(a), boxCenter(b)]
}
// should return {x1, y1, x2, y2}
function getIntersection(a, b) {
var segment = buildSegmentThroughCenters(a, b)
console.log(segment[0], segment[1])
var ia = boxSegmentIntersection(a, segment)
var ib = boxSegmentIntersection(b, segment)
if (ia && ib) {
// problem: the arrows are drawn after the intersection with the box
// solution: move the arrow toward the other end
var unitV = Point.unit(Point.sub(ib, ia))
// k = the width of the marker
var k = 18
ib = Point.sub(ib, Point.scale(unitV, k))
return {
x1: ia.x,
y1: ia.y,
x2: ib.x,
y2: ib.y
}
}
}
var drag = d3.behavior.drag()
.origin(function(d) {
return d;
})
.on('dragstart', function(e) {
d3.event.sourceEvent.stopPropagation();
})
.on('drag', function(e) {
e.x = d3.event.x;
e.y = d3.event.y;
var id = 'g#' + e.id
var target = d3.select(id)
target.data().x = e.x
target.data().y = e.y
target.attr({
transform: 'translate(' + e.x + ',' + e.y + ')'
});
d3.selectAll('line[data-start=' + e.id + ']')
.each(function (d) {
var line = d3.select(this)
var other = d3.select('g#' + line.attr('data-stop'))
var intersection = getIntersection(target, other)
intersection && line.attr(intersection)
})
d3.selectAll('line[data-stop=' + e.id + ']')
.each(function (d) {
var line = d3.select(this)
var other = d3.select('g#' + line.attr('data-start'))
var intersection = getIntersection(other, target)
intersection && line.attr(intersection)
})
})
.on('dragend', function(e) {
});
g.call(drag);
})
svg#mysvg { border: 1px solid black;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="mysvg" width="800" height="800">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refx="0" refy="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#f00" />
</marker>
</defs>
</svg>
Here's the result: https://jsfiddle.net/he0f4u23/2/
For source arrows I just filled rectangles with white to paint the arrow.
For target it is a little bit more trickier than you think. You have to calculate source and target rectangles positions and draw your arrow accordingly.
I've made a tarmid function with addition to you mid function. Your mid function calculates the arrows source point which is fine. But for target point I used the tarmid function which is:
tarmid: function(d) {
var startnode = this.node(d.start);
var endnode = this.node(d.stop);
if(startnode.x == endnode.x && startnode.y <= endnode.y){
var x = endnode.width / 2.0 + endnode.x,
y = endnode.y -17;
}else if(startnode.x < endnode.x && startnode.y <= endnode.y){
var x = endnode.x-17,
y = endnode.y + startnode.height / 2.0;
}
return { x: x, y: y };
}
see how I calculated the target point according to the rectangle placement.
Also notice that these two are not the only cases for all rectangle placement and you must update your function accordingly so I'm leaving the rest to you.
I'm doing some tests with d3.js regarding zooming. At the moment, I have successfully implemented geometric zoom in my test, but it has a drawback: the elements under the zoomed g are being scaled. As I understood it, this could be solved by using semantic zooming.
The problem is that I need scale in my test, as I'm syncing it with a jQuery.UI slider value.
On the other hand, I would like the text elements being resized to maintain their size after a zoom operation.
I have an example of my current attempt here.
I'm having trouble changing my code to fit this purpose. Can any one share some insight/ideas?
For your solution I have merged 2 examples:
Semantic Zooming
Programmatic Zooming
Code snippets:
function zoom() {
text.attr("transform", transform);
var scale = zoombehavior.scale();
//to make the scale rounded to 2 decimal digits
scale = Math.round(scale * 100) / 100;
//setting the slider to the new value
$("#slider").slider( "option", "value", scale );
//setting the slider text to the new value
$("#scale").val(scale);
}
//note here we are not handling the scale as its Semantic Zoom
function transform(d) {
//translate string
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
}
function interpolateZoom(translate, scale) {
zoombehavior
.scale(scale)//we are setting this zoom only for detecting the scale for slider..we are not zoooming using scale.
.translate(translate);
zoom();
}
var slider = $(function() {
$("#slider").slider({
value: zoombehavior.scaleExtent()[0],//setting the value
min: zoombehavior.scaleExtent()[0],//setting the min value
max: zoombehavior.scaleExtent()[1],//settinng the ax value
step: 0.01,
slide: function(event, ui) {
var newValue = ui.value;
var center = [centerX, centerY],
extent = zoombehavior.scaleExtent(),
translate = zoombehavior.translate(),
l = [],
view = {
x: translate[0],
y: translate[1],
k: zoombehavior.scale()
};
//translate w.r.t the center
translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
view.k = newValue;//the scale as per the slider
//the translate after the scale(so we are multiplying the translate)
l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
view.x += center[0] - l[0];
view.y += center[1] - l[1];
interpolateZoom([view.x, view.y], view.k);
}
});
});
I am zooming w.r.t. 250,250 which is the center of the clip circle.
Working code here (have added necessary comments)
Hope this helps!
To do what you want, you need to refactor the code first a little bit. With d3, it is good practice to use data() to append items to a selection, rather than using for loops.
So this :
for(i=0; i<7; i++){
pointsGroup.append("text")
.attr("x", function(){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randx = Math.random();
return Math.floor(plusOrMinus*randx*75)+centerx;
})
.attr("y", function(){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randy = Math.random();
return Math.floor(plusOrMinus*randy*75)+centery;
})
.html("star")
.attr("class", "point material-icons")
.on("click", function(){console.log("click!");});
}
Becomes this
var arr = [];
for(i=0; i<7; i++){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randx = Math.random();
var x = Math.floor(plusOrMinus*randx*75)+centerx;
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randy = Math.random();
var y = Math.floor(plusOrMinus*randy*75)+centery;
arr.push({"x":x,"y":y});
}
pointsGroup.selectAll("text")
.data(arr)
.enter()
.append("text")
.attr("x", function(d,i){
return d.x;// This corresponds to arr[i].x
})
.attr("y", function(d,i){
return d.y;// This corresponds to arr[i].y
})
.html("star")
.attr("class", "point material-icons")
.on("click", function(){console.log("click!");});
This way, you can access individual coordinates using for instance.attr("x",function(d,i){ //d.x is equal to arr.x, i is item index in the selection});
Then, to achieve your goal, rather than changing the scale of your items, you should use a linear scale to change each star position.
First, add linear scales to your fiddle and apply them to the zoom:
var scalex = d3.scale.linear();
var scaley = d3.scale.linear();
var zoom = d3.behavior.zoom().x(scalex).y(scaley).scaleExtent([1, 5]).on('zoom', onZoom);
And finally on zoom event apply the scale to each star's x and y
function onZoom(){
d3.selectAll("text")
.attr("x",function(d,i){
return scalex(d.x);
})
.attr("y",function(d,i){
return scaley(d.y);
});
}
At this point, zoom will work without the slider. To add the slider, simply change manually the zoom behavior scale value during onSlide event, then call onZoom.
function onSlide(scale){
var sc = $("#slider").slider("value");
zoom.scale(sc);
onZoom();
}
Note: I used this config for the slider:
var slider = $(function() {
$( "#slider" ).slider({
value: 1,
min: 1,
max: 5,
step: 0.1,
slide: function(event, ui){
onSlide(5/ui.value);
}
});
});
Please note that at this point the zoom from the ui is performed relative to (0,0) at this point, and not your "circle" window center. To fix it I simplified the following function from the programmatic example, that computes valid translate and scale to feed to the zoom behavior.
// To handle center zooming
var width = 500;
var height = 600;
function zoomClick(sliderValue) {
var center = [width / 2, height / 2],
extent = zoom.scaleExtent(),
translate = zoom.translate();
var view = {
x: zoom.translate()[0],
y: zoom.translate()[1],
k: zoom.scale()
};
var target_zoom = sliderValue;
if (target_zoom < extent[0] || target_zoom > extent[1]) {
return false;
}
var translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
view.k = target_zoom;
var l = [];
l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
view.x += center[0] - l[0];
view.y += center[1] - l[1];
// [view.x view.y] is valid translate
// view.k is valid scale
// Then, simply feed them to the zoom behavior
zoom
.scale(view.k)
.translate([view.x, view.y]);
// and call onZoom to update points position
onZoom();
}
then just change onSlide to use this new function every time slider moves
function onSlide(scale){
var sc = $("#slider").slider("value");
zoomClick(sc);
}
Full snippet
function onZoom(){
d3.selectAll("text")
.attr("x",function(d,i){
return scalex(d.x);
})
.attr("y",function(d,i){
return scaley(d.y);
});
}
function onSlide(scale){
var sc = $("#slider").slider("value");
zoomClick(sc);
}
var scalex = d3.scale.linear();
var scaley = d3.scale.linear();
var zoom = d3.behavior.zoom().x(scalex).y(scaley).scaleExtent([1, 5]).on('zoom', onZoom);
var svg = d3.select("body").append("svg")
.attr("height", "500px")
.attr("width", "500px")
.call(zoom)
.on("mousedown.zoom", null)
.on("touchstart.zoom", null)
.on("touchmove.zoom", null)
.on("touchend.zoom", null);
var centerx = 250,
centery = 250;
var circleGroup = svg.append("g")
.attr("id", "circleGroup");
var circle = circleGroup.append("circle")
.attr("cx", "50%")
.attr("cy", "50%")
.attr("r", 150)
.attr("class", "circle");
var pointsParent = svg.append("g").attr("clip-path", "url(#clip)").attr("id", "pointsParent");
var pointsGroup = pointsParent.append("g")
.attr("id", "pointsGroup");
var arr = [];
for(i=0; i<7; i++){
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randx = Math.random();
var x = Math.floor(plusOrMinus*randx*75)+centerx;
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
var randy = Math.random();
var y = Math.floor(plusOrMinus*randy*75)+centery;
arr.push({"x":x,"y":y});
}
pointsGroup.selectAll("text")
.data(arr)
.enter()
.append("text")
.attr("x", function(d,i){
return d.x;// This corresponds to arr[i].x
})
.attr("y", function(d,i){
return d.y;// This corresponds to arr[i].y
})
.html("star")
.attr("class", "point material-icons")
.on("click", function(){console.log("click!");});
zoom(pointsGroup);
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:circle")
.attr("id", "clip-circ")
.attr("cx", centerx)
.attr("cy", centery)
.attr("r", 149);
var slider = $(function() {
$( "#slider" ).slider({
value: 1,
min: 1,
max: 5,
step: 0.1,
slide: function(event, ui){
onSlide(5/ui.value);
}
});
});
// To handle center zooming
var width = 500;
var height = 600;
function zoomClick(sliderValue) {
var target_zoom = 1,
center = [width / 2, height / 2],
extent = zoom.scaleExtent(),
translate = zoom.translate();
var view = {x: zoom.translate()[0], y: zoom.translate()[1], k: zoom.scale()};
target_zoom = sliderValue;
if (target_zoom < extent[0] || target_zoom > extent[1]) { return false; }
var translate0 = [];
translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
view.k = target_zoom;
var l = [];
l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];
view.x += center[0] - l[0];
view.y += center[1] - l[1];
zoom
.scale(view.k)
.translate([view.x, view.y]);
onZoom();
}
body {
font: 10px sans-serif;
}
text {
font: 10px sans-serif;
}
.circle{
stroke: black;
stroke-width: 2px;
fill: white;
}
.point{
fill: goldenrod;
cursor: pointer;
}
.blip{
fill: black;
}
#slider{
width: 200px;
margin: auto;
}
<!DOCTYPE html>
<html>
<head>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
<link href="https://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css" />
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<meta charset=utf-8 />
<title>d3.JS slider zoom</title>
</head>
<body>
<div id="slider"></div>
</body>
</html>
I am working on tree layout of D3.js. it's simple tree layout but i have more than 8 or 9 childs per each node.
So I need to split childrens in rows if i set 3 child per row than if node have 7 child than there will be 3 rows under node with 3,3,1 child just like in image.
I tried this solution But it's just change x, y positions for two columns only ans also when child have grand-child than node overlap each other.
I am working with depth now..If any one have solution than most welcome
I already solved it but i forget to post answer
here after the tree manage it's own layout if you want to split them by row
var tree = d3.layout.tree()
.separation(function(a, b) {
return a.parent === b.parent ? 1 : 0.8;
})
.nodeSize([200,200]);
var _nodesData = tree.nodes(rootObject);
// _nodesData.forEach(function(d) { d.y = d.depth * 220;});
_nodesData.forEach(function (d) {
d.y = d.depth * 220;
if (d.children) {
d.children.forEach(function (d, i) {
d.x = d.parent.x - (d.parent.children.length-1)*200/30
+ (d.parent.children.indexOf(d))*200/10 - i % 10 * 100 ;
});
}
});
here 200 is space between each child and 10 is number of child per each row
and obviously this make your next level child override issue for that you can save last max Y and max X on each level and use for starting point of next level just like next code
var tree1 = d3.layout.tree()
.separation(function(a, b) {
return a.parent === b.parent ? 1.5 : 1.5;
})
.nodeSize([150,200]);
var _nodesData1 = tree1.nodes(_newRecordData[_x]);
_nodesData1.forEach(function (d) {
var _midpoint = d.x;
var _newRowPoint = d.x - ( 170 * 3.5)
var _countInRow = 7;
var _k = 0;
var previousTextLong = false;
var previousDirection = 1;
// if the node has too many children, go in and fix their positions to two columns.
if (d.children && d.children.length > 8) {
var _previousX = _newRowPoint;
var _previousY = d.children[0].y;
d.children.forEach(function (d, i) {
d.x = _previousX;
var s = 0;
if(previousTextLong){
s = 20*previousDirection;
}
d.y = _previousY + s;
if(d.name && d.name.length > 25){
previousTextLong = true;
previousDirection = previousDirection * -1;
}else{
previousTextLong = false;
}
_k++;
if(_k == 8){
_k = 0;
_previousX = _newRowPoint;
_previousY = _previousY + 200;
}else{
_previousX = _previousX + 170;
}
});
}
});
I think this give more customization for positions... I use multiple tree layout in one diagram and manage them after node positions are made ...
I am building prototype tool to draw simple diagrams.
I need to draw an arrow between two boxes, the problem is i have to find edges of two boxes so that the arrow line does not intersect with the box.
This is the drawing that visualize my problem:
How to find x1,y1 and x2,y2 ?
-- UPDATE --
After 2 days finding solution, this is example & function that i use:
var box1 = { x:1,y:10,w:30,h:30 };
var box2 = { x:100,y:110,w:30,h:30 };
var edge1 = findBoxEdge(box1,box2,1,0);
var edge2 = findBoxEdge(box1,box2,2,0);
function findBoxEdge(box1,box2,box,distant) {
var c1 = box1.x + box1.w/2;
var d1 = box1.y + box1.h/2;
var c2 = box2.x + box2.w/2;
var d2 = box2.y + box2.h/2;
var w,h,delta_x,delta_y,s,c,e,ox,oy,d;
if (box == 1) {
w = box1.w/2;
h = box1.h/2;
} else {
w = box2.w/2;
h = box2.h/2;
}
if (box == 1) {
delta_x = c2-c1;
delta_y = d2-d1;
} else {
delta_x = c1-c2;
delta_y = d1-d2;
}
w+=5;
h+=5;
//intersection is on the top or bottom
if (w*Math.abs(delta_y) > h * Math.abs(delta_x)) {
if (delta_y > 0) {
s = [h*delta_x/delta_y,h];
c = "top";
}
else {
s = [-1*h*delta_x/delta_y,-1*h];
c = "bottom";
}
}
else {
//intersection is on the left or right
if (delta_x > 0) {
s = [w,w*delta_y/delta_x];
c = "right";
}
else {
s = [-1*w,-1*delta_y/delta_x];
c = "left";
}
}
if (typeof(distant) != "undefined") {
//for 2 paralel distant of 2e
e = distant;
if (delta_y == 0) ox = 0;
else ox = e*Math.sqrt(1+Math.pow(delta_x/delta_y,2))
if (delta_x == 0) oy = 0;
else oy = e*Math.sqrt(1+Math.pow(delta_y/delta_x,2))
if (delta_y != 0 && Math.abs(ox + h * (delta_x/delta_y)) <= w) {
d = [sgn(delta_y)*(ox + h * (delta_x/delta_y)),sgn(delta_y)*h];
}
else if (Math.abs(-1*oy + (w * delta_y/delta_x)) <= h) {
d = [sgn(delta_x)*w,sgn(delta_x)*(-1*oy + w * (delta_y/delta_x))];
}
if (delta_y != 0 && Math.abs(-1*ox+(h * (delta_x/delta_y))) <= w) {
d = [sgn(delta_y)*(-1*ox + h * (delta_x/delta_y)),sgn(delta_y)*h];
}
else if (Math.abs(oy + (w * delta_y/delta_x)) <= h) {
d = [sgn(delta_x)*w,sgn(delta_x)*(oy + w * (delta_y/delta_x))];
}
if (box == 1) {
return [Math.round(c1 +d[0]),Math.round(d1 +d[1]),c];
} else {
return [Math.round(c2 +d[0]),Math.round(d2 +d[1]),c];
}
} else {
if (box == 1) {
return [Math.round(c1 +s[0]),Math.round(d1 +s[1]),c];
} else {
return [Math.round(c2 +s[0]),Math.round(d2 +s[1]),c];
}
}
tl;dr -> Look at the jsbin code-example
It is our goal to draw a line from the edges of two Rectangles A & B that would be drawn through their centers.
Therefore we'll have to determine where the line pierces through the edge of a Rect.
We can assume that our Rect is an object containing x and y as offset from the upper left edge and width and height as dimension offset.
This can be done by the following code. The Method you should look at closely is pointOnEdge.
// starting with Point and Rectangle Types, as they ease calculation
var Point = function(x, y) {
return { x: x, y: y };
};
var Rect = function(x, y, w, h) {
return { x: x, y: y, width: w, height: h };
};
var isLeftOf = function(pt1, pt2) { return pt1.x < pt2.x; };
var isAbove = function(pt1, pt2) { return pt1.y < pt2.y; };
var centerOf = function(rect) {
return Point(
rect.x + rect.width / 2,
rect.y + rect.height / 2
);
};
var gradient = function(pt1, pt2) {
return (pt2.y - pt1.y) / (pt2.x - pt1.x);
};
var aspectRatio = function(rect) { return rect.height / rect.width; };
// now, this is where the fun takes place
var pointOnEdge = function(fromRect, toRect) {
var centerA = centerOf(fromRect),
centerB = centerOf(toRect),
// calculate the gradient from rectA to rectB
gradA2B = gradient(centerA, centerB),
// grab the aspectRatio of rectA
// as we want any dimensions to work with the script
aspectA = aspectRatio(fromRect),
// grab the half values, as they are used for the additional point
h05 = fromRect.width / 2,
w05 = fromRect.height / 2,
// the norm is the normalized gradient honoring the aspect Ratio of rectA
normA2B = Math.abs(gradA2B / aspectA),
// the additional point
add = Point(
// when the rectA is left of rectB we move right, else left
(isLeftOf(centerA, centerB) ? 1 : -1) * h05,
// when the rectA is below
(isAbove(centerA, centerB) ? 1 : -1) * w05
);
// norm values are absolute, thus we can compare whether they are
// greater or less than 1
if (normA2B < 1) {
// when they are less then 1 multiply the y component with the norm
add.y *= normA2B;
} else {
// otherwise divide the x component by the norm
add.x /= normA2B;
}
// this way we will stay on the edge with at least one component of the result
// while the other component is shifted towards the center
return Point(centerA.x + add.x, centerA.y + add.y);
};
I wrote a jsbin, you can use to test with some boxes (lower part, in the ready method):
You might want to take a look at a little Geometry helper I wrote some time ago on top of prototype.js
I really hope, that this helps you with your problem ;)
To draw a line between those boxes, you'd first have to define where you want the line to be.
Apparently you want to draw the lines/arrows from the right edge of Rect A to the left edge of
Rect B, somewhat like this:
Assuming your know the origin (upper left Point as { x, y } of a Rect) and its Size (width and height), you first want to determine the position of the center of the edges:
var rectA, rectB; // I assume you have those data
var rectARightEdgeCenter = {
// x is simply the origin's x plus the width
x: rectA.origin.x + rectA.size.width,
// for y you need to add only half the height to origin.y
y: rectA.origin.y + rectA.size.height / 2.0
}
var rectBLeftEdgeCenter = {
// x will be simply the origin's x
x: rectB.origin.x,
// y is half the height added to the origin's y, just as before
y: rectB.origin.y + rectB.size.height / 2.0
}
The more interesting question would be how to determine, from which edge to which other edge you might want to draw the lines in a more dynamic scenario.
If your boxes just pile up from left to right the given solution will fit,
but you might want to check for minimum distances of the edges, to determine a possible best arrow.