How to create a funnel chart with svg - svg

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>

Related

Rotated rectangle not match text bbox

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>

Draw all voxels that pass through a 3D line in 3D voxel space

I want to draw a 3D voxelized line, that is, to find all the voxels which a line passes. 3D bresenham always skips some voxels. As shown in the figure, the voxels generated by 3D bresenham cannot completely contain the line between the start voxel and target voxel.
The algorithm in this link: Algorithm for drawing a 4-connected line can solve my problem on a 2D plane, but I failed to improve it to 3D.
The method in Pierre Baret's link can solve my problem. When the line passes only the vertices of a certain voxel, whether to visit the current voxel is a very vague question, so I made a little changes to the method. When two or more values in tMaxX, tMaxY, and tMaxZ are equal, the voxels generated by the method in the paper are as shown in a. I made a little change to generate the result in b. A more normal condition is shown in c, which compares lines generated by 3D bresenham and this method respectively.
The code implemented by c++:
void line3D(int endX, int endY, int endZ, int startX, int startY, int startZ, void draw){
int x1 = endX, y1 = endY, z1 = endZ, x0 = startX, y0 = startY, z0 = startZ;
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
int dz = abs(z1 - z0);
int stepX = x0 < x1 ? 1 : -1;
int stepY = y0 < y1 ? 1 : -1;
int stepZ = z0 < z1 ? 1 : -1;
double hypotenuse = sqrt(pow(dx, 2) + pow(dy, 2) + pow(dz, 2));
double tMaxX = hypotenuse*0.5 / dx;
double tMaxY = hypotenuse*0.5 / dy;
double tMaxZ = hypotenuse*0.5 / dz;
double tDeltaX = hypotenuse / dx;
double tDeltaY = hypotenuse / dy;
double tDeltaZ = hypotenuse / dz;
while (x0 != x1 || y0 != y1 || z0 != z1){
if (tMaxX < tMaxY) {
if (tMaxX < tMaxZ) {
x0 = x0 + stepX;
tMaxX = tMaxX + tDeltaX;
}
else if (tMaxX > tMaxZ){
z0 = z0 + stepZ;
tMaxZ = tMaxZ + tDeltaZ;
}
else{
x0 = x0 + stepX;
tMaxX = tMaxX + tDeltaX;
z0 = z0 + stepZ;
tMaxZ = tMaxZ + tDeltaZ;
}
}
else if (tMaxX > tMaxY){
if (tMaxY < tMaxZ) {
y0 = y0 + stepY;
tMaxY = tMaxY + tDeltaY;
}
else if (tMaxY > tMaxZ){
z0 = z0 + stepZ;
tMaxZ = tMaxZ + tDeltaZ;
}
else{
y0 = y0 + stepY;
tMaxY = tMaxY + tDeltaY;
z0 = z0 + stepZ;
tMaxZ = tMaxZ + tDeltaZ;
}
}
else{
if (tMaxY < tMaxZ) {
y0 = y0 + stepY;
tMaxY = tMaxY + tDeltaY;
x0 = x0 + stepX;
tMaxX = tMaxX + tDeltaX;
}
else if (tMaxY > tMaxZ){
z0 = z0 + stepZ;
tMaxZ = tMaxZ + tDeltaZ;
}
else{
x0 = x0 + stepX;
tMaxX = tMaxX + tDeltaX;
y0 = y0 + stepY;
tMaxY = tMaxY + tDeltaY;
z0 = z0 + stepZ;
tMaxZ = tMaxZ + tDeltaZ;
}
}
draw(x0, y0, z0);
}
}

Pixelate image in node js

