Related
I have an HTML page on which I am trying to display a sky plot. For the display, I am using the D3 library in JS.
Here is my code:
var deg2rad = Math.PI/180;
var width = 400, height = 350, radius = Math.min(width, height) / 2 - 30;
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var r = d3.scale.linear()
.domain([90, 0])
.range([0, radius]);
var line = d3.svg.line.radial()
.radius(function(d) {return r(d[1]);})
.angle(function(d) {return -d[0] + Math.PI / 2;});
var gr = null;
createSkyplot();
function createSkyplot(){
gr = svg.append("g")
.attr("class", "r axis")
.selectAll("g")
.data(r.ticks(5))
.enter().append("g");
gr.append("circle").attr("r", r).style('fill', 'white');
gr.append("text")
.attr("y", function(d) { return -r(d) - 4; })
.attr("transform", "rotate(20)")
.style("text-anchor", "middle")
.style('fill', 'blue')
.text(function(d) { return d;});
var ga = svg.append("g")
.attr("class", "a axis")
.selectAll("g")
.data(d3.range(0, 360, 45))
.enter().append("g")
.attr("transform", function(d) {return "rotate(" + (d - 90) + ")";});
ga.append("line").attr("x2", radius).style('stroke', 'black').style('stroke-dasharray', '1,8');
ga.append("text")
.attr("x", radius + 6)
.attr("dy", ".35em")
.style("text-anchor", function(d) { return d < 360 && d > 90 ? "end" : null; })
.attr("transform", function(d) { return d < 360 && d > 90 ? "rotate(180 " + (radius + 3) + ",0)" : null; })
.text(function(d) { return d + "°"; });
}
function updateSkyPlot(d){
var pos = [];
var inview = d.inView;
for (var elem in inview){
if (inview.hasOwnProperty(elem)) {
console.log(inview[elem].data[0]);
// the azimuth should be in radians and substracted from (Math.PI/2)
var d = [(Math.PI/2) - inview[elem].data[2]*deg2rad, inview[elem].data[1]];
pos.push(d);
}
}
var r = d3.scale.linear()
.domain([90, 0])
.range([0, radius]);
var line = d3.svg.line.radial()
.radius(function(d) {return r(d[1]);})
.angle(function(d) {return -d[0] + Math.PI / 2;});
var color = d3.scale.category20();
svg.selectAll('circle').remove();
gr.append("circle").attr("r", r).style('fill', 'white');
svg.selectAll("point").data(pos).enter().append("circle").attr("class", "point")
.attr("transform", function(d) {
var coors = line([d]).slice(1).slice(0, -1);
return "translate(" + coors + ")"})
.attr("r", 8)
.attr("fill",function(d,i){return color(i);});
}
I use the updateSkyPlot(d) method to update the plot. The input for this method is a JSON object of the next type:
{"type" : "data", "inView" : {[prn, elv, azi], [prn, elv, azi] ..}}.
When I run this code in the HTML page I am able to produce the next plot:
My question is, what would be the best way to add the prn number inside the corresponding circles?
What should I add to the code to make it work?
Any help would be appreciated, thank you.
Here is Full HTML snippet:
<!DOCTYPE html>
<html>
<head>
<meta charset='ISO-8859-1'>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body style='background-color:lightgray'>
<div id="chart" style='width: 400px; height: 400px; padding-left: 5px; padding-bottom: 5px;'></div>
<script>
var deg2rad = Math.PI/180;
var width = 400, height = 350, radius = Math.min(width, height) / 2 - 30;
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var r = d3.scale.linear()
.domain([90, 0])
.range([0, radius]);
var line = d3.svg.line.radial()
.radius(function(d) {return r(d[1]);})
.angle(function(d) {return -d[0] + Math.PI / 2;});
var gr = null;
createSkyplot();
var json = {"type" : "GSV",
"inView" : [{"data" : [1, 45, 90]},
{"data" : [3, 70, 225]}]
};
updateSkyPlot(json);
function createSkyplot(){
//////////////////////
gr = svg.append("g")
.attr("class", "r axis")
.selectAll("g")
.data(r.ticks(5))
.enter().append("g");
gr.append("circle").attr("r", r).style('fill', 'white');
gr.append("text")
.attr("y", function(d) { return -r(d) - 4; })
.attr("transform", "rotate(20)")
.style("text-anchor", "middle")
.style('fill', 'blue')
.text(function(d) { return d;});
/////////////////////
/////////////////////
var ga = svg.append("g")
.attr("class", "a axis")
.selectAll("g")
.data(d3.range(0, 360, 45))
.enter().append("g")
.attr("transform", function(d) {return "rotate(" + (d - 90) + ")";});
ga.append("line").attr("x2", radius).style('stroke', 'black').style('stroke-dasharray', '1,8');
ga.append("text")
.attr("x", radius + 6)
.attr("dy", ".35em")
.style("text-anchor", function(d) { return d < 360 && d > 90 ? "end" : null; })
.attr("transform", function(d) { return d < 360 && d > 90 ? "rotate(180 " + (radius + 3) + ",0)" : null; })
.text(function(d) { return d + "°"; });
/////////////////////
}
function updateSkyPlot(d){
var pos = [];
var inview = d.inView;
for (var elem in inview){
if (inview.hasOwnProperty(elem)) {
// the azimuth should be in radians and substracted from (Math.PI/2)
var d = [(Math.PI/2) - inview[elem].data[2]*deg2rad, inview[elem].data[1]];
pos.push(d);
}
}
var r = d3.scale.linear()
.domain([90, 0])
.range([0, radius]);
var line = d3.svg.line.radial()
.radius(function(d) {return r(d[1]);})
.angle(function(d) {return -d[0] + Math.PI / 2;});
var color = d3.scale.category20();
svg.selectAll('circle').remove();
gr.append("circle").attr("r", r).style('fill', 'white');
svg.selectAll("point").data(pos).enter().append("circle").attr("class", "point")
.attr("transform", function(d) {
var coors = line([d]).slice(1).slice(0, -1);
return "translate(" + coors + ")"})
.attr("r", 8)
.attr("fill",function(d,i){return color(i);});
}
</script>
</body>
</html>
Here is a possiblity:
<!DOCTYPE html>
<html>
<head>
<meta charset='ISO-8859-1'>
<script src="https://d3js.org/d3.v3.min.js"></script>
</head>
<body style='background-color:lightgray'>
<div id="chart" style='width: 400px; height: 400px; padding-left: 5px; padding-bottom: 5px;'></div>
<script>
var deg2rad = Math.PI/180;
var width = 400, height = 350, radius = Math.min(width, height) / 2 - 30;
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var r = d3.scale.linear()
.domain([90, 0])
.range([0, radius]);
var line = d3.svg.line.radial()
.radius(function(d) {return r(d[1]);})
.angle(function(d) {return -d[0] + Math.PI / 2;});
var gr = null;
createSkyplot();
var json = {"type" : "GSV",
"inView" : [{"data" : [1, 45, 90]},
{"data" : [3, 70, 225]}]
};
updateSkyPlot(json);
function createSkyplot(){
//////////////////////
gr = svg.append("g")
.attr("class", "r axis")
.selectAll("g")
.data(r.ticks(5))
.enter().append("g");
gr.append("circle").attr("r", r).style('fill', 'white');
gr.append("text")
.attr("y", function(d) { return -r(d) - 4; })
.attr("transform", "rotate(20)")
.style("text-anchor", "middle")
.style('fill', 'blue')
.text(function(d) { return d;});
/////////////////////
/////////////////////
var ga = svg.append("g")
.attr("class", "a axis")
.selectAll("g")
.data(d3.range(0, 360, 45))
.enter().append("g")
.attr("transform", function(d) {return "rotate(" + (d - 90) + ")";});
ga.append("line").attr("x2", radius).style('stroke', 'black').style('stroke-dasharray', '1,8');
ga.append("text")
.attr("x", radius + 6)
.attr("dy", ".35em")
.style("text-anchor", function(d) { return d < 360 && d > 90 ? "end" : null; })
.attr("transform", function(d) { return d < 360 && d > 90 ? "rotate(180 " + (radius + 3) + ",0)" : null; })
.text(function(d) { return d + "°"; });
/////////////////////
}
function updateSkyPlot(d){
var pos = [];
var inview = d.inView;
for (var elem in inview){
if (inview.hasOwnProperty(elem)) {
// the azimuth should be in radians and substracted from (Math.PI/2)
var d = [(Math.PI/2) - inview[elem].data[2]*deg2rad, inview[elem].data[1]];
pos.push({ "angle": d, "label": inview[elem].data[0] });
}
}
var r = d3.scale.linear()
.domain([90, 0])
.range([0, radius]);
var line = d3.svg.line.radial()
.radius(function(d) {return r(d[1]);})
.angle(function(d) {return -d[0] + Math.PI / 2;});
var color = d3.scale.category20();
svg.selectAll('circle').remove();
gr.append("circle").attr("r", r).style('fill', 'white');
var points = svg.selectAll("point")
.data(pos)
.enter()
.append("a")
.attr("transform", function(d) {
var coors = line([d.angle]).slice(1).slice(0, -1);
return "translate(" + coors + ")"
});
points.append("circle")
.attr("class", "point")
.attr("r", 8)
.attr("fill",function(d,i){return color(i);});
points.append("text")
.text( function(d) { return d.label })
.attr("transform", "translate(-4,5)")
}
</script>
</body>
</html>
I have kept the label in data (when you create the pos array with circle positions):
var pos = [];
for (var elem in inview) {
if (inview.hasOwnProperty(elem)) {
var d = [(Math.PI/2) - inview[elem].data[2]*deg2rad, inview[elem].data[1]];
// This is the modified line:
pos.push({ "position": d, "label": inview[elem].data[0] });
// pos.push(d);
}
}
Then instead of directly creating the circles, I create an intermediate container which will contain for each data point both the circle and the text label; and which is translated to its position:
var points = svg.selectAll("point")
.data(pos)
.enter()
.append("a") // The container
.attr("transform", function(d) {
var coors = line([d.position]).slice(1).slice(0, -1);
return "translate(" + coors + ")"
});
This way we can include for each data point both the circle:
points.append("circle")
.attr("class", "point")
.attr("r", 8)
.attr("fill",function(d,i){return color(i);});
and the label:
points.append("text")
.text( function(d) { return d.label })
.attr("transform", "translate(-4,5)")
Here in my d3 radial chart, I am trying to get the label text just above the segment arcs instead of keeping outside the outer circle.
Fiddle
var width = 360,
height = 300,
barHeight = height / 2 - 40;
var formatNumber = d3.format("s");
var color = d3.scale.ordinal()
.range(["#F15D5D","#FAD64B"]);
var svg = d3.select('#chart').append("svg")
.attr("width", width)
.attr("height", height)
.attr('class','radial')
.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")");
var data = [{
"name": "ABC",
"value":4
},
{
"name": "XYZ",
"value":5
},{
"name": "DEF",
"value":2
},
{
"name": "GHI",
"value":3
},{
"name": "JKL",
"value":1
}];
data.sort(function(a,b) { return b.value - a.value; });
var extent = [0, d3.max(data, d=>d.value)];
var barScale = d3.scale.linear()
.domain(extent)
.range([0, barHeight]);
var keys = data.map(function(d,i) { return d.name; });
var numBars = keys.length;
// X scale
var x = d3.scale.linear()
.domain(extent)
.range([0, -barHeight]);
// X axis
var xAxis = d3.svg.axis()
.scale(x).orient("left")
.ticks(3)
.tickFormat(formatNumber);
// Inner circles
var circles = svg.selectAll("circle")
.data(x.ticks(5))
.enter().append("circle")
.attr("r", function(d) {return barScale(d);})
.style("fill", "none")
//.style("stroke", "black")
//.style("stroke-dasharray", "2,2")
.style("stroke-width",".5px");
// Create arcs
var arc = d3.svg.arc()
.startAngle(function(d,i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b-a) / 4;
var x = a+d;
var y = b-d;
return x;//(i * 2 * Math.PI) / numBars;
})
.endAngle(function(d,i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b-a) / 4;
var x = a+d;
var y = b-d;
return y;//((i + 1) * 2 * Math.PI) / numBars;
})
.innerRadius(0);
// Render colored arcs
var segments = svg.selectAll("path")
.data(data)
.enter().append("path")
.each(function(d) { d.outerRadius = 0; })
.style("fill", function (d) { return color(d.name); })
.attr("d", arc);
// Animate segments
segments.transition().ease("elastic").duration(1000).delay(function(d,i) {return (25-i)*50;})
.attrTween("d", function(d,index) {
var i = d3.interpolate(d.outerRadius, barScale(+d.value));
return function(t) { d.outerRadius = i(t); return arc(d,index); };
});
// Outer circle
svg.append("circle")
.attr("r", barHeight)
.classed("outer", true)
.style("fill", "none")
//.style("stroke", "black")
.style("stroke-width",".5px");
// Apply x axis
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
// Labels
var labelRadius = barHeight * 1.025;
var labels = svg.append("g")
.classed("labels", true);
labels.append("def")
.append("path")
.attr("id", "label-path")
.attr("d", "m0 " + -labelRadius + " a" + labelRadius + " " + labelRadius + " 0 1,1 -0.01 0");
labels.selectAll("text")
.data(keys)
.enter().append("text")
.style("text-anchor", "middle")
.style("font-weight","bold")
.style("fill", function(d, i) {return "#555";})
.append("textPath")
.attr("xlink:href", "#label-path")
.attr("startOffset", function(d, i) {return i * 100 / numBars + 50 / numBars + '%';})
.text(function(d) {return d.toUpperCase(); });
We can set the position of the <defs>'s paths using the same data you used to create the arcs.
First, let's create an enter selection:
var labels = svg.selectAll("foo")
.data(data)
.enter()
.append("g")
.classed("labels", true);
Then, we append the paths using the barScale for each value (hardcoded padding of 4px here):
labels.append("def")
.append("path")
.attr("id", (d, i) => "label-path" + i)
.attr("d", d => "m0 " + -(barScale(d.value) + 4) +
" a" + (barScale(d.value) + 4) + " " +
(barScale(d.value) + 4) + " 0 1,1 -0.01 0");
Please notice that we have to use unique IDs. Then, we change the IDs in the text-paths:
.attr("xlink:href", (d, i) => "#label-path" + i)
Here is your updated fiddle: https://jsfiddle.net/qt3e0rex/
And the same code in the Stack snippet:
var width = 360,
height = 300,
barHeight = height / 2 - 40;
var formatNumber = d3.format("s");
var color = d3.scale.ordinal()
.range(["#F15D5D", "#FAD64B"]);
var svg = d3.select('body').append("svg")
.attr("width", width)
.attr("height", height)
.attr('class', 'radial')
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var data = [{
"name": "ABC",
"value": 4
}, {
"name": "XYZ",
"value": 5
}, {
"name": "DEF",
"value": 2
}, {
"name": "GHI",
"value": 3
}, {
"name": "JKL",
"value": 1
}];
data.sort(function(a, b) {
return b.value - a.value;
});
var extent = [0, d3.max(data, d => d.value)];
var barScale = d3.scale.linear()
.domain(extent)
.range([0, barHeight]);
var keys = data.map(function(d, i) {
return d.name;
});
var numBars = keys.length;
// X scale
var x = d3.scale.linear()
.domain(extent)
.range([0, -barHeight]);
// X axis
var xAxis = d3.svg.axis()
.scale(x).orient("left")
.ticks(3)
.tickFormat(formatNumber);
// Inner circles
var circles = svg.selectAll("circle")
.data(x.ticks(5))
.enter().append("circle")
.attr("r", function(d) {
return barScale(d);
})
.style("fill", "none")
//.style("stroke", "black")
//.style("stroke-dasharray", "2,2")
.style("stroke-width", ".5px");
// Create arcs
var arc = d3.svg.arc()
.startAngle(function(d, i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b - a) / 4;
var x = a + d;
var y = b - d;
return x; //(i * 2 * Math.PI) / numBars;
})
.endAngle(function(d, i) {
var a = (i * 2 * Math.PI) / numBars;
var b = ((i + 1) * 2 * Math.PI) / numBars;
var d = (b - a) / 4;
var x = a + d;
var y = b - d;
return y; //((i + 1) * 2 * Math.PI) / numBars;
})
.innerRadius(0);
// Render colored arcs
var segments = svg.selectAll("path")
.data(data)
.enter().append("path")
.each(function(d) {
d.outerRadius = 0;
})
.style("fill", function(d) {
return color(d.name);
})
.attr("d", arc);
// Animate segments
segments.transition().ease("elastic").duration(1000).delay(function(d, i) {
return (25 - i) * 50;
})
.attrTween("d", function(d, index) {
var i = d3.interpolate(d.outerRadius, barScale(+d.value));
return function(t) {
d.outerRadius = i(t);
return arc(d, index);
};
});
// Outer circle
svg.append("circle")
.attr("r", barHeight)
.classed("outer", true)
.style("fill", "none")
//.style("stroke", "black")
.style("stroke-width", ".5px");
// Apply x axis
svg.append("g")
.attr("class", "x axis")
.call(xAxis);
// Labels
var labelRadius = barHeight * 1.025;
var labels = svg.selectAll("foo")
.data(data)
.enter()
.append("g")
.classed("labels", true);
labels.append("def")
.append("path")
.attr("id", (d, i) => "label-path" + i)
.attr("d", d => "m0 " + -(barScale(d.value) + 4) + " a" + (barScale(d.value) + 4) + " " + (barScale(d.value) + 4) + " 0 1,1 -0.01 0");
labels.append("text")
.style("text-anchor", "middle")
.style("font-weight", "bold")
.style("fill", function(d, i) {
return "#555";
})
.append("textPath")
.attr("xlink:href", (d, i) => "#label-path" + i)
.attr("startOffset", function(d, i) {
return i * 100 / numBars + 50 / numBars + '%';
})
.text(function(d) {
return d.name.toUpperCase();
});
body {
background-color:#fff;
}
.axis path, .axis line {
fill: none;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.tick text, line {
display:none;
}
circle {
stroke:#ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Can someone help me implementing a spiral chart similar to the one below using d3.js?
I've just got the basic spiral plot (a simple one) as of now but not been able to append bars to the plot based on the timeline as shown in the image. I'm trying out a few things (if you see the commented code).
Here's my fiddle, and my code:
var width = 400,
height = 430,
axes = 12,
tick_axis = 9,
start = 0,
end = 2.25;
var theta = function(r) {
return 2 * Math.PI * r;
};
var angle = d3.scale.linear()
.domain([0, axes]).range([0, 360])
var r = d3.min([width, height]) / 2 - 40;
var r2 = r;
var radius = d3.scale.linear()
.domain([start, end])
.range([0, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 8) + ")");
var points = d3.range(start, end + 0.001, (end - start) / 1000);
var spiral = d3.svg.line.radial()
.interpolate("cardinal")
.angle(theta)
.radius(radius);
var path = svg.selectAll(".spiral")
.data([points])
.enter().append("path")
.attr("class", "spiral")
.attr("d", spiral)
var z = d3.scale.category20();
var circles = svg.selectAll('.circle')
.data(points);
/* circles.enter().append('circle')
.attr('r', 5)
.attr('transform', function(d) { return 'translate(' + d + ')'})
.style('fill', function(d) { return z(d); });
*/
var circle = svg.append("circle")
.attr("r", 13)
.attr("transform", "translate(" + points[0] + ")");
var movingCircle = circle.transition().duration(4000)
.attrTween('transform', translateAlongPath(path.node()))
// .attr('cx', function(d) { return radius(d) * Math.cos(theta(d))})
// .attr('cy', function(d) { return radius(d) * Math.sin(theta(d))})
function translateAlongPath(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
//console.log(p)
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function pathXY(path) {
var l = path.getTotalLength();
var start = 0;
/* for(i=start; i<l; i++) {
var point = path.getPointAtLength(i);
svg.append('rect').transition().duration(400).attr('transform', 'translate(' + point.x +','+point.y+')')
.attr('width', 10).attr('height', 30).style('fill', z);
}*/
}
pathXY(path.node());
/*var test = translateAlongPath(path.node())()();
//console.log(test)
var bars = svg.selectAll('.bar')
.data(points).enter().append('rect').transition().duration(2000)
// .attrTween('transform', translateAlongPath(path.node()))
.attr('class', 'bar')
.attr('width', 10)
.attr('height', 20)
.style('fill', function(d) { return z(d)});
*/
var rect = svg.append('rect').attr('width', 10).attr('height', 10);
rect.transition().duration(3400)
.attrTween('transform', translateAlongPath(path.node()));
It'd be great to have a few similar examples (i.e. spiral timeline plot).
Thanks.
Glad you came back and updated your question, because this is an interesting one. Here's a running minimal implementation. I've commented it ok, so let me know if you have any questions...
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
var width = 500,
height = 500,
start = 0,
end = 2.25,
numSpirals = 4;
var theta = function(r) {
return numSpirals * Math.PI * r;
};
var r = d3.min([width, height]) / 2 - 40;
var radius = d3.scaleLinear()
.domain([start, end])
.range([40, r]);
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// create the spiral, borrowed from http://bl.ocks.org/syntagmatic/3543186
var points = d3.range(start, end + 0.001, (end - start) / 1000);
var spiral = d3.radialLine()
.curve(d3.curveCardinal)
.angle(theta)
.radius(radius);
var path = svg.append("path")
.datum(points)
.attr("id", "spiral")
.attr("d", spiral)
.style("fill", "none")
.style("stroke", "steelblue");
// fudge some data, 2 years of data starting today
var spiralLength = path.node().getTotalLength(),
N = 730,
barWidth = (spiralLength / N) - 1;
var someData = [];
for (var i = 0; i < N; i++) {
var currentDate = new Date();
currentDate.setDate(currentDate.getDate() + i);
someData.push({
date: currentDate,
value: Math.random()
});
}
// here's our time scale that'll run along the spiral
var timeScale = d3.scaleTime()
.domain(d3.extent(someData, function(d){
return d.date;
}))
.range([0, spiralLength]);
// yScale for the bar height
var yScale = d3.scaleLinear()
.domain([0, d3.max(someData, function(d){
return d.value;
})])
.range([0, (r / numSpirals) - 30]);
// append our rects
svg.selectAll("rect")
.data(someData)
.enter()
.append("rect")
.attr("x", function(d,i){
// placement calculations
var linePer = timeScale(d.date),
posOnLine = path.node().getPointAtLength(linePer),
angleOnLine = path.node().getPointAtLength(linePer - barWidth);
d.linePer = linePer; // % distance are on the spiral
d.x = posOnLine.x; // x postion on the spiral
d.y = posOnLine.y; // y position on the spiral
d.a = (Math.atan2(angleOnLine.y, angleOnLine.x) * 180 / Math.PI) - 90; //angle at the spiral position
return d.x;
})
.attr("y", function(d){
return d.y;
})
.attr("width", function(d){
return barWidth;
})
.attr("height", function(d){
return yScale(d.value);
})
.style("fill", "steelblue")
.style("stroke", "none")
.attr("transform", function(d){
return "rotate(" + d.a + "," + d.x + "," + d.y + ")"; // rotate the bar
});
// add date labels
var tF = d3.timeFormat("%b %Y"),
firstInMonth = {};
svg.selectAll("text")
.data(someData)
.enter()
.append("text")
.attr("dy", 10)
.style("text-anchor", "start")
.style("font", "10px arial")
.append("textPath")
// only add for the first of each month
.filter(function(d){
var sd = tF(d.date);
if (!firstInMonth[sd]){
firstInMonth[sd] = 1;
return true;
}
return false;
})
.text(function(d){
return tF(d.date);
})
// place text along spiral
.attr("xlink:href", "#spiral")
.style("fill", "grey")
.attr("startOffset", function(d){
return ((d.linePer / spiralLength) * 100) + "%";
})
</script>
</body>
</html>
I have a scatter plot matrix for which I need a tooltip. I tried using the following code, but then, it gives me tooltips at random points and not at the exact cells.
Can someone tell me where am I going wrong ? Or is not possible to generate a tooltip for my data?
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
font: 10px sans-serif;
padding: 10px;
}
.axis,
.frame {
shape-rendering: crispEdges;
}
.axis line {
stroke: #ddd;
}
.axis path {
display: none;
}
.frame {
fill: none;
stroke: #aaa;
}
circle {
fill-opacity: .7;
}
circle.hidden {
fill: #ccc !important;
}
.extent {
fill: #000;
fill-opacity: .125;
stroke: #fff;
}
</style>
<body>
<div id="chart3"> </div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<script>
var width = 419,
size = 130,
padding = 19.5,
height = 313;
var x = d3.scale.linear().domain([0,100])
.range([padding / 2, size - padding / 2]);
var y = d3.scale.linear().domain([0,1])
.range([size - padding / 2, padding / 2]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5);
var color = d3.scale.ordinal()
.domain(['no chemo', 'induction', 'induction+chemoRT', 'concurrent'])
.range(['#ffae19', '#4ca64c', '#4682B4', '#c51b8a']);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([50,70])
.html(function (d) {
var coordinates = d3.mouse(this);
xValue = x.invert(coordinates[0]);
yValue = y.invert(coordinates[1]);
return "<strong> Age Of Patient " + d3.format(".2f")(xValue * 100)+
" <br/> Probability of Survival : " + d3.format(".2f")(yValue*100) + " % </strong>";
});
d3.csv("SurvivalProbability.csv", function (error, data) {
if (error)
throw error;
var domainByTrait = {},
traits = d3.keys(data[0]).filter(function (d) {
return (d == 'AgeAtTx' || d == 'Probability of Survival')
}),
n = traits.length;
traits.forEach(function (trait) {
domainByTrait[trait] = d3.extent(data, function (d) {
return d[trait];
});
});
xAxis.tickSize(size * n);
yAxis.tickSize(-size * n);
var brush = d3.svg.brush()
.x(x)
.y(y)
.on("brushstart", brushstart)
.on("brush", brushmove)
.on("brushend", brushend);
var svg = d3.select("#chart3").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + padding + "," + padding / 2 + ")");
svg.call(tip);
svg.selectAll(".x.axis")
.data(traits)
.enter().append("g")
.attr("class", "x axis")
.attr("transform", function (d, i) {
return "translate(" + (n - i - 1) * size + ",0)";
})
.each(function (d) {
x.domain(domainByTrait[d]);
d3.select(this).call(xAxis);
});
svg.selectAll(".y.axis")
.data(traits)
.enter().append("g")
.attr("class", "y axis")
.attr("transform", function (d, i) {
return "translate(0," + i * size + ")";
})
.each(function (d) {
y.domain(domainByTrait[d]);
d3.select(this).call(yAxis);
});
var cell = svg.selectAll(".cell")
.data(cross(traits, traits))
.enter().append("g")
.attr("class", "cell")
.attr("transform", function (d) {
return "translate(" + (n - d.i - 1) * size + "," + d.j * size + ")";
})
.each(plot)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
// Titles for the diagonal.
cell.filter(function (d) {
return d.i === d.j;
}).append("text")
.attr("x", padding)
.attr("y", padding)
.attr("dy", ".71em")
.text(function (d) {
return d.x;
});
cell.call(brush);
function plot(p) {
var cell = d3.select(this);
x.domain(domainByTrait[p.x]);
y.domain(domainByTrait[p.y]);
cell.append("rect")
.attr("class", "frame")
.attr("x", padding / 2)
.attr("y", padding / 2)
.attr("width", size - padding)
.attr("height", size - padding);
cell.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function (d) {
return x(d[p.x]);
})
.attr("cy", function (d) {
return y(d[p.y]);
})
.attr("r", 5)
.style("fill", function (d) {
return color(d.Chemotherapy);
});
}
var brushCell;
// Clear the previously-active brush, if any.
function brushstart(p) {
if (brushCell !== this) {
d3.select(brushCell).call(brush.clear());
x.domain(domainByTrait[p.x]);
y.domain(domainByTrait[p.y]);
brushCell = this;
}
}
// Highlight the selected circles.
function brushmove(p) {
var e = brush.extent();
svg.selectAll("circle").classed("hidden", function (d) {
return e[0][0] > d[p.x] || d[p.x] > e[1][0]
|| e[0][1] > d[p.y] || d[p.y] > e[1][1];
});
}
// If the brush is empty, select all circles.
function brushend() {
if (brush.empty())
svg.selectAll(".hidden").classed("hidden", false);
}
function cross(a, b) {
var c = [], n = a.length, m = b.length, i, j;
for (i = - 1; ++i < n; )
for (j = - 1; ++j < m; )
c.push({x: a[i], i: i, y: b[j], j: j});
return c;
}
d3.select(self.frameElement).style("height", size * n + padding + 20 + "px");
var legendRectSize = 10;
var legendSpacing = 10;
var legend = svg.append("g")
.selectAll("g")
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function (d, i) {
var height = legendRectSize;
var x = 2 * size;
var y = (i * height) + 120;
return 'translate(' + x + ',' + y + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendSpacing)
.text(function (d) {
return d;
});
});
</script>
A screenshot of my data - Survival Probability.csv
Ethnicity,AgeAtTx,Site,Tcategory,Nodal_Disease,ecog,Chemotherapy,Local_Therapy,Probability of Survival,KM OS,OS (months),sex
white,65.93972603,supraglottic,T3,N+,0,no chemo,LP/RT alone,0.366190068,0,112.9,Female
white,69.42465753,supraglottic,T3,N+,0,induction,PLRT,0.396018836,0,24.1,Male
white,68.14246575,supraglottic,T3,N0,3,no chemo,LP/RT alone,0.439289384,0,3.566666667,Female
white,40.30410959,supraglottic,T3,N+,1,no chemo,LP/RT alone,0.512773973,1,226.3,Male
white,47.96438356,supraglottic,T3,N+,0,no chemo,PLRT,0.472208904,0,9.6,Female
white,70.3369863,supraglottic,T3,N+,0,no chemo,LP/RT alone,0.324965753,0,25.26666667,Male
white,60.50136986,supraglottic,T3,N+,2,no chemo,LP/RT alone,0.323424658,0,9.5,Female
white,60.72328767,supraglottic,T3,N+,1,no chemo,LP/RT alone,0.321344178,0,15.03333333,Male
white,59.36986301,supraglottic,T3,N0,1,induction,LP/chemoRT,0.646532534,0,4.5,Male
other,57.64931507,supraglottic,T3,N+,1,concurrent,LP/chemoRT,0.662662671,1,52.73333333,Male
This is an interesting situation. It boils down essentially to element append order and mouse-events. First, let's fix the obvious. You want a tooltip on each circle, so you shouldn't be calling tip.show when you mouse over a cell, but on the circles:
cell.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) {
return x(d[p.x]);
})
.attr("cy", function(d) {
return y(d[p.y]);
})
.attr("r", 5)
.style("fill", function(d) {
return color(d.Chemotherapy);
})
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
But you'll notice with this change, we don't receive the events on our circles. This is because svg.brush is placing a rect over each cell so that you can select with the extent, and it's receiving the mouse events. So to fix that we change the order of drawing to brush then circle:
var cell = svg.selectAll(".cell")
.data(cross(traits, traits))
.enter().append("g")
.attr("class", "cell")
.attr("transform", function(d) {
return "translate(" + (n - d.i - 1) * size + "," + d.j * size + ")";
});
// add the brush stuff
cell.call(brush);
// now the circles
cell.each(plot);
But we still have a problem. We've got one more rect on top of our circles, the frame rect. Since we don't care about mouse events on it just do a simple:
.style("pointer-events", "none");
Putting this all together:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
font: 10px sans-serif;
padding: 10px;
}
.axis,
.frame {
shape-rendering: crispEdges;
}
.axis line {
stroke: #ddd;
}
.axis path {
display: none;
}
.frame {
fill: none;
stroke: #aaa;
}
circle {
fill-opacity: .7;
}
circle.hidden {
fill: #ccc !important;
}
.extent {
fill: #000;
fill-opacity: .125;
stroke: #fff;
}
</style>
<body>
<div id="chart3"> </div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<script>
var width = 419,
size = 130,
padding = 19.5,
height = 313;
var x = d3.scale.linear().domain([0, 100])
.range([padding / 2, size - padding / 2]);
var y = d3.scale.linear().domain([0, 1])
.range([size - padding / 2, padding / 2]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5);
var color = d3.scale.ordinal()
.domain(['no chemo', 'induction', 'induction+chemoRT', 'concurrent'])
.range(['#ffae19', '#4ca64c', '#4682B4', '#c51b8a']);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([50, 70])
.html(function(d) {
console.log(d)
var coordinates = d3.mouse(this);
xValue = x.invert(coordinates[0]);
yValue = y.invert(coordinates[1]);
return "<strong> Age Of Patient " + d3.format(".2f")(xValue * 100) +
" <br/> Probability of Survival : " + d3.format(".2f")(yValue * 100) + " % </strong>";
});
//d3.csv("data.csv", function(error, data) {
// if (error)
// throw error;
var data = [{"Ethnicity":"white","AgeAtTx":"65.93972603","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N+","ecog":"0","Chemotherapy":"no chemo","Local_Therapy":"LP/RT alone","Probability of Survival":"0.366190068","KM OS":"0","OS (months)":"112.9","sex":"Female"},{"Ethnicity":"white","AgeAtTx":"69.42465753","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N+","ecog":"0","Chemotherapy":"induction","Local_Therapy":"PLRT","Probability of Survival":"0.396018836","KM OS":"0","OS (months)":"24.1","sex":"Male"},{"Ethnicity":"white","AgeAtTx":"68.14246575","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N0","ecog":"3","Chemotherapy":"no chemo","Local_Therapy":"LP/RT alone","Probability of Survival":"0.439289384","KM OS":"0","OS (months)":"3.566666667","sex":"Female"},{"Ethnicity":"white","AgeAtTx":"40.30410959","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N+","ecog":"1","Chemotherapy":"no chemo","Local_Therapy":"LP/RT alone","Probability of Survival":"0.512773973","KM OS":"1","OS (months)":"226.3","sex":"Male"},{"Ethnicity":"white","AgeAtTx":"47.96438356","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N+","ecog":"0","Chemotherapy":"no chemo","Local_Therapy":"PLRT","Probability of Survival":"0.472208904","KM OS":"0","OS (months)":"9.6","sex":"Female"},{"Ethnicity":"white","AgeAtTx":"70.3369863","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N+","ecog":"0","Chemotherapy":"no chemo","Local_Therapy":"LP/RT alone","Probability of Survival":"0.324965753","KM OS":"0","OS (months)":"25.26666667","sex":"Male"},{"Ethnicity":"white","AgeAtTx":"60.50136986","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N+","ecog":"2","Chemotherapy":"no chemo","Local_Therapy":"LP/RT alone","Probability of Survival":"0.323424658","KM OS":"0","OS (months)":"9.5","sex":"Female"},{"Ethnicity":"white","AgeAtTx":"60.72328767","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N+","ecog":"1","Chemotherapy":"no chemo","Local_Therapy":"LP/RT alone","Probability of Survival":"0.321344178","KM OS":"0","OS (months)":"15.03333333","sex":"Male"},{"Ethnicity":"white","AgeAtTx":"59.36986301","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N0","ecog":"1","Chemotherapy":"induction","Local_Therapy":"LP/chemoRT","Probability of Survival":"0.646532534","KM OS":"0","OS (months)":"4.5","sex":"Male"},{"Ethnicity":"other","AgeAtTx":"57.64931507","Site":"supraglottic","Tcategory":"T3","Nodal_Disease":"N+","ecog":"1","Chemotherapy":"concurrent","Local_Therapy":"LP/chemoRT","Probability of Survival":"0.662662671","KM OS":"1","OS (months)":"52.73333333","sex":"Male"}];
var domainByTrait = {},
traits = d3.keys(data[0]).filter(function(d) {
return (d == 'AgeAtTx' || d == 'Probability of Survival')
}),
n = traits.length;
traits.forEach(function(trait) {
domainByTrait[trait] = d3.extent(data, function(d) {
return d[trait];
});
});
xAxis.tickSize(size * n);
yAxis.tickSize(-size * n);
var brush = d3.svg.brush()
.x(x)
.y(y)
.on("brushstart", brushstart)
.on("brush", brushmove)
.on("brushend", brushend);
var svg = d3.select("#chart3").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + padding + "," + padding / 2 + ")");
svg.call(tip);
svg.selectAll(".x.axis")
.data(traits)
.enter().append("g")
.attr("class", "x axis")
.attr("transform", function(d, i) {
return "translate(" + (n - i - 1) * size + ",0)";
})
.each(function(d) {
x.domain(domainByTrait[d]);
d3.select(this).call(xAxis);
});
svg.selectAll(".y.axis")
.data(traits)
.enter().append("g")
.attr("class", "y axis")
.attr("transform", function(d, i) {
return "translate(0," + i * size + ")";
})
.each(function(d) {
y.domain(domainByTrait[d]);
d3.select(this).call(yAxis);
});
var cell = svg.selectAll(".cell")
.data(cross(traits, traits))
.enter().append("g")
.attr("class", "cell")
.attr("transform", function(d) {
return "translate(" + (n - d.i - 1) * size + "," + d.j * size + ")";
});
cell.call(brush);
cell.each(plot);
// Titles for the diagonal.
cell.filter(function(d) {
return d.i === d.j;
}).append("text")
.attr("x", padding)
.attr("y", padding)
.attr("dy", ".71em")
.text(function(d) {
return d.x;
});
function plot(p) {
var cell = d3.select(this);
x.domain(domainByTrait[p.x]);
y.domain(domainByTrait[p.y]);
cell.append("rect")
.attr("class", "frame")
.attr("x", padding / 2)
.attr("y", padding / 2)
.attr("width", size - padding)
.attr("height", size - padding)
.style("pointer-events", "none");
cell.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) {
return x(d[p.x]);
})
.attr("cy", function(d) {
return y(d[p.y]);
})
.attr("r", 5)
.style("fill", function(d) {
return color(d.Chemotherapy);
})
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
}
var brushCell;
// Clear the previously-active brush, if any.
function brushstart(p) {
if (brushCell !== this) {
d3.select(brushCell).call(brush.clear());
x.domain(domainByTrait[p.x]);
y.domain(domainByTrait[p.y]);
brushCell = this;
}
}
// Highlight the selected circles.
function brushmove(p) {
var e = brush.extent();
svg.selectAll("circle").classed("hidden", function(d) {
return e[0][0] > d[p.x] || d[p.x] > e[1][0] || e[0][1] > d[p.y] || d[p.y] > e[1][1];
});
}
// If the brush is empty, select all circles.
function brushend() {
if (brush.empty())
svg.selectAll(".hidden").classed("hidden", false);
}
function cross(a, b) {
var c = [],
n = a.length,
m = b.length,
i, j;
for (i = -1; ++i < n;)
for (j = -1; ++j < m;)
c.push({
x: a[i],
i: i,
y: b[j],
j: j
});
return c;
}
var legendRectSize = 10;
var legendSpacing = 10;
var legend = svg.append("g")
.selectAll("g")
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize;
var x = 2 * size;
var y = (i * height) + 120;
return 'translate(' + x + ',' + y + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendSpacing)
.text(function(d) {
return d;
});
//});
</script>
The code below displays marker-ends on arrows/paths/lines as intended, but the color of the marker-end does not vary by line (i.e., it is always the same orange color, not the color of its respective line). I think the code is defaulting to the color assigned to the first field of my data(?). Any advice would be appreciated.
<script src="http://www.protobi.com/javascripts/d3.v3.min.js"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<script src="http://www.protobi.com/examples/pca/pca.js"></script>
<script type="text/javascript">
var margin = {top: 20, right: 20, bottom: 20, left: 20};
var width = 1500 - margin.left - margin.right;
var height = 1500 - margin.top - margin.bottom;
var angle = Math.PI * 0;
var color = d3.scale.category10();
var x = d3.scale.linear().range([width, 0]); // switch to match how R biplot shows it
var y = d3.scale.linear().range([height, 0]);
x.domain([-3.5,3.5]).nice()
y.domain([-3.5,3.5]).nice()
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("/brand.csv", function(error, data) {
var matrix = data.map(function(d){
return d3.values(d).slice(1,d.length).map(parseFloat);
});
var pca = new PCA();
matrix = pca.scale(matrix,true,true);
pc = pca.pca(matrix,2)
var A = pc[0]; // this is the U matrix from SVD
var B = pc[1]; // this is the dV matrix from SVD
var brand_names = Object.keys(data[0]); // first row of data file ["ATTRIBUTE", "BRAND A", "BRAND B", "BRAND C", ...]
brand_names.shift(); // drop the first column label, e.g. "ATTRIBUTE"
data.map(function(d,i){
label: d.ATTRIBUTE,
d.pc1 = A[i][0];
d.pc2 = A[i][1];
});
var label_offset = {
"Early/First line": 20,
"Unfamiliar":10,
"Convenient": -5
}
var brands = brand_names
.map(function(key, i) {
return {
brand: key,
pc1: B[i][0]*4,
pc2: B[i][1]*4
}
});
function rotate(x,y, dtheta) {
var r = Math.sqrt(x*x + y*y);
var theta = Math.atan(y/x);
if (x<0) theta += Math.PI;
return {
x: r * Math.cos(theta + dtheta),
y: r * Math.sin(theta + dtheta)
}
}
data.map(function(d) {
var xy = rotate(d.pc1, d.pc2, angle);
d.pc1 = xy.x;
d.pc2 = xy.y;
});
brands.map(function(d) {
var xy = rotate(d.pc1, d.pc2, angle);
d.pc1 = xy.x;
d.pc2 = xy.y;
});
var showAxis = false; // normally we don't want to see the axis in PCA, it's meaningless
if (showAxis) {
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("PC1");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("PC2");
}
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 10.5)
.attr("cx", function(d) { return x(d.pc1); })
.attr("cy", function(d) { return y(d.pc2); })
.style("fill", function(d) { return color(d['species']); })
.on('mouseover', onMouseOverAttribute)
.on('mouseleave', onMouseLeave);
svg.selectAll("text.brand")
.data(brands)
.enter().append("text")
.attr("class", "label-brand")
.attr("x", function(d) { return x(d.pc1) + 10; })
.attr("y", function(d) { return y(d.pc2) + 0; })
.text(function(d) { return d['brand']})
svg.selectAll("marker.brand")
.data(brands)
.enter().append("svg:marker")
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.style("fill", function(d) { return color(d['brand']); });
svg.selectAll(".line")
.data(brands)
.enter().append("line")
.attr("class", "square")
.attr('x1', function(d) { return x(-d.pc1);})
.attr('y1', function(d) { return y(-d.pc2); })
.attr("x2", function(d) { return x(d.pc1); })
.attr("y2", function(d) { return y(d.pc2); })
.style("stroke", function(d) { return color(d['brand']); })
.style('marker-end', "url(#end-arrow)")
.on('mouseover', onMouseOverBrand)
.on('mouseleave', onMouseLeave);
svg.selectAll("text.attr")
.data(data)
.enter().append("text")
.attr("class", "label-attr")
.attr("x", function(d,i ) { return x(d.pc1)+4 ; })
.attr("y", function(d ,i) { return y(d.pc2) + (label_offset[d.ATTRIBUTE]||0); })
.text(function(d,i) { return d.ATTRIBUTE})
var pctFmt = d3.format('0%')
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([10, 20])
.direction('e')
.html(function(values,title) {
var str =''
str += '<h3>' + (title.length==1 ? 'Brand ' : '' )+ title + '</h3>'
str += "<table>";
for (var i=0; i<values.length; i++) {
if (values[i].key != 'ATTRIBUTE' && values[i].key != 'pc1' && values[i].key != 'pc2') {
str += "<tr>";
str += "<td>" + values[i].key + "</td>";
str += "<td class=pct>" + pctFmt(values[i].value) + "</td>";
str + "</tr>";
}
}
str += "</table>";
return str;
});
svg.call(tip);
function getSpPoint(A,B,C){
var x1=A.x, y1=A.y, x2=B.x, y2=B.y, x3=C.x, y3=C.y;
var px = x2-x1, py = y2-y1, dAB = px*px + py*py;
var u = ((x3 - x1) * px + (y3 - y1) * py) / dAB;
var x = x1 + u * px, y = y1 + u * py;
return {x:x, y:y}; //this is D
}
// draw line from the attribute a perpendicular to each brand b
function onMouseOverAttribute(a,j) {
brands.forEach(function(b, idx) {
var A = { x: 0, y:0 };
var B = { x: b.pc1, y: b.pc2 };
var C = { x: a.pc1, y: a.pc2 };
b.D = getSpPoint(A,B,C);
});
svg.selectAll('.tracer')
.data(brands)
.enter()
.append('line')
.attr('class', 'tracer')
.attr('x1', function(b,i) { return x(a.pc1); return x1; })
.attr('y1', function(b,i) { return y(a.pc2); return y1; })
.attr('x2', function(b,i) { return x(b.D.x); return x2; })
.attr('y2', function(b,i) { return y(b.D.y); return y2; })
.style("stroke", function(d) { return "#aaa"});
delete a.D;
var tipText = d3.entries(a);
tip.show(tipText, a.ATTRIBUTE);
};
// draw line from the brand axis a perpendicular to each attribute b
function onMouseOverBrand(b,j) {
data.forEach(function(a, idx) {
var A = { x: 0, y:0 };
var B = { x: b.pc1, y: b.pc2 };
var C = { x: a.pc1, y: a.pc2 };
a.D = getSpPoint(A,B,C);
});
svg.selectAll('.tracer')
.data(data)
.enter()
.append('line')
.attr('class', 'tracer')
.attr('x1', function(a,i) { return x(a.D.x); })
.attr('y1', function(a,i) { return y(a.D.y); })
.attr('x2', function(a,i) { return x(a.pc1); })
.attr('y2', function(a,i) { return y(a.pc2); })
.style("stroke", function(d) { return "#aaa"});
var tipText = data.map(function(d) {
return {key: d.ATTRIBUTE, value: d[b['brand']] }
})
tip.show(tipText, b.brand);
};
function onMouseLeave(b,j) {
svg.selectAll('.tracer').remove()
tip.hide();
}
});
While you are creating an svg:marker for each line, you give them all the same id. When they are then used on your line elements, since they all have the same id you are only using one of them.
Simple fix, give them unique ids:
svg.selectAll("marker.brand")
.data(brands)
.enter().append("svg:marker")
.attr('id', function(d,i){
return 'end-arrow' + i; //<-- append index postion
})
...
svg.selectAll(".line")
.data(brands)
.enter().append("line")
.attr("class", "square")
.attr('x1', function(d) {
return x(-d.pc1);
})
.attr('y1', function(d) {
return y(-d.pc2);
})
.attr("x2", function(d) {
return x(d.pc1);
})
.attr("y2", function(d) {
return y(d.pc2);
})
.style("stroke", function(d) {
return color(d['brand']);
})
.style('marker-end', function(d,i){
return "url(#end-arrow"+i+")"; //<--use the one with the right id
})
....
Example here.