I'm trying to generate a linechart in NVD3 with a vertical line. Particularly this kind of line chart.
The linechart has two panels a viewing panel and a zoom panel, I would want the line to be on both.
Something like this:
Is this feasible?
Edit:
I found out a way to do this by just appending to the data an extra stream which represents a line. e.g.
streams[3] = {key:'myline', values:[{x:68,y:0},{x:68,y:7}]}
Is there a better way?
Yes it's possible,
Here is an example of how to do it :
https://gist.github.com/timelyportfolio/80d85f78a5a975fa29d7#file-code-r
The solution here is to add a javascript function drawing vertical lines using NVD3 ( read carefully the comments ) :
function drawVerticalLines(opts) {
// CAREFUL HERE !!! the css pasth ".nvd3 .nv-focus .nv-linesWrap" depends on the type of chart you are using, lineChart would use only ".nvd3 .nv-linesWrap" ... !
if (!(d3.select('#' + opts.id + ' the css pasth ".nvd3 .nv-focus .nv" depends on the type of chart you are using, lineChart would use only -linesWrap').select('.vertical-lines')[0][0])) {
// Adds new g element with .vertical-lines class; use a css debugger to verify
d3.select('#' + opts.id + ' .nvd3 .nv-focus .nv-linesWrap').append('g')
.attr('class', 'vertical-lines')
}
vertLines = d3.select('#' + opts.id + ' .nvd3 .nv-focus .nv-linesWrap').select('.vertical-lines').selectAll('.vertical-line')
.data(
[{
'date': new Date('1967-11-30'),
'label': 'something to highlight 1967'
}, {
'date': new Date('2001-11-30'),
'label': 'something to highlight 2001'
}])
var vertG = vertLines.enter()
.append('g')
.attr('class', 'vertical-line')
vertG.append('svg:line')
vertG.append('text')
vertLines.exit().remove()
// CAREFUL 2 : chart.xAxis.scale() scale depends how you are defining your x Axis in nvd3 chart ... if your are using timestamps, (d.date / 60 / 60 / 24 / 1000) becomes (d.date)
vertLines.selectAll('line')
.attr('x1', function(d) {
return chart.xAxis.scale()(d.date / 60 / 60 / 24 / 1000)
})
.attr('x2', function(d) {
return chart.xAxis.scale()(d.date / 60 / 60 / 24 / 1000)
})
.attr('y1', chart.yAxis.scale().range()[0])
.attr('y2', chart.yAxis.scale().range()[1])
.style('stroke', 'red')
vertLines.selectAll('text')
.text(function(d) {
return d.label
})
.attr('dy', '1em')
//x placement ; change dy above for minor adjustments but mainly
// change the d.date/60/60/24/1000
//y placement ; change 2 to where you want vertical placement
//rotate -90 but feel free to change to what you would like
.attr('transform', function(d) {
return 'translate(' +
chart.xAxis.scale()(d.date / 60 / 60 / 24 / 1000) +
',' +
chart.yAxis.scale()(2) +
') rotate(-90)'
})
//also you can style however you would like
//here is an example changing the font size
.style('font-size', '80%')
}
And call this method in nv.addGraph Callback :
var sharedChart = null; // Shared reference on the chart
nv.addGraph(function() {
.....
sharedChart = chart;
return chart;
,
function() {
drawVerticalLines(opts, sharedChart);
}
);
With opts ... (obviously you don't really need it):
var opts${widgetID.replace('-', '0')} = {
"dom": "chart${widgetID}",
"width": 800,
"height": 400,
"x": "date",
"y": "value",
"group": "variable",
"type": "lineWithFocusChart",
"id": "chart${widgetID}"
};
Hope this helps, it took me quite a long time to find it and to make it work !
Related
simple by using rapheal i successfully make animation along path , but i can't reverse the animation direction ,,, just how to make it animate to the other direction when clicking the same path .
var paper = Raphael(0,0,1024,768);
var pathOne = paper.path(['M', 15,15 , 100,75]).attr({'stroke-width':18}).data("id",1);
//and this is just the circle
var circle = paper.circle(0, 0, 13).attr({
fill: '#09c', cursor: 'pointer'
});
//make the path as custom attribute so it can ba accessed
function pathPicker(thatPath){
paper.customAttributes.pathFactor = function(distance) {
var point = thatPath.getPointAtLength(distance * thatPath.getTotalLength());
var dx = point.x,
dy = point.y;
return {
transform: ['t', dx, dy]
};
}
}
//initialize for first move
pathPicker(pathOne);
circle.attr({pathFactor: 0}); // Reset
//Asign first path and first move
function firstMove(){
circle.animate({pathFactor: 1}, 1000});
}
pathOne.click(function(){
firstMove();
});
I couldn't get the original to run, so here is something using the main bits that should suit...
There's not a lot to it, get the length of the path, iterate over the length, draw object at the path. It uses the Raphael customAttributes to be able to animate it. I've added a custom toggle to make it easy to switch between them.
These are the key changes..
var len = pathOne.getTotalLength();
paper.customAttributes.along = function (v) {
var point = pathOne.getPointAtLength(v * len);
return {
transform: "t" + [point.x, point.y] + "r" + point.alpha
};
};
circle.attr({ along: 0 });
function animateThere( val ) {
val = +!val; // toggle
pathOne.click( function() { animateThere( val ) } );
circle.animate({ along: val }, 2000 );
};
pathOne.click( function() { animateThere(0) } );
jsfiddle
For completeness, you may want to do some extra checks like only allow the click if the animation has finished or something, as there may be a problem if you quickly click a lot and it buffering up animations.
another ThreeJS question:
How can I make a hovered (intersected) object scale smooth to a defined size? My current code is
INTERSECTED.scale.x *= 1.5;
INTERSECTED.scale.y *= 1.5;
but this only works like on/off.
Edit: My Solution (with tweenJS)
While mouseOver I scale up the element to 200% :
function scaleUp(){
new TWEEN.Tween( INTERSECTED.scale ).to( {
x: 2,
y: 2
}, 350 ).easing( TWEEN.Easing.Bounce.EaseOut).start();
}
While mouseOut I scale down the element to 100% :
function scaleDown(){
new TWEEN.Tween( INTERSECTED.scale ).to( {
x: 1,
y: 1
}, 350 ).easing( TWEEN.Easing.Bounce.EaseOut).start();
}
Finally I call the functions in the mouse function.
if (intersects[0].object != INTERSECTED)
scaleUp();
else
scaleDown();
That's all. Very useful for UIs I think.
Using the tween library (found in three.js/examples/js/libs/tween.min.js) you can animate the scale like so:
function setupObjectScaleAnimation( object, source, target, duration, delay, easing )
{
var l_delay = ( delay !== undefined ) ? delay : 0;
var l_easing = ( easing !== undefined ) ? easing : TWEEN.Easing.Linear.None;
new TWEEN.Tween( source )
.to( target, duration )
.delay( l_delay )
.easing( l_easing )
.onUpdate( function() { object.scale.copy( source ); } )
.start();
}
and then call it like so:
setupObjectScaleAnimation( INTERSECTED,
{ x: 1, y: 1, z: 1 }, { x: 2, y: 2, z: 2 },
2000, 500, TWEEN.Easing.Linear.None );
Or if you want to use the render loop:
clock = new THREE.Clock();
time = clock.getElapsedTime();
INSPECTED.scale.x = time / 1000;
INSPECTED.scale.y = time / 1000;
You can change the divisor based on how fast or slow you want the animation to happen.
I want to draw three different shapes collected inside a g element. I want to draw text, line and a graph. The problem im having with the code below is that the text is shown but the line does not show. It is drawn and I can see it in the source but it is not visible on the screen. What am I missing here?
//Bind data to a new g element
line = svg.selectAll("g")
.data(source)
.enter()
.append("g")
.attr({
"transform": function(d,i){ return "translate(50 " + (height - 50 + 15 * i) + ")"}
});
line.append("text")
.text(function(d,i){
return d.name;
});
line.append("line")
.attr({
"opacity" : 1,
"stroke-width" : 2,
"stroke" : "blue",
"class" : "crisp",
"x1" : function (d) { return vfTimelineScale(d.start); },
"y1" : function (d,i) { return (height - 50 + 15 * i); },
"x2" : function (d) { return vfTimelineScale(d.stop); },
"y2" : function (d,i) { return (height - 50 + 15 * i); },
})
You are already positioning through the transformation, I presume you don't need the lines to be 450 px offset vertically from the text. Change the formula to something such as...
"x1" : function (d) { return vfTimelineScale(d.start); },
"y1" : 50,
"x2" : function (d) { return vfTimelineScale(d.stop); },
"y2" : 50
You will need to change the 50 value to suit your needs of course.
I have built a graph with D3.js based on this example of a force-directed graph, but rather than having straight lines between the nodes I am creating curved lines using SVG path elements. The data structure for an individual link includes a source and target which represent the nodes to which the link is connected. Also the link data structure also contains a line element which contains an array of points defining the path. The data structure for a link looks like this:
{
id:4,
type:"link",
fixed:true,
source: {
id:1,
name: "A",
type:"node",
x:226,
y:190,
fixed:1,
index:0,
weight:1,
},
target: {
id:2,
name: "B",
type:"node",
x:910,
y:85,
fixed:1,
index:1,
weight:1,
},
line:[{x:387, y:69}, {x:541.5, y:179}, {x:696, y:179}]
}
Now in my on tick event handler I have the current x and y co-ordinates for for nodes A and B by means of the references d.source.x, d.source.y and d.target.x, d.target.y. I also have the initial position of node A (first element of d.line) and of node B (last element of d.line). What I am trying to do is to recalculate the points in between the first and last points based on the changes made to the positions of nodes A and B.
Here is my on tick event handler:
force.on("tick", function() {
svg.selectAll("g.node").attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
svg.selectAll("g.link .line").attr("d", function(d) {
var xOffset = 0, // Want to calculate this
yOffset = 0; // Want to calculate this
var line = [ ];
for (var i=0; i<d.line.length; i++) {
if (i==0) {
line[line.length] = { x : d.source.x, y: d.source.y }
}
else if (i==d.line.length-1) {
line[line.length] = { x : d.target.x, y: d.target.y }
}
else {
line[line.length] = { x: d.line[i].x + xOffset, y: d.line[i].y + yOffset }
}
}
return self.lineGenerator(line);
})
});
Not offsetting the x and y co-ordinates for the points in the middle of the path results in these points staying static when nodes A or B are dragged. Can anyone explain how to go about calculating xOffset and yOffset so that the path will correctly move/rotate when the nodes are dragged? Or if anyone knows of a better (read "easier"!) way of accomplishing this with D3.js then please let me know.
UPDATE:
Here is my line generator code in the chart constructor:
this.lineGenerator = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("cardinal");
Let me explain the problem with some images, hopefully they will make the problem that I am having clearer. It is not a matter of the lines being linear, my lines are curved by the line generator, but when I drag a node, I want the curve to drag as well. So all the points on the line must update to reflect the change made to the position of the node. In other words the path must not get distorted by the nodes being moved.
So, in pictures; given this situation:
If I drag node B down and to the left, I want to see something like this:
(Note that this was rotated with image editting software, hence the background is also rotated)
Instead I am getting something like this:
Note how the one intermediary point in the line has stayed static when node B was moved, causing the curve to distort. This is what I am trying to avoid happening.
Hopefully this makes the problem clearer.
One often wants to make a set of objects in Raphael draggable, but using .transform() to do so can be maddening. Say you start like this:
var paper = Raphael(0, 0, 500, 500);
var set = paper.set();
set.push(paper.circle(100,100,35), paper.circle(150,150,15));
set.attr("fill", "orange");
set.data("myset", set);
set.drag(
function(dx, dy, x, y, e) {
this.data('myset').transform("T" + dx + "," + dy);
},
function(x, y, e) {},
function(e) {}
);
If you try this out, you see it works once. But if you drag, stop, then drag again, it resets the position to 0,0 relative to the original position, as you'd expect from .transform(). No good.
A variant of this question has been touched on here, and the respondent suggested prepending transforms with "...". That's all fine and good, but for two things:
you still have to track previous position, since you don't want to
translate by (dx,dy) on every call of dragmove, which will send
the objects flying off the screen.
I worry about creating a monster
transform if an object is dragged many times. (Though maybe I
shouldn't.)
My tentative solution is to track the offset from the original positioning in another key/value pair, like so:
var paper = Raphael(0, 0, 500, 500);
var set = paper.set();
set.push(
paper.circle(100,100,35),
paper.circle(150,150,15)
);
set.attr("fill", "orange");
set.data("myset", set);
set.data("position", [0,0]);
var current_position = [0,0];
set.drag(
function(dx, dy, x, y, e) {
this.data('myset').transform("T" + (this.data("position")[0] + dx) + "," + (this.data("position")[1] + dy));
current_position = [dx,dy];
},
function(x, y, e) {
},
function(e) {
this.data('myset').data("position", [
this.data("position")[0] += current_position[0],
this.data("position")[1] += current_position[1]
]);
}
);
You can see it in action here.
It works, but it feels incredibly sloppy. I must be missing something obvious, right?
My answer is similar to your last variant:
var onmove = function (dx,dy){
this.transform(this.default_transform+'T'+dx+','+dy);
},
onstart = function (){
this.default_transform = this.transform();
},
onend = function(){
this.default_transform = this.transform();
};
set.drag(onmove, onstart, onend);
Don't worry it won't create a long long line of transformations because Raphael converts everything to one whole matrix transformation so it doesn't build up each time you move an object.