What I am doing now is, I am getting all pixels with var getPixels = require("get-pixels"), then I am looping over the array of pixels with this code:
var pixelation = 10;
var imageData = ctx.getImageData(0, 0, img.width, img.height);
var data = imageData.data;
for(var y = 0; y < sourceHeight; y += pixelation) {
for(var x = 0; x < sourceWidth; x += pixelation) {
var red = data[((sourceWidth * y) + x) * 4];
var green = data[((sourceWidth * y) + x) * 4 + 1];
var blue = data[((sourceWidth * y) + x) * 4 + 2];
//Assign
for(var n = 0; n < pixelation; n++) {
for(var m = 0; m < pixelation; m++) {
if(x + m < sourceWidth) {
data[((sourceWidth * (y + n)) + (x + m)) * 4] = red;
data[((sourceWidth * (y + n)) + (x + m)) * 4 + 1] = green;
data[((sourceWidth * (y + n)) + (x + m)) * 4 + 2] = blue;
}
}
}
}
}
The problem with this method is that the result image is too sharp.
What I am looking for is something, similar to this one which has been done with ImageMagick -sale
The command which I've used for the second one is
convert -normalize -scale 10% -scale 1000% base.jpg base2.jpg
the problem with this method is that I don't know how to specify the actual pixel size.
So is it possible to get the second result with that for loop. Or is better to use imagemagick -scale but if some one can help with the math, so I can have actual pixel size would be great.
Not sure what maths you are struggling with, but if we start with a 600x600 image like this:
Then, if you want the final image to have just 5 blocky pixels across and 5 blocky pixels down the page, you can scale it down to 5x5 and then scale it back up to its original size:
convert start.png -scale 5x5 -scale 600x600 result.png
Or, if you want to go to 10x10 blocky pixels:
convert start.png -scale 10x10 -scale 600x600 result2.png
The way you've written this, the image is processed into pixels of size nxn where n is specified by your pixelation variable. Increasing pixelation will provide the desired "coarseness".

d3.js: How to convert edges from lines to curved paths in a network visualization by drawing a quadratic Bezier curve?

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>

Cant able to drag set of elements

I am creating two elements (1. arrow shape and 2. dotted line) using path (Raphael and SVG) and I want to drag these two together but I am only able to drag it independently. Here is my code for this:
gaugeSvg = Raphael("gauge");
$(document).ready( function () {
redraw();
});
function redraw() {
//Add a Arrow and line
var rect = gaugeSvg.path('M 0 0 L 40 -34 L 40 -14 L 80 -14 L 80 14 L 40 14 L 40 34 Z');
rect.attr({
"stroke": "black",
"fill" : "black",
"enable" : "true",
}).translate(left + width, goalY);
var txt = gaugeSvg.path('M 0 0 L ' + width + " 0");
txt.attr({
"stroke": "black",
"stroke-width": 12,
"stroke-dasharray": "-",
"stroke-linecap": "round"
}).translate(left, goalY);
//Create a set so we can move the
//arrow and line at the same time
var g = gaugeSvg.set();
g.push(rect, txt);
// var g = gaugeSvg.set(rect, txt);
var me = this,
lx = 0,
ly = 0,
ox = 0,
oy = 0,
moveFnc = function(dx, dy) {
this.translate(dx-ox, dy-oy);
ox = dx;
oy = dy;
},
startFnc = function() {},
endFnc = function() {
ox = lx;
oy = ly;
};
g.drag(moveFnc, startFnc, endFnc);
}
I have'nt used Rapheal . But i have achieved this Drag functionlaity with SVG amd Javascript.
In order to move multiple elements, you need to group them. Means put these elements in 'g' group and then apply drag function on this 'g'. and once finished, you need to 'ungroup' them.
you can see demo here http://jsfiddle.net/rehankhalid/5t5pX/
var mainsvg = document.getElementsByTagName('svg')[0];
function mousemove(event) {
var svgXY = getSvgCordinates(event);// get current x,y w.r.t to your svg.
dx = svgXY.x - mx;// mx means x cordinates of mouse down
dy = svgXY.y - my;
draggroup.setAttribute('transform', 'translate(' + dx + ',' + dy + ')');
}
function getSvgCordinates(event) {
var m = mainsvg.getScreenCTM();
var p = mainsvg.createSVGPoint();
var x, y;
x = event.pageX;
y = event.pageY;
p.x = x;
p.y = y;
p = p.matrixTransform(m.inverse());
x = p.x;
y = p.y;
x = parseFloat(x.toFixed(3));
y = parseFloat(y.toFixed(3));
return {x: x, y: y};
}

Resources