I am using KineticJS in my project. I need to connect two shapes using a curved line. One of the shapes can be dragged. I am able to put the curved line between shapes. The problem arises when user starts dragging the shapes. The requirement is that it should be properly curved (please refer to screen shots), irrespective of distance between them and their position with respect to each other. I am doing this:
var utils = {
_getCenter: function(x1, y1, x2, y2) {
return {
x: (x1 + x2) / 2,
y: (y1 + y2) / 2
}
},
// Converts from degrees to radians.
_radians: function(degrees) {
return degrees * Math.PI / 180;
},
// Converts from radians to degrees.
_degrees: function(radians) {
return radians * 180 / Math.PI;
}
};
function amplitude(point) {
var rad_90 = utils._radians(90);
var rad_45 = utils._radians(45);
var rad_60 = utils._radians(60);
console.log(rad_90);
return {
x: point.x * Math.cos(rad_60),
y: point.y * Math.sin(rad_60)
};
}
var width = window.innerWidth;
var height = window.innerHeight;
var stage = new Kinetic.Stage({
container: 'container',
width: width,
height: height
});
var layer = new Kinetic.Layer();
var circle = new Kinetic.Circle({
x: stage.getWidth() / 2,
y: stage.getHeight() / 2,
radius: 20,
fill: 'red',
stroke: 'black',
strokeWidth: 2
});
var attachedCircle = new Kinetic.Circle({
x: stage.getWidth() / 4,
y: stage.getHeight() / 4,
radius: 20,
fill: 'red',
stroke: 'black',
strokeWidth: 2,
draggable: true
});
var center = amplitude(utils._getCenter(circle.getX(), circle.getY(), attachedCircle.getX(), attachedCircle.getY()));
var line = new Kinetic.Line({
points: [circle.getX(), circle.getY(), center.x, center.y, attachedCircle.getX(), attachedCircle.getY()],
fill: 'black',
stroke: 'green',
strokeWidth: 3,
/*
* line segments with a length of 33px
* with a gap of 10px
*/
dash: [33, 10],
id: 'line',
tension: 0.5
});
attachedCircle.on('dragmove', function(e) {
var targetCircle = e.target;
var tempCenter = amplitude(utils._getCenter(circle.getX(), circle.getY(), targetCircle.getX(), targetCircle.getY()));
console.log(tempCenter);
line.setPoints([circle.getX(), circle.getY(), tempCenter.x, tempCenter.y, targetCircle.getX(), targetCircle.getY()]);
});
// add the shape to the layer
layer.add(line);
layer.add(attachedCircle);
layer.add(circle);
// add the layer to the stage
stage.add(layer);
I don't know what I am missing. I have created the plunkr for this.
To define you amplitude function you need to use two input points:
function amplidure2(p1, p2) {
var alpha = Math.atan((p1.x - p2.x) / (p1.y - p2.y)) + Math.PI / 2;
if (p1.y < p2.y) {
alpha += Math.PI;
}
var center = utils._getCenter(p1.x, p1.y, p2.x, p2.y);
var r = 50;
return {
x: center.x + r * Math.sin(alpha),
y: center.y + r * Math.cos(alpha)
}
}
DEMO
I'm attempting to integrate some charts into a project and I've got this one working just like the example, but properly displaying my data, but What I'd like to be able to do is change the baseline from 0 to another value as most of my data is points > 700, making the line relatively flat, as they are all high values.
Can anyone tell me where the baseline value is set in this? I'm not much of a javascript guy, so I'm having trouble.
Edit: I'd also like to be able to set a Fixed Top Line as well, ideally, if anyone sees an easy way to do both?
Edit2: Found the method of fixing the Topline: There's a "max" variable. Just alter that.
The Example is here. http://raphaeljs.com/analytics.html
The Javascript in Question is this:
Raphael.fn.drawGrid = function (x, y, w, h, wv, hv, color) {
color = color || "#000";
var path = ["M", Math.round(x) + .5, Math.round(y) + .5, "L", Math.round(x + w) + .5, Math.round(y) + .5, Math.round(x + w) + .5, Math.round(y + h) + .5, Math.round(x) + .5, Math.round(y + h) + .5, Math.round(x) + .5, Math.round(y) + .5],
rowHeight = h / hv,
columnWidth = w / wv;
for (var i = 1; i < hv; i++) {
path = path.concat(["M", Math.round(x) + .5, Math.round(y + i * rowHeight) + .5, "H", Math.round(x + w) + .5]);
}
for (i = 1; i < wv; i++) {
path = path.concat(["M", Math.round(x + i * columnWidth) + .5, Math.round(y) + .5, "V", Math.round(y + h) + .5]);
}
return this.path(path.join(",")).attr({stroke: color});
};
$(function () {
$("#data").css({
position: "absolute",
left: "-9999em",
top: "-9999em"
});
});
window.onload = function () {
function getAnchors(p1x, p1y, p2x, p2y, p3x, p3y) {
var l1 = (p2x - p1x) / 2,
l2 = (p3x - p2x) / 2,
a = Math.atan((p2x - p1x) / Math.abs(p2y - p1y)),
b = Math.atan((p3x - p2x) / Math.abs(p2y - p3y));
a = p1y < p2y ? Math.PI - a : a;
b = p3y < p2y ? Math.PI - b : b;
var alpha = Math.PI / 2 - ((a + b) % (Math.PI * 2)) / 2,
dx1 = l1 * Math.sin(alpha + a),
dy1 = l1 * Math.cos(alpha + a),
dx2 = l2 * Math.sin(alpha + b),
dy2 = l2 * Math.cos(alpha + b);
return {
x1: p2x - dx1,
y1: p2y + dy1,
x2: p2x + dx2,
y2: p2y + dy2
};
}
// Grab the data
var labels = [],
data = [];
$("#data tfoot th").each(function () {
labels.push($(this).html());
});
$("#data tbody td").each(function () {
data.push($(this).html());
});
// Draw
var width = 800,
height = 250,
leftgutter = 30,
bottomgutter = 20,
topgutter = 20,
colorhue = .6 || Math.random(),
color = "hsl(" + [colorhue, .5, .5] + ")",
r = Raphael("holder", width, height),
txt = {font: '12px Helvetica, Arial', fill: "#fff"},
txt1 = {font: '10px Helvetica, Arial', fill: "#fff"},
txt2 = {font: '12px Helvetica, Arial', fill: "#000"},
X = (width - leftgutter) / labels.length,
max = Math.max.apply(Math, data),
Y = (height - bottomgutter - topgutter) / max;
r.drawGrid(leftgutter + X * .5 + .5, topgutter + .5, width - leftgutter - X, height - topgutter - bottomgutter, 10, 10, "#000");
var path = r.path().attr({stroke: color, "stroke-width": 4, "stroke-linejoin": "round"}),
bgp = r.path().attr({stroke: "none", opacity: .3, fill: color}),
label = r.set(),
lx = 0, ly = 0,
is_label_visible = false,
leave_timer,
blanket = r.set();
label.push(r.text(60, 12, "24 hits").attr(txt));
label.push(r.text(60, 27, "22 September 2008").attr(txt1).attr({fill: color}));
label.hide();
var frame = r.popup(100, 100, label, "right").attr({fill: "#000", stroke: "#666", "stroke-width": 2, "fill-opacity": .7}).hide();
var p, bgpp;
for (var i = 0, ii = labels.length; i < ii; i++) {
var y = Math.round(height - bottomgutter - Y * data[i]),
x = Math.round(leftgutter + X * (i + .5)),
t = r.text(x, height - 6, labels[i]).attr(txt).toBack();
if (!i) {
p = ["M", x, y, "C", x, y];
bgpp = ["M", leftgutter + X * .5, height - bottomgutter, "L", x, y, "C", x, y];
}
if (i && i < ii - 1) {
var Y0 = Math.round(height - bottomgutter - Y * data[i - 1]),
X0 = Math.round(leftgutter + X * (i - .5)),
Y2 = Math.round(height - bottomgutter - Y * data[i + 1]),
X2 = Math.round(leftgutter + X * (i + 1.5));
var a = getAnchors(X0, Y0, x, y, X2, Y2);
p = p.concat([a.x1, a.y1, x, y, a.x2, a.y2]);
bgpp = bgpp.concat([a.x1, a.y1, x, y, a.x2, a.y2]);
}
var dot = r.circle(x, y, 4).attr({fill: "#333", stroke: color, "stroke-width": 2});
blanket.push(r.rect(leftgutter + X * i, 0, X, height - bottomgutter).attr({stroke: "none", fill: "#fff", opacity: 0}));
var rect = blanket[blanket.length - 1];
(function (x, y, data, lbl, dot) {
var timer, i = 0;
rect.hover(function () {
clearTimeout(leave_timer);
var side = "right";
if (x + frame.getBBox().width > width) {
side = "left";
}
var ppp = r.popup(x, y, label, side, 1),
anim = Raphael.animation({
path: ppp.path,
transform: ["t", ppp.dx, ppp.dy]
}, 200 * is_label_visible);
lx = label[0].transform()[0][1] + ppp.dx;
ly = label[0].transform()[0][2] + ppp.dy;
frame.show().stop().animate(anim);
label[0].attr({text: data + " hit" + (data == 1 ? "" : "s")}).show().stop().animateWith(frame, anim, {transform: ["t", lx, ly]}, 200 * is_label_visible);
label[1].attr({text: lbl + " September 2008"}).show().stop().animateWith(frame, anim, {transform: ["t", lx, ly]}, 200 * is_label_visible);
dot.attr("r", 6);
is_label_visible = true;
}, function () {
dot.attr("r", 4);
leave_timer = setTimeout(function () {
frame.hide();
label[0].hide();
label[1].hide();
is_label_visible = false;
}, 1);
});
})(x, y, data[i], labels[i], dot);
}
p = p.concat([x, y, x, y]);
bgpp = bgpp.concat([x, y, x, y, "L", x, height - bottomgutter, "z"]);
path.attr({path: p});
bgp.attr({path: bgpp});
frame.toFront();
label[0].toFront();
label[1].toFront();
blanket.toFront();
};
It's been a while since you posted the question, but maybe the answer is still helpful for someone else:
You need to add some extra variables to get the minimum data and to set a new baseline:
The maximum value is already defined here.
max = Math.max.apply(Math, data), //e.g. 800
Now you just do the same to get the minimum value:
min = Math.min.apply(Math, data), //e.g. 700
From these two variables you can get the range of the values. Use 'range' instead of 'max' to calculate Y.
range = max - min, //e.g 100
Y = (height - bottomgutter - topgutter ) / range;
Now jump to the Loop where y, Y0 and Y2 are defined and change
y = Math.round(height - bottomgutter - Y * data[i]),
Y0 = Math.round(height - bottomgutter - Y * data[i - 1]),
Y2 = Math.round(height - bottomgutter - Y * data[i + 1]),
to:
y = Math.round(height - bottomgutter - Y * data[i] + min * Y),
Y0 = Math.round(height - bottomgutter - Y * data[i - 1] + min * Y),
Y2 = Math.round(height - bottomgutter - Y * data[i + 1] + min * Y),
And if you prefer copy&paste, that is the complete edited window.onload function() with a new baseline:
window.onload = function () {
function getAnchors(p1x, p1y, p2x, p2y, p3x, p3y) {
var l1 = (p2x - p1x) / 2,
l2 = (p3x - p2x) / 2,
a = Math.atan((p2x - p1x) / Math.abs(p2y - p1y)),
b = Math.atan((p3x - p2x) / Math.abs(p2y - p3y));
a = p1y < p2y ? Math.PI - a : a;
b = p3y < p2y ? Math.PI - b : b;
var alpha = Math.PI / 2 - ((a + b) % (Math.PI * 2)) / 2,
dx1 = l1 * Math.sin(alpha + a),
dy1 = l1 * Math.cos(alpha + a),
dx2 = l2 * Math.sin(alpha + b),
dy2 = l2 * Math.cos(alpha + b);
return {
x1: p2x - dx1,
y1: p2y + dy1,
x2: p2x + dx2,
y2: p2y + dy2
};
}
// Grab the data
var labels = [],
data = [];
$("#data tfoot th").each(function () {
labels.push($(this).html());
});
$("#data tbody td").each(function () {
data.push($(this).html());
});
// Draw
var width = 800,
height = 250,
leftgutter = 30,
bottomgutter = 20,
topgutter = 20,
colorhue = .6 || Math.random(),
color = "hsl(" + [colorhue, .5, .5] + ")",
r = Raphael("holder", width, height),
txt = {font: '12px Helvetica, Arial', fill: "#fff"},
txt1 = {font: '10px Helvetica, Arial', fill: "#fff"},
txt2 = {font: '12px Helvetica, Arial', fill: "#000"},
X = (width - leftgutter) / labels.length,
max = Math.max.apply(Math, data),
min = Math.min.apply(Math, data),
range = max - min,
Y = (height - bottomgutter - topgutter ) / range;
r.drawGrid(leftgutter + X * .5 + .5, topgutter + .5, width - leftgutter - X, height - topgutter - bottomgutter, 10, 10, "#000");
var path = r.path().attr({stroke: color, "stroke-width": 4, "stroke-linejoin": "round"}),
bgp = r.path().attr({stroke: "none", opacity: .3, fill: color}),
label = r.set(),
lx = 0, ly = 0,
is_label_visible = false,
leave_timer,
blanket = r.set();
label.push(r.text(60, 12, "24 hits").attr(txt));
label.push(r.text(60, 27, "22 September 2008").attr(txt1).attr({fill: color}));
label.hide();
var frame = r.popup(100, 100, label, "right").attr({fill: "#000", stroke: "#666", "stroke-width": 2, "fill-opacity": .7}).hide();
var p, bgpp;
for (var i = 0, ii = labels.length; i < ii; i++) {
var y = Math.round(height - bottomgutter - Y * data[i] + min * Y),
x = Math.round(leftgutter + X * (i + .5)),
t = r.text(x, height - 6, labels[i]).attr(txt).toBack();
if (!i) {
p = ["M", x, y, "C", x, y];
bgpp = ["M", leftgutter + X * .5, height - bottomgutter, "L", x, y, "C", x, y];
}
if (i && i < ii - 1) {
var Y0 = Math.round(height - bottomgutter - Y * data[i - 1] + min * Y),
X0 = Math.round(leftgutter + X * (i - .5)),
Y2 = Math.round(height - bottomgutter - Y * data[i + 1] + min * Y),
X2 = Math.round(leftgutter + X * (i + 1.5));
var a = getAnchors(X0, Y0, x, y, X2, Y2);
p = p.concat([a.x1, a.y1, x, y, a.x2, a.y2]);
bgpp = bgpp.concat([a.x1, a.y1, x, y, a.x2, a.y2]);
}
var dot = r.circle(x, y, 4).attr({fill: "#333", stroke: color, "stroke-width": 2});
blanket.push(r.rect(leftgutter + X * i, 0, X, height - bottomgutter).attr({stroke: "none", fill: "#fff", opacity: 0}));
var rect = blanket[blanket.length - 1];
(function (x, y, data, lbl, dot) {
var timer, i = 0;
rect.hover(function () {
clearTimeout(leave_timer);
var side = "right";
if (x + frame.getBBox().width > width) {
side = "left";
}
var ppp = r.popup(x, y, label, side, 1),
anim = Raphael.animation({
path: ppp.path,
transform: ["t", ppp.dx, ppp.dy]
}, 200 * is_label_visible);
lx = label[0].transform()[0][1] + ppp.dx;
ly = label[0].transform()[0][2] + ppp.dy;
frame.show().stop().animate(anim);
label[0].attr({text: data + " hit" + (data == 1 ? "" : "s")}).show().stop().animateWith(frame, anim, {transform: ["t", lx, ly]}, 200 * is_label_visible);
label[1].attr({text: lbl + " September 2008"}).show().stop().animateWith(frame, anim, {transform: ["t", lx, ly]}, 200 * is_label_visible);
dot.attr("r", 6);
is_label_visible = true;
}, function () {
dot.attr("r", 4);
leave_timer = setTimeout(function () {
frame.hide();
label[0].hide();
label[1].hide();
is_label_visible = false;
}, 1);
});
})(x, y, data[i], labels[i], dot);
}
p = p.concat([x, y, x, y]);
bgpp = bgpp.concat([x, y, x, y, "L", x, height - bottomgutter, "z"]);
path.attr({path: p});
bgp.attr({path: bgpp});
frame.toFront();
label[0].toFront();
label[1].toFront();
blanket.toFront();
};
I am building a custom chart with Raphael.js which includes arcs and percentages. The text label of an arc needs to move with the arc. I have achieved this by using the onAnimation callback and getting the point of the outer arc and position the text label accordingly:
var paper = Raphael("paper", 200, 200);
var centerX = paper.width / 2;
var centerY = paper.height / 2;
// This is a dummy percentage; in real app this corresponds to actual value
var percentage = 0;
paper.customAttributes.arc = function(centerX, centerY, startAngle, endAngle, innerR, outerR) {
var radians = Math.PI / 180,
largeArc = +(endAngle - startAngle > 180),
outerX1 = centerX + outerR * Math.cos((startAngle - 90) * radians),
outerY1 = centerY + outerR * Math.sin((startAngle - 90) * radians),
outerX2 = centerX + outerR * Math.cos((endAngle - 90) * radians),
outerY2 = centerY + outerR * Math.sin((endAngle - 90) * radians),
innerX1 = centerX + innerR * Math.cos((endAngle - 90) * radians),
innerY1 = centerY + innerR * Math.sin((endAngle - 90) * radians),
innerX2 = centerX + innerR * Math.cos((startAngle - 90) * radians),
innerY2 = centerY + innerR * Math.sin((startAngle - 90) * radians);
var path = [
["M", outerX1, outerY1],
//move to the start point
["A", outerR, outerR, 0, largeArc, 1, outerX2, outerY2],
//draw the outer edge of the arc
["L", innerX1, innerY1],
//draw a line inwards to the start of the inner edge of the arc
["A", innerR, innerR, 0, largeArc, 0, innerX2, innerY2],
//draw the inner arc
["z"]
//close the path
];
return {
path: path
};
};
var arc = paper.path().attr({
'fill': "#cccccc",
'stroke': 'none',
'arc': [centerX, centerY, 0, 0, 60, 80]
});
arc.toBack();
var percentagesStart = 0;
var percentagesEnd = 0;
var percentagesDelta = 0;
var label = paper.text(
arc.attrs.path[1][6],
arc.attrs.path[1][7],
"0").toFront();
label.attr({
'font-size': 16,
'fill': "#000000",
'font-family': 'sans-serif',
'text-anchor': 'middle'
});
arc.onAnimation(function(anim) {
console.log("test");
label.attr({
x: this.attrs.path[1][6],
y: this.attrs.path[1][7],
text: (percentage++)
});
});
arc.animate({
'arc': [centerX, centerY, 0, 220, 60, 80]
}, 1000, 'easeInOut');
<script src="https://cdnjs.cloudflare.com/ajax/libs/raphael/2.1.4/raphael-min.js"></script>
<div id="paper"></div>
However, the text label must be positioned where the arc ends, with some nice margin. The text element position must be transformed relative to the arc.
I have tried to use
label.transform("t-10,10");
And tried some other x, y values but that does not work, because at some point the x value has to be negative, and at some point the x value has to be positive, depending on the arc's angle.
Is there a way to position this correctly?