I'm new to d3 and a pretty average javascript programmer. I've got some code to create a circle, and a function to get the x and y co-ordinates of a point on a circle:
var innerCircle = svg.append('circle')
.attr({
cx: 100,
cy: 100,
r: 50,
'stroke': 'white',
'fill': 'transparent',
});
var pointOnCircle = function(circle, radians){
var cx = parseInt(circle.attr('cx'));
var cy = parseInt(circle.attr('cy'));
var r = parseInt(circle.attr('r'));
var x = cx + Math.sin(radians) * r;
var y = cy + Math.cos(radians) * r;
return {x: x, y: y};
}
It works. But I feel like continuing with this approach will make my code a messy grab bag of global functions, and that I should be able to make it object-oriented, so rather than calling:
var point = pointOnCircle(circle, Math.PI);
I can instead call:
var point = circle.pointAt(Math.PI);
But this would involve me either attaching a pointAt function to a d3 object somehow, or creating my own Circle object that has a pointAt function, and wraps a d3 object. Is either of these a good idea?
There's other points where I feel like I'd want something similar - kind of like I want to be mapping 'objects' to documents, as opposed to plain old data to documents. Is this a common requirement, or am I missing something conceptually?
What's the best way to approach my pointOnCircle issue that I'm having above?
Most of the d3 examples are small, self-contained, and written in one single script. Are there any examples showing how to build something with more reusable functions?
You can follow d3.js style of functional programming as demonstrated below
function innerCircle() {
var current_attr, current_style, circle_elem;
var _circle = function (svg) {
circle_elem = svg.append('circle')
.attr(current_attr)
.attr(current_style);
return circle_elem;
}
_circle.pointAt = function (randians) {
if(! circle_elem) //If the circle is not drawn yet.
return {x: -1, y: -1};
var cx = parseInt(circle_elem.attr('cx'));
var cy = parseInt(circle_elem.attr('cy'));
var r = parseInt(circle_elem.attr('r'));
var x = cx + Math.sin(radians) * r;
var y = cy + Math.cos(radians) * r;
return {x: x, y: y};
}
_circle.attr = function(attr_val){
if(! arguments.length)
return current_attr;
current_attr = attr_val;
return _circle;
}
_circle.style = function(style_val){
if(arguments.length == 1)
return current_style;
current_style = style_val;
return _circle;
}
return _circle;
}
This is a typical example of functional programming. The main object is _circle function is obtained by calling innerCircle. _circle draws a circle to an svg according to its set attributes (current_attr, current_style). To draw a circle to an svg, you can do it d3.js way:
var new_circle = innerCircle();
svg.call( new_circle );
The _circle function has 3 defined methods, attr, style and pointAt. attr and style are getter/setter functions, if you call them without arguments they will return the current value (getter), and if called with an argument, they will set the current value to it.
new_circle.style(); //get the current style
//set attributes
new_circle.attr({
cx: 100,
cy: 100,
r: 50,
'stroke': 'white',
'fill': 'transparent',
});
You can also call your pointAt function similarly.
new_circle.pointAt(Math.PI);
One last caveat to this programming style is the return _circle; statement at the end of all setter functions, which allows chaining. So, your example can be reproduced by:
var new_circle = innerCircle()
.attr({
cx: 100,
cy: 100,
r: 50,
})
.style({
'stroke': 'white',
'fill': 'transparent',
});
svg.call(new_circle);
Hope this helps. Let me know of any unclear points.
I hope this helps.
var myProgram = {};
myProgram.circleModule = (function() {
var innerCircle = d3.select("#svg").append('circle')
.attr({
cx: 100,
cy: 100,
r: 50,
'stroke': 'black',
'fill': 'red',
});
var pointOnCircle = function(circle, radians) {
var cx = parseInt(circle.attr('cx'));
var cy = parseInt(circle.attr('cy'));
var r = parseInt(circle.attr('r'));
var x = cx + Math.sin(radians) * r;
var y = cy + Math.cos(radians) * r;
return {
x: x,
y: y
};
}
return {
circle: innerCircle,
pointOnCircle: pointOnCircle
}
})();
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled Document</title>
<script src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
</head>
<body>
<svg id="svg" width="200" height="200">
</svg>
Related
I'm looking for a way to zoom from place to place on a globe in D3 v4 (v4 is important). What I'm looking for is basically exactly this: https://www.jasondavies.com/maps/zoom/ My problem is that Jason Davies obfuscated his code, so I can't read it, and I can't find a bl.ock containing that project or anything similar to it. I'll provide a link to what I've got here: http://plnkr.co/edit/0mjyR3ovTfkDXB8FTG0j?p=preview
The relevant is probably inside the .tween():
.tween("rotate", function () {
var p = d3.geoCentroid(points[i]),
r = d3.geoInterpolate(projection.rotate(), [-p[0], -p[1]]);
return function (t) {
projection.rotate(r(t));
convertedLongLats = [projection(points[0].coordinates), projection(points[1].coordinates)]
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
for (var j = 0; j < points.length; j++) {
var textCoords = projection(points[j].coordinates);
c.fillStyle = textColors, c.textAlign = "center", c.font = "18px FontAwesome", c.fillText(points[j].icon, textCoords[0], textCoords[1]);
textCoords[0] += 15;
c.textAlign = "left", c.font = " 12px Roboto", c.fillText(points[j].location, textCoords[0], textCoords[1]);
}
c.strokeStyle = textColors, c.lineWidth = 4, c.setLineDash([10, 10]), c.beginPath(), c.moveTo(convertedLongLats[0][0], convertedLongLats[0][1]), c.lineTo(convertedLongLats[1][0], convertedLongLats[1][1]), c.stroke();
};
})
Basically, I want what I've got now but I want it to zoom in, pretty much exactly like it is in the Animated World Zoom example I provided above. I'm not really looking for code, I'd rather someone point me in the right direction with an example or something (it's worth noting that I'm fairly new to d3 and that this project is heavily based on World Tour by mbostock, so it uses canvas). Thank you all in advance!
Based on your plunker and comment, a challenge in zooming out between two points in a transition is that the interpolator will only interpolate between two values. The solution in your plunker relies on two interpolators, one for zooming in and zooming out. This method has added un-needed complexity and somewhere along the line, as you note, it jumps to an incorrect scale. You could simplify this:
Take an interpolator that interpolates between -1 and 1, and weight each scale according to the absolute value of the interpolator. At zero, the zoom should be out all the way, while at -1,1, you should be zoomed in:
var s = d3.interpolate(-1,1);
// get the appropriate scale:
scale = Math.abs(0-s(t))*startEndScale + (1-Mat.abs(0-s(t)))*middleScale
This is a little clunky as it goes from zooming out to zooming in rather abruptly, so you could ease it with a sine type easing:
var s = d3.interpolate(0.0000001,Math.PI);
// get the appropriate scale:
scale = (1-Math.abs(Math.sin(s(t))))*startEndScale + Math.abs(Math.sin(s(t)))*middleScale
I've applied this to your plunker here.
For a simple and minimal example using the example that I suggested and your two points and path (and using your plunkr as a base), stripping out the animated line and icons, I would probably put together something like (plunker, snippet below best viewed on full screen):
var width = 600,
height = 600;
var points = [{
type: "Point",
coordinates: [-74.2582011, 40.7058316],
location: "Your Location",
icon: "\uF015"
}, {
type: "Point",
coordinates: [34.8887969, 32.4406351],
location: "Caribe Royale Orlando",
icon: "\uF236"
}];
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var c = canvas.node().getContext("2d");
var point = points[0].coordinates;
var projection = d3.geoOrthographic()
.translate([width / 2, height / 2])
.scale(width / 2)
.clipAngle(90)
.precision(0.6)
.rotate([-point[0], -point[1]]);
var path = d3.geoPath()
.projection(projection)
.context(c);
var colorLand = "#4d4f51",
colorGlobe = "#2e3133",
textColors = "#fff";
d3.json("https://unpkg.com/world-atlas#1/world/110m.json", function (error, world) {
if (error) throw error;
var sphere = { type: "Sphere" };
var land = topojson.feature(world, world.objects.land);
var i = 0;
var scaleMiddle = width/2;
var scaleStartEnd = width * 2;
loop();
function loop() {
d3.transition()
.tween("rotate",function() {
var flightPath = {
type: 'Feature',
geometry: {
type: "LineString",
coordinates: [points[i++%2].coordinates, points[i%2].coordinates]
}
};
// next point:
var p = points[i%2].coordinates;
// current rotation:
var currentRotation = projection.rotate();
// next rotation:
var nextRotation = projection.rotate([-p[0],-p[1]]).rotate();
// Interpolaters:
var r = d3.geoInterpolate(currentRotation,nextRotation);
var s = d3.interpolate(0.0000001,Math.PI);
return function(t) {
// apply interpolated values
projection.rotate(r(t))
.scale( (1-Math.abs(Math.sin(s(t))))*scaleStartEnd + Math.abs(Math.sin(s(t)))*scaleMiddle ) ;
c.clearRect(0, 0, width, height);
c.fillStyle = colorGlobe, c.beginPath(), path(sphere), c.fill();
c.fillStyle = colorLand, c.beginPath(), path(land), c.fill();
c.beginPath(), path(flightPath), c.globalAlpha = 0.5, c.shadowColor = "#fff", c.shadowBlur = 5, c.lineWidth = 0.5, c.strokeStyle = "#fff", c.stroke(), c.shadowBlur = 0, c.globalAlpha = 1;
}
})
.duration(3000)
.on("end", function() { loop(); })
}
});
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
I have the following code which creates me rectangle that contains some text. I need to create multiple addressable instances of this rectangle so that I can individually animate them. Each rectangle needs to contain a different text label.
var s = Snap(800, 600);
var block = s.rect(50, 50, 100, 100, 5, 5);
block.attr({
fill: "rgb(236, 240, 241)",
stroke: "#1f2c39",
strokeWidth: 3
});
var text = s.text(70, 105, "Hello World");
text.attr({
'font-size':20
});
block.attr({
width: (text.node.clientWidth + 50)
});
Rather than repeating my code I would like to create a function that accepts the text and the coordinates for placing the rectangle. What is the best way to achieve this ? Is this capability already included within snap.svg ?
UPDATE
I created another plugin, this time to import and scale SVG images. Is this the best approach to take for this ? Is the only way to scale the image using the `transform attribute ?
Import SVG plugin example.
Snap.plugin( function( Snap, Element, Paper, global ) {
Paper.prototype.importImage = function( x, y, scale ) {
var ig1 = s.group();
var image = Snap.load("../package.svg", function ( loadedFragment ) {
ig1.attr({transform: 'translate(' + x + ',' + y + ') scale('+ scale +')'});
ig1.append( loadedFragment);
} );
return ig1;
}
});
You could create a plugin to give you a new element option that does it for you, for example...
Snap.plugin( function( Snap, Element, Paper, global ) {
Paper.prototype.textRect = function( text, x, y ) {
var block = s.rect(x, y, 100, 100, 5, 5)
.attr({
fill: "rgb(236, 240, 241)",
stroke: "#1f2c39",
strokeWidth: 3,
});
var text = s.text(x + 20, y + 50, text).attr({ 'font-size': 20 });
block.attr({ width: (text.node.clientWidth + 50) });
}
});
var s = Snap(800,800)
s.textRect('Hi', 100, 100 );
s.textRect('There', 100, 200 );
example fiddle
You may want to put them both in a 'g' group element if you will move them around or drag them or something, so you can perform operations on the group.
Here is the "dragger" defined in the raphaeljs graffle example.
var dragger = function () {
this.ox = this.type == "rect" ? this.attr("x") : this.attr("cx");
this.oy = this.type == "rect" ? this.attr("y") : this.attr("cy");
this.animate({"fill-opacity": .2}, 500);
},
move = function (dx, dy) {
var att = this.type == "rect" ? {x: this.ox + dx, y: this.oy + dy} : {cx: this.ox + dx, cy: this.oy + dy};
this.attr(att);
for (var i = connections.length; i--;) {
r.connection(connections[i]);
}
r.safari();
},
up = function () {
this.animate({"fill-opacity": 0}, 500);
},
r = Raphael("holder", 640, 480),
connections = [],
shapes = [ r.ellipse(190, 100, 30, 20),
r.rect(290, 80, 60, 40, 10),
r.rect(290, 180, 60, 40, 2),
r.ellipse(450, 100, 20, 20)
];
Am I right that:
That move, up, r, connections and shapes are INDEPENDENT variables? I.e. NOT methods/ attributes of dragger?
This is just a sequential definition of variables at an equivalent scope?
Are move, up, etc. considered to have been declared using the var keyword?
Thanks for the help. Love and peace.
This is just like a long series of var statements.
Similar, and easier to read:
var a = 1, b = 2, c = 3;
In this case I definitely would NOT use this syntax, and would instead use a separate line for each variable. It's really hard to read as is.
In short: yes, you are right on both counts. This is exactly the same as
var dragger = ...;
var move = ...;
var up = ....;
...
except it's a pain to read. The extra commas hiding between declarations are what tie the whole thing together into the var statement.
I have a function which displays lines (x and y coordinates) based on the time information. The x and y coordinates specify the position of the drawn points whereas time represents the timestamps (in milliseconds) of the respective points.
Currently, there is a function which displays line as below
<script type="text/javascript" src="https://raw.github.com/DmitryBaranovskiy/raphael/master/raphael-min.js"></script>
<script type="text/javascript">
function drawLine(points) {
var paths = ['M ' + points[0].x + ' ' + points[0].y];
for (var i = 1; i < points.length; i++) {
var p = points[i];
paths.push(paths[i - 1] + ' L ' + p.x + ' ' + p.y);
}
var paper = new Raphael(document.getElementById('canvas_container'), 500, 500);
var line = paper.path(paths[0]);
var next = 1;
function animate() {
if (paths[next]) {
duration = points[next].t - points[next - 1].t
line.animate({ path: paths[next] }, duration, 'linear', animate);
next++;
}
}
animate();
}
</script>
And the function can be called using associative arrays as follows:
drawLine([
{ x: 0, y: 0, t: 0 },
{ x: 100, y: 230, t: 1520 },
{ x: 210, y: 290, t: 3850 },
{ x: 150, y: 200, t: 5060 },
]);
The question is, how can I modify this function to display points and not the lines?
You can add a drawPoint method, which will take an object with x and y properties
function drawPoint(point) {
paper.circle(point.x, point.y, 5).attr('fill', 'red');
};
Then call it from your animate function, before the points[next] comparison
drawPoint(points[next - 1]);
Here's the JSFiddle http://jsfiddle.net/jaimem/2krgN/
If you don't want the lines, then you don't need paths
function drawPoints(points){
var paper = new Raphael('canvas_container', 500, 500),
idx = 0;
function animate(){
if(points[idx]){
var currP = points[idx],
prevP = points[idx - 1],
d = currP.t - (prevP ? prevP.t : 0 );
paper.circle(currP.x, currP.y, 1)
.attr('fill', 'red')
.animate({r:5}, d, animate);
idx++
}
}
animate();
}
The recursive animate callback might be a little difficult to understand/read, so might just want to use a setTimeout. Also you can pass a string with the id of an element to the Raphael constructor and the library will find the DOM node for you.
JS Fiddle: http://jsfiddle.net/jaimem/Q5G5y/2/
I am new to Kineticjs and not a great javascript coder so I am hoping to get some help with this example.
http://jsfiddle.net/pwM8M/
I am trying to store the x axis on doors so when a redraw unrelated to the doors is done they don't go back to the default position. (multiple types of doors and windows too)
Each form element can have multiple quantities (more than one door) so I need a way to store/retrieve the data currently contained in the alert on jsfiddle.
I did some research but have come up empty. Can anyone help with what I have provided?
function OH(x,y,w,h) {
var oh = new Kinetic.Text({
x:x,
y: y,
width: w*scale,
height: h*scale,
stroke: wtc,
strokeWidth: 1,
fill: 'brown',
fontSize: 6,
fontFamily: 'Calibri',
padding:4,
text: w+' x '+h+' OH\n\n<- DRAG ->',
align: 'center',
textFill: 'white',
draggable: true,
dragConstraint:'horizontal',
dragBounds: {
left:xoffset, right: xoffset+width+length-(w*scale)
}
});
oh.on('dragend', function(e) {
alert(oh.getPosition().x);
});
window.south.add(oh);
}
Thanks,
I have fixed sized 40x40 rectangle here in which i am using dragging function. try this
var box = new Kinetic.Rect({
x: parseFloat(area.size.x),
y: parseFloat(area.size.y),
width: 40, //parseFloat(area.size.width)
height: 40,
fill: area.color,
stroke: 'black',
strokeWidth: 1,
opacity: 0.6,
id : area.id + id,
draggable: true,
dragBoundFunc: function(pos) {
// x
var newX = pos.x < 0 ? 40 : pos.x;
var newX = newX > _self.canvasWidth - area.size.width ? _self.canvasWidth - area.size.width : newX;
// y
var newY = pos.y < 0 ? 40 : pos.y;
var newY = newY > _self.canvasHeight - area.size.height ? _self.canvasHeight - area.size.height : newY;
return {
x: newX,
y: newY
};
Use this function
box.on('dragend', function() {
_self.draw = false;
_self.dragArea(area, box);
});
and try this to play with x y coordinates
dragArea: function(area, box){
if(box){
$$('#' + this.formId + ' [name="areas[' + area.id + '][size][x]"]').first().value = parseInt(box.attrs.x);
$$('#' + this.formId + ' [name="areas[' + area.id + '][size][y]"]').first().value = parseInt(box.attrs.y);
area.size.x = parseInt(box.attrs.x);
area.size.y = parseInt(box.attrs.y);
}
},
Create a new array before the function, like so:
var xArray = new Array();
then inside your function
oh.on('dragend', function(e) {
alert(oh.getPosition().x);
// ADD NEW ITEM TO ARRAY, STORE X POSITION
xArray.push(oh.getPosition().x);
});
and so all the x values get stored in the array.
If you need to clear the array, you can simply create a new one again with the same name.
And you can iterate through it with a loop if needed.
updated:
http://jsfiddle.net/pwM8M/2/