Related
I try to rotate text and background rect to annotate two point. but after rotation, the background position not match the text bbox.
console.clear()
var svg = d3.select('body')
.append('svg')
add_defs(svg)
var g = svg.append('g')
var data = [[0,0],[100,100]]
var nodes = g.selectAll('.circle')
.data(data)
.join('circle')
.attr('cx',d => d[0])
.attr('cy',d => d[1])
.attr('r',2)
.attr('stroke','none')
.attr('fill','red')
ann_line(svg,data[0][0],data[0][1],data[1][0],data[1][1],"hello")
adjust_view(svg)
function ann_line(svg,ax,ay,bx,by,text) {
var w = 10
var dx = bx - ax
var dy = by - ay
var l = Math.sqrt(dx*dx+dy*dy)
dx /= l
dy /= l
var a1x = ax + w*dy
var a1y = ay - w*dx
var a2x = ax + 2*w*dy
var a2y = ay - 2*w*dx
var b1x = bx + w*dy
var b1y = by - w*dx
var b2x = bx + 2*w*dy
var b2y = by - 2*w*dx
var path = ['M',a1x,a1y,'L',b1x,b1y]
var line = svg.append('path')
.attr('d',path.join(' '))
.attr('stroke','black')
.attr('marker-start', 'url(#arrhead)')
.attr('marker-end', 'url(#arrhead)');
var cx = (a1x + b1x)/2
var cy = (a1y + b1y)/2
var alpha = Math.atan(dx/dy)/Math.PI*180
const label = svg.append('text')
.text(text)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('transform',`translate(${cx},${cy}) rotate(${alpha})`)
const bb = label.node().getBBox();
svg.append('rect').lower()
.attr('stroke','none')
.style('fill', 'steelblue')
.attr('transform',`translate(${cx-bb.width/2},${cy-bb.height/2}) rotate(${alpha})`)
.attr('width', bb.width)
.attr('height', bb.height)
}
function renderTextInCenterOfLine(line, text) {
const from = parseInt(line.attr('x1'));
const to = parseInt(line.attr('x2'));
const y = parseInt(line.attr('y1'));
const svg = d3.select('svg');
const textBackground = svg.append('rect')
const textElement = svg.append('text')
.text(text)
.attr('x', (from + to) / 2)
.attr('y', y)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'central');
const box = textElement.node().getBBox();
const width = box.width + 50; //
const height = box.height + 20;
textBackground
.attr('x', (from + to - width) / 2)
.attr('y', y - height / 2)
.attr('width', width)
.attr('height', height)
.style('fill', 'white')
}
function add_defs(svg) {
var lw = 1
var w = 6
var h = 12
var m = 2
var lc = 'black'
var path = ['M',2+w,2,'L',2+w,2+h,'M',2+m,2+h/2,'L',2,2+h/4,2+w-lw,2+h/2,2,2+3*h/4,'z']
svg
.append('defs')
.append('marker')
.attr('id', 'arrhead')
.attr('viewBox', [0, 0, w+4, h+4])
.attr('refX', w+1)
.attr('refY', 2+h/2)
.attr('markerWidth', w+4)
.attr('markerHeight', h+4)
.attr('orient', 'auto-start-reverse')
.append('path')
.attr('d',path.join(' '))
.attr('stroke', lc)
.attr('stroke-width',lw)
.attr('fill',lc)
.attr('stroke-miterlimit', 10)
}
function adjust_view(svg) {
var bbox = svg.node().getBBox()//getBoundingClientRect()
svg.attr('width',600)
.attr('height',400)
.attr('viewBox',[bbox.x,bbox.y,bbox.width,bbox.height])
.style('border','2px solid red')
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
If you want to rotate the element around its centre calculate that point (x,y) and put those coordinates in the rotate method:
rotate(${alpha} ${bb.width/2} ${bb.height/2})`
Here is your code with that change:
console.clear()
var svg = d3.select('body')
.append('svg')
add_defs(svg)
var g = svg.append('g')
var data = [
[0, 0],
[100, 100]
]
var nodes = g.selectAll('.circle')
.data(data)
.join('circle')
.attr('cx', d => d[0])
.attr('cy', d => d[1])
.attr('r', 2)
.attr('stroke', 'none')
.attr('fill', 'red')
ann_line(svg, data[0][0], data[0][1], data[1][0], data[1][1], "hello")
adjust_view(svg)
function ann_line(svg, ax, ay, bx, by, text) {
var w = 10
var dx = bx - ax
var dy = by - ay
var l = Math.sqrt(dx * dx + dy * dy)
dx /= l
dy /= l
var a1x = ax + w * dy
var a1y = ay - w * dx
var a2x = ax + 2 * w * dy
var a2y = ay - 2 * w * dx
var b1x = bx + w * dy
var b1y = by - w * dx
var b2x = bx + 2 * w * dy
var b2y = by - 2 * w * dx
var path = ['M', a1x, a1y, 'L', b1x, b1y]
var line = svg.append('path')
.attr('d', path.join(' '))
.attr('stroke', 'black')
.attr('marker-start', 'url(#arrhead)')
.attr('marker-end', 'url(#arrhead)');
var cx = (a1x + b1x) / 2
var cy = (a1y + b1y) / 2
var alpha = Math.atan(dx / dy) / Math.PI * 180
const rect = svg.append('rect');
const label = svg.append('text')
.text(text)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('transform', `translate(${cx},${cy}) rotate(${alpha})`)
const bb = label.node().getBBox();
rect.attr('stroke', 'none')
.style('fill', 'steelblue')
.attr('transform', `translate(${cx-bb.width/2},${cy-bb.height/2}) rotate(${alpha} ${bb.width/2} ${bb.height/2})`)
.attr('width', bb.width)
.attr('height', bb.height)
}
function renderTextInCenterOfLine(line, text) {
const from = parseInt(line.attr('x1'));
const to = parseInt(line.attr('x2'));
const y = parseInt(line.attr('y1'));
const svg = d3.select('svg');
const textBackground = svg.append('rect')
const textElement = svg.append('text')
.text(text)
.attr('x', (from + to) / 2)
.attr('y', y)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'central');
const box = textElement.node().getBBox();
const width = box.width + 50; //
const height = box.height + 20;
textBackground
.attr('x', (from + to - width) / 2)
.attr('y', y - height / 2)
.attr('width', width)
.attr('height', height)
.style('fill', 'white')
}
function add_defs(svg) {
var lw = 1
var w = 6
var h = 12
var m = 2
var lc = 'black'
var path = ['M', 2 + w, 2, 'L', 2 + w, 2 + h, 'M', 2 + m, 2 + h / 2, 'L', 2, 2 + h / 4, 2 + w - lw, 2 + h / 2, 2, 2 + 3 * h / 4, 'z']
svg
.append('defs')
.append('marker')
.attr('id', 'arrhead')
.attr('viewBox', [0, 0, w + 4, h + 4])
.attr('refX', w + 1)
.attr('refY', 2 + h / 2)
.attr('markerWidth', w + 4)
.attr('markerHeight', h + 4)
.attr('orient', 'auto-start-reverse')
.append('path')
.attr('d', path.join(' '))
.attr('stroke', lc)
.attr('stroke-width', lw)
.attr('fill', lc)
.attr('stroke-miterlimit', 10)
}
function adjust_view(svg) {
var bbox = svg.node().getBBox() //getBoundingClientRect()
svg.attr('width', 600)
.attr('height', 400)
.attr('viewBox', [bbox.x, bbox.y, bbox.width, bbox.height])
.style('border', '2px solid red')
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
I want to draw a fixed horizontal line (or a ruler) that gives info about size/distance or zooming factor like the one in Google Maps (see here).
Here is the another example with different zoom levels and camera used is orthographic
I try to implement the same with perspective camera but I would not able do it correctly
Below is the result I am getting with perspective camera
The logic that i am using to draw the ruler is
var rect = myCanvas.getBoundingClientRect();
var canvasWidth = rect.right - rect.left;
var canvasHeight = rect.bottom - rect.top;
var Canvas2D_ctx = myCanvas.getContext("2d");
// logic to calculate the rulerwidth
var distance = getDistance(camera.position, model.center);
canvasWidth > canvasHeight && (distance *= canvasWidth / canvasHeight);
var a = 1 / 3 * distance,
l = Math.log(a) / Math.LN10,
l = Math.pow(10, Math.floor(l)),
a = Math.floor(a / l) * l;
var rulerWidth = a / h;
var text = 1E5 <= a ? a.toExponential(3) : 1E3 <= a ? a.toFixed(0) : 100 <= a ? a.toFixed(1) : 10 <= a ? a.toFixed(2) : 1 <= a ? a.toFixed(3) : .01 <= a ? a.toFixed(4) : a.toExponential(3);
Canvas2D_ctx.lineCap = "round";
Canvas2D_ctx.textBaseline = "middle";
Canvas2D_ctx.textAlign = "start";
Canvas2D_ctx.font = "12px Sans-Serif";
Canvas2D_ctx.strokeStyle = 'rgba(255, 0, 0, 1)';
Canvas2D_ctx.lineWidth = 0;
var m = canvasWidth * 0.01;
var n = canvasHeight - 50;
Canvas2D_ctx.beginPath();
Canvas2D_ctx.moveTo(m, n);
n += 12;
Canvas2D_ctx.lineTo(m, n);
m += canvasWidth * rulerWidth;
Canvas2D_ctx.lineTo(m, n);
n -= 12;
Canvas2D_ctx.lineTo(m, n);
Canvas2D_ctx.stroke();
Canvas2D_ctx.fillStyle = 'rgba(255, 0, 0, 1)';
Canvas2D_ctx.fillText(text + " ( m )", (m) /2 , n + 6)
Can any one help me ( logic to calculate the ruler Width) in fixing this issue and to render the scale meter / ruler correctly for both perspective and orthographic camera.
I'm trying to create an funnel chart like this with svg. http://i.stack.imgur.com/hUyru.jpg
My first attempt was with svg filter effects, but then I found out that svg filter effects aren't supported in IE.
The second attempt was with svg paths but I can't manage to transform the path based on the previous circle.
http://codepen.io/justpixel/pen/MwOLRQ
<path transform="translate(0 27) scale(0.9 0.6)" fill="#ED1C24" d="M240.208,110.922c-43.5-29-140.417,19.125-175.322,19.125V0c34.906,0,131.822,50.422,175.333,18.667
L240.208,110.922z"/>
Do you have any tips on how can I do this?
It's quite simple. as long as you know how to create SVG elements with JS - and you know a little bit of trigonometry.
var svgns = "http://www.w3.org/2000/svg";
// Make the graph
var radius = [88, 66, 56, 27];
var inter_circle_gap = 80;
var startX = 120;
var startY = 120;
var funnelSqueezeFactor = 0.3;
// Draw the funnels
var g = document.getElementById("funnels");
var x = startX; // centre of first circle
var numFunnels = radius.length - 1;
for (var i=0; i<numFunnels; i++)
{
nextX = x + radius[i] + inter_circle_gap + radius[i+1];
makeFunnel(g, x, nextX, startY, radius[i], radius[i+1]);
x = nextX;
}
// Draw the circles
var g = document.getElementById("circles");
var x = startX - radius[0]; // left edge of first circle
for (var i=0; i<radius.length; i++)
{
x += radius[i]; // centre X for this circle
makeCircle(g, x, startY, radius[i]);
x += radius[i] + inter_circle_gap; // step to left edge of next circle
}
// Function to make a circle
function makeCircle(g, x, y, r)
{
var circle = document.createElementNS(svgns, "circle");
circle.setAttribute("cx", x);
circle.setAttribute("cy", y);
circle.setAttribute("r", r);
g.appendChild(circle);
}
// Function to make a funnel
function makeFunnel(g, x1, x2, y, r1, r2)
{
var tangentAngle = 30 * Math.PI / 180;; // 30 degrees
startPointX = r1 * Math.sin(tangentAngle);
startPointY = r1 * Math.cos(tangentAngle);
endPointX = r2 * Math.sin(tangentAngle);
endPointY = r2 * Math.cos(tangentAngle);
ctrlPointX = (x1 + x2) / 2;
ctrlPointY = (startPointY + endPointY) * funnelSqueezeFactor / 2;
var d = 'M' + (x1 + startPointX) + ',' + (y - startPointY);
d += ' Q' + ctrlPointX + ',' + (y - ctrlPointY) + ','
+ (x2 - endPointX) + ',' + (y - endPointY);
d += ' L' + (x2 - endPointX) + ',' + (y + endPointY);
d += ' Q' + ctrlPointX + ',' + (y + ctrlPointY) + ','
+ (x1 + startPointX) + ',' + (y + startPointY);
d += "Z";
var path = document.createElementNS(svgns, "path");
path.setAttribute("d", d);
g.appendChild(path);
}
#circles circle {
fill: #27293d;
}
#funnels path {
fill: #f5d135;
}
<svg width="779px" height="306px">
<g id="funnels"></g>
<g id="circles"></g>
</svg>
I have created a small Raphael app to showcase my struggle.
I created four handles which can be moved. A 'sheet' is covering the entire screen except for the square between the 4 handles.
Whenever the handles are dragged the sheet is placed accordingly.
What ends up happening is that in certain situations, the sheet folds on itself.
It's best if you just see the fiddle. You'll get what I'm talking about
http://jsfiddle.net/8qtffq0s/
How can I avoid this?
Notice: The screen is white. The black part is the sheet, and the white part is a gap in the sheet and not the other way around.
//raphael object
var paper = Raphael(0, 0, 600, 600)
//create 4 handles
h1 = paper.circle(50, 50, 10).attr("fill","green")
h2 = paper.circle(300, 50, 10).attr("fill", "blue")
h3 = paper.circle(300, 300, 10).attr("fill", "yellow")
h4 = paper.circle(50, 300, 10).attr("fill", "red")
//create covering sheet
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", h1.attrs.cx, h1.attrs.cy,"L", h4.attrs.cx, h4.attrs.cy, h3.attrs.cx, h3.attrs.cy, h2.attrs.cx, h2.attrs.cy,'z']
sheet = paper.path(path).attr({ "fill": "black", "stroke": "white" }).toBack()
//keep starting position of each handle on dragStart
var startX,startY
function getPos(handle) {
startX= handle.attrs.cx
startY = handle.attrs.cy
}
//Redraw the sheet to match the new handle placing
function reDrawSheet() {
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", h1.attrs.cx, h1.attrs.cy, "L", h4.attrs.cx, h4.attrs.cy, h3.attrs.cx, h3.attrs.cy, h2.attrs.cx, h2.attrs.cy, 'z']
sheet.attr("path",path)
}
//enable handle dragging
h1.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h2.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h3.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h4.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
Update: I improved the function "reDrawSheet" so now it can classify the points on the strings as top left, bottom left, bottom right, and top right
This solved many of my problems, but in some cases the sheet still folds on it self.
new fiddle: http://jsfiddle.net/1kj06co4/
new code:
function reDrawSheet() {
//c stands for coordinates
c = [{ x: h1.attrs.cx, y: h1.attrs.cy }, { x: h4.attrs.cx, y: h4.attrs.cy }, { x: h3.attrs.cx, y: h3.attrs.cy }, { x: h2.attrs.cx, y: h2.attrs.cy }]
//arrange the 4 points by height
c.sort(function (a, b) {
return a.y - b.y
})
//keep top 2 points
cTop = [c[0], c[1]]
//arrange them from left to right
cTop.sort(function (a, b) {
return a.x - b.x
})
//keep bottom 2 points
cBottom = [c[2], c[3]]
//arrange them from left to right
cBottom.sort(function (a, b) {
return a.x - b.x
})
//top left most point
tl = cTop[0]
//bottom left most point
bl = cBottom[0]
//top right most point
tr = cTop[1]
//bottom right most point
br = cBottom[1]
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", tl.x,tl.y, "L", bl.x,bl.y, br.x,br.y, tr.x,tr.y, 'z']
sheet.attr("path",path)
}
To make things super clear, this is what I'm trying to avoid:
Update 2:
I was able to avoid the vertices from crossing by checking which path out of the three possible paths is the shortest and choosing it.
To do so, I added a function that checks the distance between two points
function distance(a, b) {
return Math.sqrt(Math.pow(b.x - a.x, 2) + (Math.pow(b.y - a.y, 2)))
}
And altered the code like so:
function reDrawSheet() {
//c stands for coordinates
c = [{ x: h1.attrs.cx, y: h1.attrs.cy }, { x: h4.attrs.cx, y: h4.attrs.cy }, { x: h3.attrs.cx, y: h3.attrs.cy }, { x: h2.attrs.cx, y: h2.attrs.cy }]
//d stands for distance
d=distance
//get the distance of all possible paths
d1 = d(c[0], c[1]) + d(c[1], c[2]) + d(c[2], c[3]) + d(c[3], c[0])
d2 = d(c[0], c[2]) + d(c[2], c[3]) + d(c[3], c[1]) + d(c[1], c[0])
d3 = d(c[0], c[2]) + d(c[2], c[1]) + d(c[1], c[3]) + d(c[3], c[0])
//choose the shortest distance
if (d1 <= Math.min(d2, d3)) {
tl = c[0]
bl = c[1]
br = c[2]
tr = c[3]
}
else if (d2 <= Math.min(d1, d3)) {
tl = c[0]
bl = c[2]
br = c[3]
tr = c[1]
}
else if (d3 <= Math.min(d1, d2)) {
tl = c[0]
bl = c[2]
br = c[1]
tr = c[3]
}
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", tl.x,tl.y, "L", bl.x,bl.y, br.x,br.y, tr.x,tr.y, 'z']
sheet.attr("path",path)
}
Now the line does not cross itself like the image I attached about, but the sheet "flips" so everything turns black.
You can see the path is drawn correctly to connect the for points by the white stroke, but it does not leave a gap
new fiddle: http://jsfiddle.net/1kj06co4/1/
Picture of problem:
So... the trouble is to tell the inside from the outside.
You need the following functions:
function sub(a, b) {
return { x: a.x - b.x , y: a.y - b.y };
}
function neg(a) {
return { x: -a.x , y: -a.y };
}
function cross_prod(a, b) {
// 2D vecs, so z==0.
// Therefore, x and y components are 0.
// Return the only important result, z.
return (a.x*b.y - a.y*b.x);
}
And then you need to do the following once you've found tl,tr,br, and bl:
tlr = sub(tr,tl);
tbl = sub(bl,tl);
brl = sub(bl,br);
btr = sub(tr,br);
cropTL = cross_prod( tbl, tlr );
cropTR = cross_prod(neg(tlr),neg(btr));
cropBR = cross_prod( btr, brl );
cropBL = cross_prod(neg(brl),neg(tbl));
cwTL = cropTL > 0;
cwTR = cropTR > 0;
cwBR = cropBR > 0;
cwBL = cropBL > 0;
if (cwTL) {
tmp = tr;
tr = bl;
bl = tmp;
}
if (cwTR == cwBR && cwBR == cwBL && cwTR!= cwTL) {
tmp = tr;
tr = bl;
bl = tmp;
}
My version of the fiddle is here. :) http://jsfiddle.net/1kj06co4/39/
I have a d3 network where points are connected by lines. I want to replace the lines with curved SVG paths. I have forgotten the math to calculate the control point's coordinates. Does anyone know how to do this?
For example, look at the image below:
There exist points A and B. I have them connected at present by a line L. I want to replace L with a curve, C. To do that I need to find a line that is perpendicular to the mid-point of line L, of length M (length set as a percentage of L), to be the control point of spline C. Then I need to define an SVG path to define C.
How do I do this in d3 with SVG? I've done this before in Raphael/SVG a long time ago, but the math escapes me. And I'm not sure how its done in D3.
Just to be clear for others, what we're talking about is a quadratic Bezier curve. That gives you a smooth curve between two points with one control point.
The basic method is:
Find your A-B midpoint, call it J.
Do some trig to find the point at the end of line segment M, call it K
Use the SVG Q or T path commands to draw the quadratic Bezier curve, starting from A, going to B, with the control point K. (note that this won't look exactly like your diagram, but that can be tuned by changing the length of M).
Here's a JavaScript function to return the path you'll need:
function draw_curve(Ax, Ay, Bx, By, M) {
// Find midpoint J
var Jx = Ax + (Bx - Ax) / 2
var Jy = Ay + (By - Ay) / 2
// We need a and b to find theta, and we need to know the sign of each to make sure that the orientation is correct.
var a = Bx - Ax
var asign = (a < 0 ? -1 : 1)
var b = By - Ay
var bsign = (b < 0 ? -1 : 1)
var theta = Math.atan(b / a)
// Find the point that's perpendicular to J on side
var costheta = asign * Math.cos(theta)
var sintheta = asign * Math.sin(theta)
// Find c and d
var c = M * sintheta
var d = M * costheta
// Use c and d to find Kx and Ky
var Kx = Jx - c
var Ky = Jy + d
return "M" + Ax + "," + Ay +
"Q" + Kx + "," + Ky +
" " + Bx + "," + By
}
You can see this in action at this jsfiddle or the snippet (below).
Edit: If a quadratic curve doesn't fit, you can pretty easily adapt the function to do cubic Bezier or arc segments.
var adjacencyList = {
1: [2],
2: [3],
3: [1],
};
var nodes = d3.values(adjacencyList),
links = d3.merge(nodes.map(function(source) {
return source.map(function(target) {
return {
source: source,
target: adjacencyList[target]
};
});
}));
var w = 960,
h = 500;
var M = 50;
var vis = d3.select("#svg-container").append("svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([w, h])
.linkDistance(100)
.charge(-100)
.start();
var link = vis.selectAll(".link")
.data(links)
.enter().append("svg:path")
.attr("class", "link");
console.log(link)
var node = vis.selectAll("circle.node")
.data(nodes)
.enter().append("svg:circle")
.attr("r", 5)
.call(force.drag);
force.on("tick", function() {
link.attr("d", function(d) {
return draw_curve(d.source.x, d.source.y, d.target.x, d.target.y, M);
});
node.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
});
function draw_curve(Ax, Ay, Bx, By, M) {
// side is either 1 or -1 depending on which side you want the curve to be on.
// Find midpoint J
var Jx = Ax + (Bx - Ax) / 2
var Jy = Ay + (By - Ay) / 2
// We need a and b to find theta, and we need to know the sign of each to make sure that the orientation is correct.
var a = Bx - Ax
var asign = (a < 0 ? -1 : 1)
var b = By - Ay
var bsign = (b < 0 ? -1 : 1)
var theta = Math.atan(b / a)
// Find the point that's perpendicular to J on side
var costheta = asign * Math.cos(theta)
var sintheta = asign * Math.sin(theta)
// Find c and d
var c = M * sintheta
var d = M * costheta
// Use c and d to find Kx and Ky
var Kx = Jx - c
var Ky = Jy + d
return "M" + Ax + "," + Ay +
"Q" + Kx + "," + Ky +
" " + Bx + "," + By
}
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #ccc;
fill: none
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.0.0/d3.min.js"></script>
<body>
<div id="svg-container">
</div>
</body>