Right now I recalculate all the points which I select on the heatmap. I am relatively new at D3 and don't particularly understand how the zoom behavior works. Can someone help? This is the chunk of code that allows me to zoom into the heatmap with self selected Area:
function selectArea(area,svg,dataset,num,oldxStart,oldyStart) {
svg
.attr("width",width)
.attr("height",height)
var cols = dataset.dim[1]; //x
var rows = dataset.dim[0]; //y
var zoomDat = [];
var newxLab=[];
var newyLab = [];
//Makes the selection rectangles
area
.on("mousedown", function() {
var e = this,
origin = d3.mouse(e),
rect = svg
.append("rect")
.attr("class", "zoom");
origin[0] = Math.max(0, Math.min(width, origin[0]));
origin[1] = Math.max(0, Math.min(height, origin[1]));
d3.select('body')
.on("mousemove.zoomRect", function() {
var m = d3.mouse(e);
m[0] = Math.max(0, Math.min(width, m[0]));
m[1] = Math.max(0, Math.min(height, m[1]));
rect.attr("x", Math.min(origin[0], m[0]))
.attr("y", Math.min(origin[1], m[1]))
.attr("width", Math.abs(m[0] - origin[0]))
.attr("height", Math.abs(m[1] - origin[1]));
})
.on("mouseup.zoomRect", function() {
var m = d3.mouse(e);
m[0] = Math.max(0, Math.min(width, m[0]));
m[1] = Math.max(0, Math.min(height, m[1]));
//x,y Start/Finish for the selection of data => Can draw box the other way, and still work.
var xStart = Math.min(Math.floor(origin[0]/xScale(1)), Math.floor(m[0]/xScale(1)))
var xFinish = Math.max(Math.floor(m[0]/xScale(1)), Math.floor(origin[0]/xScale(1)))+1
var yStart = Math.min(Math.floor(origin[1]/yScale(1)), Math.floor(m[1]/yScale(1)))
var yFinish =Math.max(Math.floor(m[1]/yScale(1)), Math.floor(origin[1]/yScale(1)))+1
var newcolMeta = [];
var newrowMeta = [];
var newyDend = [];
var newxDend = [];
//If the Y dendrogram is selected, make the X dendrogram undefined
//because I dont want the x dendrogram to change
if (num==1) {
xStart = 0;
xFinish = cols
//If the X dendrogram is selected, make the y dendrogram undefined
//because I dont want the y dendrogram to change
} else if (num==2) {
yStart = 0;
yFinish = rows
}
//Get the data selected and send it back to heatmapgrid
for (i = xStart; i<xFinish; i++) {
newxLab.push(dataset.cols[i]);
if (data.cols!=null) { //If there is no column clustering
newxDend.push(d3.select(".ends_X"+i).attr("id"))
}
}
for (i=yStart;i<yFinish; i++) {
newyLab.push(dataset.rows[i]);
if (data.rows !=null) { //If there is no row clustering
newyDend.push(d3.select(".ends_Y"+i).attr("id"))
}
for (j=xStart; j<xFinish; j++) {
zoomDat.push(dataset.data[i*cols+j]);
}
}
//Get the Metadata -> If there is more than one line of annotations, the data is in different places, just like the grid
if (colMeta !=null) {
for (i = 0; i<colHead.length; i++) {
for (j = xStart; j<xFinish; j++) {
newcolMeta.push(colMeta[i*cols+j])
}
}
colMeta = newcolMeta
}
if (rowMeta != null) {
for (i =0; i<rowHead.length; i++) {
for (j =yStart; j<yFinish; j++) {
newrowMeta.push(rowMeta[i*rows+j])
}
}
rowMeta = newrowMeta
}
//Set new parameters based on selected data
dataset.dim[1] = newxLab.length;
dataset.dim[0] = newyLab.length;
dataset.rows = newyLab;
dataset.cols = newxLab;
dataset.data = zoomDat;
colAnnote.data = colMeta;
rowAnnote.data = rowMeta;
//Changes the margin, if the dimensions are small enough
if (dataset.dim[0] <=100) {
marginleft=100;
}
if (dataset.dim[1] <=300) {
margintop = 130;
}
xGlobal.range([0,width-marginleft])
yGlobal.range([0,height-margintop])
var x = xGlobal(1);
var y = yGlobal(1);
//This slows down the program (Remove())
d3.selectAll('.rootDend').remove();
oldxStart += xStart
oldyStart += yStart
//olsxStart + xStart because the dendrogram is translated
heatmapGrid(el.select('svg.colormap'),dataset,oldxStart,oldyStart);
//New Vertical dendrogram
dendrogram(el.select('svg.rowDend'), data.rows, false, 250, height-margintop,newyDend,oldyStart,y);
//New Horizontal dendrogram
dendrogram(el.select('svg.colDend'), data.cols, true, width-marginleft, 250,newxDend,oldxStart,x);
//New annotation bar, if no annotations, don't do this
drawAnnotate(el.select('svg.colAnnote'), colAnnote,false, width-marginleft,10);
drawAnnotate(el.select('svg.rowAnnote'),rowAnnote,true,10,height-margintop);
//zoomDat = [];
//remove blue select rectangle
rect.remove();
});
});
Related
Assume data parsed from a tsv like so:
tsvData.then(function(rawData) {
var data = rawData.map(function(d) {
return {year:+d.year, age3:+d.age3*100, age1:+d.age1*100, age2:+d.age2*100, age4:+d.age4*100, age5:+d.age5*100, age6:+d.age6*100, age7:+d.age7*100}
});
And assume that we intend to create a square matrix representing these percentages (1 rect = 1%, each age group = different color):
var maxColumn = 10;
var colorMap = {
0:"#003366",
1:"#366092",
2:"#4f81b9",
3:"#95b3d7",
4:"#b8cce4",
5:"#e7eef8",
6:"#f6d18b"
};
graphGroup.selectAll('rect')
.data(data1)
.enter()
.append('rect')
.attr('x', function(d, i) {
return (i % maxColumn) * 30
})
.attr('y', function(d, i) {
return ~~((i / maxColumn) % maxColumn) * 30
})
.attr('width', 20)
.attr('height', 20)
.style('fill', function(d) {
return colorMap[d];
});
We would need some way to transform data so that it has 100 items, and those 100 items need to match the proportions of data. For simplicity let's just evaluate at data[0]. Here is my solution:
var data1 = d3.range(100).map(function(d,i) {
var age1 = data[0].age1;
var age2 = data[0].age2;
var age3 = data[0].age3;
var age4 = data[0].age4;
var age5 = data[0].age5;
var age6 = data[0].age6;
var age7 = data[0].age7;
if (i<age1) {
return 0;
} else if (i>age1 && i<(age1+age2)) {
return 1;
} else if (i>(age1+age2) && i<(age1+age2+age3)) {
return 2;
} else if (i>(age1+age2+age3) && i<(age1+age2+age3+age4)) {
return 3;
} else if (i>(age1+age2+age3+age4) && i<(age1+age2+age3+age4+age5)) {
return 4;
} else if (i>(age1+age2+age3+age4+age5) && i<(age1+age2+age3+age4+age5+age6)) {
return 5;
} else if (i>(age1+age2+age3+age4+age5+age6) && i<(age1+age2+age3+age4+age5+age6+age7)) {
return 6;
}
});
It kind of works, but it's not scalable at all, but I can't imagine how else one transforms the arrays. Just so we are clear, by transforming arrays I mean we start with:
data = [
{'age1':33.66, 'age2':14.87, 'age3':18, 'age4':14, 'age5':11, 'age6':5, 'age7':3}
];
and end with:
data1 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1...];
var margins = {top:100, bottom:300, left:100, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
//var tsvData = d3.tsv('so-demo3.tsv');
var maxColumn = 10;
//tsvData.then(function(rawData) {
/*
var data = rawData.map(function(d) {
return {year:+d.year, age3:+d.age3*100, age1:+d.age1*100, age2:+d.age2*100, age4:+d.age4*100, age5:+d.age5*100, age6:+d.age6*100, age7:+d.age7*100}
});
*/
var data = [
{'age1':33.66, 'age2':14.87, 'age3':18, 'age4':14, 'age5':11, 'age6':5, 'age7':3}
];
var data1 = d3.range(100).map(function(d,i) {
var age1 = data[0].age1;
var age2 = data[0].age2;
var age3 = data[0].age3;
var age4 = data[0].age4;
var age5 = data[0].age5;
var age6 = data[0].age6;
var age7 = data[0].age7;
if (i<age1) {
return 0;
} else if (i>age1 && i<(age1+age2)) {
return 1;
} else if (i>(age1+age2) && i<(age1+age2+age3)) {
return 2;
} else if (i>(age1+age2+age3) && i<(age1+age2+age3+age4)) {
return 3;
} else if (i>(age1+age2+age3+age4) && i<(age1+age2+age3+age4+age5)) {
return 4;
} else if (i>(age1+age2+age3+age4+age5) && i<(age1+age2+age3+age4+age5+age6)) {
return 5;
} else if (i>(age1+age2+age3+age4+age5+age6) && i<(age1+age2+age3+age4+age5+age6+age7)) {
return 6;
}
});
var colorMap = {
0:"#003366",
1:"#366092",
2:"#4f81b9",
3:"#95b3d7",
4:"#b8cce4",
5:"#e7eef8",
6:"#f6d18b"
};
graphGroup.selectAll('rect')
.data(data1)
.enter()
.append('rect')
.attr('x', function(d, i) {
return (i % maxColumn) * 30
})
.attr('y', function(d, i) {
return ~~((i / maxColumn) % maxColumn) * 30
})
.attr('width', 20)
.attr('height', 20)
.style('fill', function(d) {
return colorMap[d];
});
//})
<script src="https://d3js.org/d3.v5.min.js"></script>
Question
Does d3 offer something more sublime than my brute force approach to achieve the desired array?
I suppose you want to create a pictogram. If that's correct, this mix of reduce and map can easily create the individual arrays you want, regardless the number of properties in the datum object:
data.forEach(obj => {
modifiedData.push(Object.keys(obj).reduce((acc, _, i) =>
acc.concat(d3.range(Math.round(obj["age" + (i + 1)])).map(() => i)), []))
});
Pay attention to the fact that, since you cannot guarantee the order of the properties in an object, the reduce uses the bracket notation with "age" + index to correctly set the numbers 0, 1, 2, etc.. to age1, age2, age3, etc...
Here is the demo:
const data = [{
'age1': 33.66,
'age2': 14.87,
'age3': 18,
'age4': 14,
'age5': 11,
'age6': 5,
'age7': 3
}];
const modifiedData = [];
data.forEach(obj => {
modifiedData.push(Object.keys(obj).reduce((acc, _, i) =>
acc.concat(d3.range(Math.round(obj["age" + (i + 1)])).map(() => i)), []))
});
console.log(modifiedData)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
By the way, your current data sample does not sum 100.
There are obviously many solutions to your problem. However, since you explicitly asked for a D3 solution, this is my take on it:
// STEPS 1-5:
const data1 = d3.merge( // 5. Merge all sub-array into one.
d3.range(1,8) // 1. For every age group 1-7...
.map(d => d3.range( // 2. create an array...
Math.round(data[0][`age${d}`]) // 3. given a length as per the age property...
).map(() => d-1) // 4. populated with the value from 2.
)
);
Have a look at the following snippet for a working demo:
var margins = {top:100, bottom:300, left:100, right:100};
var height = 600;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
//var tsvData = d3.tsv('so-demo3.tsv');
var maxColumn = 10;
//tsvData.then(function(rawData) {
/*
var data = rawData.map(function(d) {
return {year:+d.year, age3:+d.age3*100, age1:+d.age1*100, age2:+d.age2*100, age4:+d.age4*100, age5:+d.age5*100, age6:+d.age6*100, age7:+d.age7*100}
});
*/
var data = [
{'age1':33.66, 'age2':14.87, 'age3':18, 'age4':14, 'age5':11, 'age6':5, 'age7':3}
];
const data1 = d3.merge( // 5. Merge all sub-array into one.
d3.range(1,8) // 1. For every age group 1-7...
.map(d => d3.range( // 2. create an array...
Math.round(data[0][`age${d}`]) // 3. given a length as per the age property...
).map(() => d-1) // 4. populated with the value from 2.
)
);
var colorMap = {
0:"#003366",
1:"#366092",
2:"#4f81b9",
3:"#95b3d7",
4:"#b8cce4",
5:"#e7eef8",
6:"#f6d18b"
};
graphGroup.selectAll('rect')
.data(data1)
.enter()
.append('rect')
.attr('x', function(d, i) {
return (i % maxColumn) * 30
})
.attr('y', function(d, i) {
return ~~((i / maxColumn) % maxColumn) * 30
})
.attr('width', 20)
.attr('height', 20)
.style('fill', function(d) {
return colorMap[d];
});
//})
<script src="https://d3js.org/d3.v5.min.js"></script>
Your goal is to represent the single values in the data array as discrete values in an a*a matrix.
So my suggestion (in pseudo-code) would be:
Scale values so the total sum is a*a (in our case 10*10 = 100):
sumAge = age1 + age2
.... data.forEach(age1/sumAge*100)
Now paint the chart:
counter = 0
data.forEach age
ageCounter = 0;
while agecounter < age
ageCounter ++
set color
while counter < 100
counter ++
draw rectangle at x,y
with x = 100 mod a
y = 100 div a
You scale the values - you loop stepwise through each data item (to set the color), and have an inner loop that is executed as often as you need to draw squares.
I need to show data using stacked circles positioned equally spaced. New to d3.js. Searched for reference but no where to be found.
d3.json("js/data/circlechart.json", function(error, jsonData) {
if (error) return console.warn(error);
var len = jsonData.length;
var color = ["#80a1e6", "#5c7dc1" , "#4062a8", "#345499" , "#274582"];
var objWidth = document.getElementById('circleChart').offsetWidth;
var cwidth = objWidth ;
var xc = cwidth / 2 ;
var xy = (cwidth / 2 ) - 5 ;
var r = xc + 20 ;
xc = xc- 20;
var ty = 25;
var svgContainer = d3.select(element[0]).append("svg").data(jsonData);
svgContainer.attr("width", cwidth).attr("height", cwidth);
for(var i =0;i<jsonData.length;i++){
var r = r - 30;
var circle = svgContainer.append("g");
circle.append("circle").attr("cx", xc).attr("cy", xy).attr("r", r).attr("fill", color[i]);
circle.append('text').attr('x', xc)
.attr('y', ty).text(jsonData[i].name)
.attr("fill","#fff")
.attr("style","transform: translateX(-20px)")
.attr("class","labelName");
ty+=31;
}
});
and my json file data is
[ { "name":"Category 1", "count":"20" },
{ "name":"Category 2", "count":"40" },
{ "name":"Category 3", "count":"52" } ]
I'm trying to make as below
You should pass your jsonData to data() function like
var circleContainer = svgSelection.selectAll("g")
.data(jsonData)
.enter()
.append("g");
and then operate with this container:
circleContainer.append('circle')
.attr('cx', 100)
.attr('cy', 100)
.attr('r', function(d) {return d.count+'px'})
.style('fill', 'none').style('stroke', 'black');
here is fiddle for you
I have a snap svg animation which animates a bunch of circles, and draws a line between them if they are within a certain proximity of each other. However, I realize that there is a lot of optimizing I can do, but I'm not exactly sure how to do it. I feel like it would be useful to
have a good example of proximity detection in snap
have some more information on optimizing animations in snap svg. It hasn't been easy to find.
Here is a working example of the animation:
http://jsfiddle.net/heaversm/sbj4W/1/
and here are the things I believe can be optimized:
Each circle calls its own animation function - the circles have all been added to a group, and I'm guessing there is a way to apply random motion to all members of a group that is more performant than call a function for each and every element within the group.
for (var i=0; i<this.drawingConfig.circles.amount;i++){
...
this.animateSingle(circleShape);
}
The proximity function is awkward - for each circle, for each update cycle, I have to loop through an array of all the other circles and find out if the X and Y coordinates are close enough to draw a line to. Plus, that means you're getting duplicate lines, because each circle will draw a line to its neighbors, instead of having a single shared line between the two.
for (var i=0;i<circles.length;i++){
var nextCircle = circles[i].node;
var nextCircleX = nextCircle.cx.baseVal.value;
var distance = Math.abs(nextCircleX-thisCircleX);
var proximity = mainModule.drawingConfig.circles.proximity;
if (distance < proximity){
var nextCircleY = nextCircle.cy.baseVal.value;
var thisCircleY = shape.node.cy.baseVal.value;
var distanceY = Math.abs(nextCircleY - thisCircleY);
if (distanceY < proximity){
var line = mainModule.s.line(thisCircleX, thisCircleY, nextCircleX, nextCircleY).attr({stroke: '#a6a8ab', strokeWidth: '1px'});
mainModule.drawingConfig.circles.circleGroup.add(line);
}
}
}
Correspondingly, I each circle's animation function clears all the lines on the screen. Ideally all the circles would be sharing one update function, and in that function, you'd clear the lines.
Snap.animate(startX, animX, function (val) {
var lines = Snap.selectAll('line');
lines.remove();
...
}, mainModule.drawingConfig.circles.animTime);
Right now, I can tell the renderer can't keep up with all of the various animations / loops. Any help optimizing the above things (or anything else you can see that I'm doing weird, would be greatly appreciated.
I cleaned this up by running only one animation loop, on a timer every 10ms, and animated the position of the circles by just giving them a slope and, each update, continuing them further along that slope. You can see an updated fiddle here:
http://jsfiddle.net/heaversm/fJ6fj/
var mainModule = {
s: Snap("#svg"),
drawingConfig: {
circles: {
amount: 20,
sizeMin: 10,
sizeMax: 20,
proximity: 100,
circleGroup: null,
circleArray: [],
animTime: 2000
},
canvas: {
width: 800,
height: 600
}
},
init: function(){
//this.sizeCanvas();
this.makeCircles();
},
sizeCanvas: function(){
$('#svg').width(800).height(600);
},
makeCircles: function(){
this.drawingConfig.circles.circleGroup = this.s.g();
for (var i=0; i<this.drawingConfig.circles.amount;i++){
var circleX = this.randomNumber(0, this.drawingConfig.canvas.width);
var circleY = this.randomNumber(0, this.drawingConfig.canvas.height);
var circleRadius = this.randomNumber(this.drawingConfig.circles.sizeMin,this.drawingConfig.circles.sizeMax);
var circleFill = '#'+Math.floor(Math.random()*16777215).toString(16);
var circleShape = this.s.circle(circleX, circleY, circleRadius);
circleShape.attr({
fill: circleFill
});
this.drawingConfig.circles.circleGroup.add(circleShape);
var circleIncline = this.setIncline();
var circleObj = { incline: circleIncline, shape: circleShape };
this.drawingConfig.circles.circleArray.push(circleObj);
}
this.update();
},
setIncline: function(){
return { incX: this.randomNumber(-5,5), incY: this.randomNumber(-5,5) }
},
update: function(){
var lines = Snap.selectAll('line');
lines.remove();
for (var i=0; i<this.drawingConfig.circles.amount; i++){
var circle = this.drawingConfig.circles.circleArray[i];
var circleX = circle.shape.node.cx.animVal.value;
var circleY = circle.shape.node.cy.animVal.value;
this.move(circle,circleX,circleY);
for (var j=0;j<i;j++){
if (i != j){
var circle2 = this.drawingConfig.circles.circleArray[j];
var circle2X = circle2.shape.node.cx.animVal.value;
var circle2Y = circle2.shape.node.cy.animVal.value;
var dist = mainModule.distance(circleX,circleY,circle2X,circle2Y);
if (dist <= mainModule.drawingConfig.circles.proximity){ //
var lineWeight = 10/dist;
var line = mainModule.s.line(circleX, circleY, circle2X, circle2Y).attr({stroke: '#a6a8ab', strokeWidth: '1px'});
}
if (dist <= 10) { //collision
circle.incline = mainModule.setIncline();
circle2.incline = mainModule.setIncline();
}
}
}
}
setTimeout(function(){ mainModule.update(); },10);
},
distance: function(circleX,circleY,circle2X,circle2Y){
var distX = circle2X - circleX;
var distY = circle2Y - circleY;
distX = distX*distX;
distY = distY*distY;
return Math.sqrt(distX + distY);
},
move: function(circle,curX,curY){
if (curX > this.drawingConfig.canvas.width || curX < 0) {
circle.incline.incX = -circle.incline.incX;
}
if (curY > this.drawingConfig.canvas.height || curY < 0) {
circle.incline.incY = -circle.incline.incY;
}
curX = curX + circle.incline.incX;
curY = curY + circle.incline.incY;
if (curX > this.drawingConfig.canvas.width) {
curX = this.drawingConfig.canvas.width;
circle.incline = this.setIncline();
} else if (curX < 0) {
curX = 0;
circle.incline = this.setIncline();
}
if (curY > this.drawingConfig.canvas.height) {
curY = this.drawingConfig.canvas.height;
circle.incline = this.setIncline();
} else if (curY < 0) {
curY = 0;
circle.incline = this.setIncline();
}
circle.shape.attr({ cx: curX, cy: curY });
},
randomNumber: function(min,max){
return Math.floor(Math.random()*(max-min+1)+min);
},
getBounds: function(shape){
shapeBox = shape.node.getBoundingClientRect();
}
}
mainModule.init();
Basically I want to scroll a object along path. I've seen several threads looking for similar solution not using paper.js but i was wondering if this possible with paper.js. Or can someone give me a working jsfiddle of object follow svg curve because I couldn't get any thing to work. I ultimately want to have a chain of divs follow the path.
// vars
var point1 = [0, 100];
var point2 = [120, 100];
var point3 = [120, 150];
// draw the line
var path = new Path();
path.add(new Point(point1), new Point(point2), new Point(point3));
path.strokeColor = "#FFF";
path.closed = true;
// draw the circle
var circle = new Path.Circle(0,100,4);
circle.strokeColor = "#FFF";
// target to move to
var target = point2;
// how many frame does it take to reach a target
var steps = 200;
// defined vars for onFrame
var dX = 0;
var dY = 0;
// position circle on path
circle.position.x = target[0];
circle.position.y = target[1];
function onFrame(event) {
//check if cricle reached its target
if (Math.round(circle.position.x) == target[0] && Math.round(circle.position.y) == target[1]) {
switch(target) {
case point1:
target = point2;
break;
case point2:
target = point3;
break;
case point3:
target = point1;
break;
}
// calculate the dX and dY
dX = (target[0] - circle.position.x)/steps;
dY = (target[1] - circle.position.y)/steps;
}
// do the movement
//circle.position.x += dX;
//circle.position.y += dY;
}
Here is the jsfiddle:
http://jsfiddle.net/J9xgY/12/
Thanks!
You can find a point along a path with path.getPointAt(offset) where offset is measured in points along the length of the path. If you can calculate the position of a slider along its track, you can multiply that by the path.length to get an offset.
You can do this with an HTML slider or with a canvas element, as shown here:
// vars
var point1 = [0, 100];
var point2 = [120, 100];
var point3 = [120, 150];
// draw the line
var path = new Path();
path.add(new Point(point1), new Point(point2), new Point(point3));
path.strokeColor = "#FFF";
path.closed = true;
// draw the circle
var circle = new Path.Circle(0,100,4);
circle.strokeColor = "#FFF";
// slider
var sliderLine = new Path(new Point(10,30.5), new Point(210, 30.5));
sliderLine.strokeColor = '#FFF';
var sliderKnob = new Path.Circle(new Point(10,30.5), 5);
sliderKnob.fillColor = '#FFF';
var sliderHit = false;
function onMouseDown(event) {
if (event.item == sliderKnob) sliderHit = true;
}
function onMouseDrag(event) {
if (sliderHit === true) {
if (event.point.x > 10 && event.point.x < 210) {
sliderKnob.position.x = event.point.x;
}
else if (event.point.x < 11) {
sliderKnob.position.x = 10;
}
else if (event.point.x > 209) {
sliderKnob.position.x = 210;
}
// Get offset and set circle position
var percent = ( sliderKnob.position.x - 10 ) / 200;
circle.position = path.getPointAt(path.length * percent);
}
}
function onMouseUp(event) {
sliderHit = false;
}
jsfiddle: http://jsfiddle.net/J9xgY/13/
Click and drag the filled circle along the line to move the circle along the triangle.
I am working on a Chrome Extension that crawls websites and finds certain types of links and then once the list of links has been populated after a predetermined number of hops, the search results are displayed using a D3 visualization (I am using the Forced Layout).
Problem: In about a couple of hops, I have too many links/nodes (more than 500 on most occasions) and so the graph looks intimidating and impossible to read, since all the nodes get cluttered in the limited space.
How can I increase the inter-node distance and make the graph more readable.
How do I give different colors to my nodes depending on their category/class?
Here are a fewof snapshots of the search results:
As is evident from the snapshots, things get pretty ugly. The code for my visualization script is given below, to give an idea of how I am implementing the graph. (I am very, very new to D3)
document.addEventListener('DOMContentLoaded', function () {
document.querySelector('#tab_5_contents').addEventListener('click', drawVisual);
//drawVisual();
});
function drawVisual(){
var w = 960, h = 500;
//var w = 1024, h = 768;
var labelDistance = 0;
var vis = d3.select("#tab_5_contents").append("svg:svg").attr("width", w).attr("height", h);
var nodes = [];
var labelAnchors = [];
var labelAnchorLinks = [];
var links = [];
for(var i = 0; i < QueuedORG.length; i++)
{
var nodeExists = 0;
//check to see if a node for the current url has already been created. If yes, do not create a new node
for(var j = 0; j < nodes.length; j++)
{
if(QueuedORG[i].url == nodes[j].label)
nodeExists = 1;
}
if (nodeExists == 0)
{
var node = {
label : QueuedORG[i].url
};
nodes.push(node);
labelAnchors.push({
node : node
});
labelAnchors.push({
node : node
});
}
};
for(var i=0;i<nodes.length; i++)
{
console.log("node i:"+i+nodes[i]+"\n");
console.log("labelAnchor i:"+i+labelAnchors[i]+"\n");
}
//To create links for connecting nodes
for(var i = 0; i < QueuedORG.length; i++)
{
var srcIndx = 0, tgtIndx = 0;
for(var j = 0; j < nodes.length; j++)
{
if( QueuedORG[i].url == nodes[j].label ) //to find the node number for the current url
{
srcIndx = j;
}
if( QueuedORG[i].parentURL == nodes[j].label ) //to find the node number for the parent url
{
tgtIndx = j;
}
}
//console.log("src:"+srcIndx+" tgt:"+tgtIndx);
//connecting the current url's node to the parent url's node
links.push({
source : srcIndx,
target : tgtIndx,
weight : 1,
});
labelAnchorLinks.push({
source : i * 2,
target : i * 2 + 1,
weight : 1
});
};
var force = d3.layout.force().size([w, h]).nodes(nodes).links(links).gravity(1).linkDistance(50).charge(-3000).linkStrength(function(x) {
return x.weight * 10
});
force.start();
var force2 = d3.layout.force().nodes(labelAnchors).links(labelAnchorLinks).gravity(0).linkDistance(0).linkStrength(8).charge(-100).size([w, h]);
force2.start();
var link = vis.selectAll("line.link").data(links).enter().append("svg:line").attr("class", "link").style("stroke", "#CCC");
var node = vis.selectAll("g.node").data(force.nodes()).enter().append("svg:g").attr("class", "node");
node.append("svg:circle").attr("r", 5).style("fill", "#555").style("stroke", "#FFF").style("stroke-width", 3);
node.call(force.drag);
var anchorLink = vis.selectAll("line.anchorLink").data(labelAnchorLinks)//.enter().append("svg:line").attr("class", "anchorLink").style("stroke", "#999");
var anchorNode = vis.selectAll("g.anchorNode").data(force2.nodes()).enter().append("svg:g").attr("class", "anchorNode");
anchorNode.append("svg:circle").attr("r", 0).style("fill", "#FFF");
anchorNode.append("svg:text").text(function(d, i) {
return i % 2 == 0 ? "" : d.node.label
}).style("fill", "#555").style("font-family", "Arial").style("font-size", 12);
var updateLink = function() {
this.attr("x1", function(d) {
return d.source.x;
}).attr("y1", function(d) {
return d.source.y;
}).attr("x2", function(d) {
return d.target.x;
}).attr("y2", function(d) {
return d.target.y;
});
}
var updateNode = function() {
this.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
force.on("tick", function() {
force2.start();
node.call(updateNode);
anchorNode.each(function(d, i) {
if(i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.childNodes[1].getBBox();
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 5;
this.childNodes[1].setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
anchorNode.call(updateNode);
link.call(updateLink);
anchorLink.call(updateLink);
});
}
The array QueuedORG in the above code acts as my dataset for all links that are being shown in the graph. Each entry in the array looks like this:
QueuedORG.push({url: currentURL, level: 1, parentURL: sourceURL, visited: 0});
Can someone please guide me to be able to increase the node repulsion and color code them? Thanks :)
You can set the distance between nodes by using the linkDistance() function of the force layout. This can be a function to allow you to set different distances for different links. Note that these distances are only suggestions to the force layout, so you won't get exactly what you ask for.
You could set this in a similar way to how you're setting the linkStrength at the moment:
force.linkDistance(function(d) {
return d.weight * 10;
});
It looks like you're already coloring the circles (node.append("svg:circle").style("fill", "#555")), which should work. Alternatively, you can assign CSS classes to them and then use that for the color.
circle.class1 {
fill: red;
stroke: black;
}
node.append("circle")
.attr("class", function(d) {
if(d.something == 1) { return "class1"; }
// etc
});