Skip to content

Instantly share code, notes, and snippets.

@mostaphaRoudsari
Last active November 26, 2020 04:03
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mostaphaRoudsari/b4e090bb50146d88aec4 to your computer and use it in GitHub Desktop.
Save mostaphaRoudsari/b4e090bb50146d88aec4 to your computer and use it in GitHub Desktop.
Parallel Coordinates with mouseover highlight and tooltip
license: mit
<!DOCTYPE html>
<style>
#wrapper {
position: relative;
float: left;
top: 20px;
font-family: sans-serif;
font-size: 10px;
}
#tooltip{
font-family: sans-serif;
font-size: 14px;
font-weight: bold;
color:black;
}
</style>
<body>
<link rel="stylesheet" type="text/css" href="http://mostapharoudsari.github.io/honeybee/pc_source_files/css/d3.parcoords.css">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src = "http://mostapharoudsari.github.io/honeybee/pc_source_files/d3/d3.parcoords.js"></script>
<div id="wrapper" class="parcoords" style="width:100%; height:420px"></div>
<script>
var color_set = d3.scale.linear()
.range(["#3182bd", "#f33"]);
// load default chart
d3.csv("results.csv", function(data){
// collect text for first column to adjust left margin
var firstCell = data.map(function(d){return d3.values(d)[0]});
// find the longest text size in the first row to adjust left margin
var textLength = 0;
firstCell.forEach(function(d){
if (d.length > textLength) textLength = d.length;
});
// get parallel coordinates
graph = d3.parcoords()('#wrapper')
.data(data)
.margin({ top: 30, left: 3 * textLength, bottom: 40, right: 0 })
.alpha(0.6)
.mode("queue")
.rate(5)
.render()
.brushMode("1D-axes") // enable brushing
//.reorderable() // I removed this for now as it can mess up with tooltips
.interactive();
// add instruction text
var instructions = "-Drag around axis to begin brush. -Click axis to clear brush. -Click a label to color data based on axis values. -Hover on each line to highlight."
d3.select("#wrapper svg").append("text")
.text(instructions)
.attr("text-anchor", "middle")
.attr("text-decoration", "overline")
.attr("transform", "translate(" + graph.width()/2 + "," + (graph.height()-5) + ")");;
// set the initial coloring based on the 3rd column
update_colors(d3.keys(data[0])[2]);
// click label to activate coloring
graph.svg.selectAll(".dimension")
.on("click", update_colors)
.selectAll(".label")
.style("font-size", "14px"); // change font sizes of selected lable
//add hover event
d3.select("#wrapper svg")
.on("mousemove", function() {
var mousePosition = d3.mouse(this);
highlightLineOnClick(mousePosition, true); //true will also add tooltip
})
.on("mouseout", function(){
cleanTooltip();
graph.unhighlight();
});
});
// update color and font weight of chart based on axis selection
// modified from here: https://syntagmatic.github.io/parallel-coordinates/
function update_colors(dimension) {
// change the fonts to bold
graph.svg.selectAll(".dimension")
.style("font-weight", "normal")
.filter(function(d) { return d == dimension; })
.style("font-weight", "bold");
// change color of lines
// set domain of color scale
var values = graph.data().map(function(d){return parseFloat(d[dimension])});
color_set.domain([d3.min(values), d3.max(values)]);
// change colors for each line
graph.color(function(d){return color_set([d[dimension]])}).render();
};
// Add highlight for every line on click
function getCentroids(data){
// this function returns centroid points for data. I had to change the source
// for parallelcoordinates and make compute_centroids public.
// I assume this should be already somewhere in graph and I don't need to recalculate it
// but I couldn't find it so I just wrote this for now
var margins = graph.margin();
var graphCentPts = [];
data.forEach(function(d){
var initCenPts = graph.compute_centroids(d).filter(function(d, i){return i%2==0;});
// move points based on margins
var cenPts = initCenPts.map(function(d){
return [d[0] + margins["left"], d[1]+ margins["top"]];
});
graphCentPts.push(cenPts);
});
return graphCentPts;
}
function getActiveData(){
// I'm pretty sure this data is already somewhere in graph
if (graph.brushed()!=false) return graph.brushed();
return graph.data();
}
function isOnLine(startPt, endPt, testPt, tol){
// check if test point is close enough to a line
// between startPt and endPt. close enough means smaller than tolerance
var x0 = testPt[0];
var y0 = testPt[1];
var x1 = startPt[0];
var y1 = startPt[1];
var x2 = endPt[0];
var y2 = endPt[1];
var Dx = x2 - x1;
var Dy = y2 - y1;
var delta = Math.abs(Dy*x0 - Dx*y0 - x1*y2+x2*y1)/Math.sqrt(Math.pow(Dx, 2) + Math.pow(Dy, 2));
//console.log(delta);
if (delta <= tol) return true;
return false;
}
function findAxes(testPt, cenPts){
// finds between which two axis the mouse is
var x = testPt[0];
var y = testPt[1];
// make sure it is inside the range of x
if (cenPts[0][0] > x) return false;
if (cenPts[cenPts.length-1][0] < x) return false;
// find between which segment the point is
for (var i=0; i<cenPts.length; i++){
if (cenPts[i][0] > x) return i;
}
}
function cleanTooltip(){
// removes any object under #tooltip is
graph.svg.selectAll("#tooltip")
.remove();
}
function addTooltip(clicked, clickedCenPts){
// sdd tooltip to multiple clicked lines
var clickedDataSet = [];
var margins = graph.margin()
// get all the values into a single list
// I'm pretty sure there is a better way to write this is Javascript
for (var i=0; i<clicked.length; i++){
for (var j=0; j<clickedCenPts[i].length; j++){
var text = d3.values(clicked[i])[j];
// not clean at all!
var x = clickedCenPts[i][j][0] - margins.left;
var y = clickedCenPts[i][j][1] - margins.top;
clickedDataSet.push([x, y, text]);
}
};
// add rectangles
var fontSize = 14;
var padding = 2;
var rectHeight = fontSize + 2 * padding; //based on font size
graph.svg.selectAll("rect[id='tooltip']")
.data(clickedDataSet).enter()
.append("rect")
.attr("x", function(d) { return d[0] - d[2].length * 5;})
.attr("y", function(d) { return d[1] - rectHeight + 2 * padding; })
.attr("rx", "2")
.attr("ry", "2")
.attr("id", "tooltip")
.attr("fill", "grey")
.attr("opacity", 0.9)
.attr("width", function(d){return d[2].length * 10;})
.attr("height", rectHeight);
// add text on top of rectangle
graph.svg.selectAll("text[id='tooltip']")
.data(clickedDataSet).enter()
.append("text")
.attr("x", function(d) { return d[0];})
.attr("y", function(d) { return d[1]; })
.attr("id", "tooltip")
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("font-size", fontSize)
.text( function (d){ return d[2];})
}
function getClickedLines(mouseClick){
var clicked = [];
var clickedCenPts = [];
// find which data is activated right now
var activeData = getActiveData();
// find centriod points
var graphCentPts = getCentroids(activeData);
if (graphCentPts.length==0) return false;
// find between which axes the point is
var axeNum = findAxes(mouseClick, graphCentPts[0]);
if (!axeNum) return false;
graphCentPts.forEach(function(d, i){
if (isOnLine(d[axeNum-1], d[axeNum], mouseClick, 2)){
clicked.push(activeData[i]);
clickedCenPts.push(graphCentPts[i]); // for tooltip
}
});
return [clicked, clickedCenPts]
}
function highlightLineOnClick(mouseClick, drawTooltip){
var clicked = [];
var clickedCenPts = [];
clickedData = getClickedLines(mouseClick);
if (clickedData && clickedData[0].length!=0){
clicked = clickedData[0];
clickedCenPts = clickedData[1];
// highlight clicked line
graph.highlight(clicked);
if (drawTooltip){
// clean if anything is there
cleanTooltip();
// add tooltip
addTooltip(clicked, clickedCenPts);
}
}
};
</script>
</body>
Case_Description Heating Cooling Pumps & Aux Vent. Fans
Base Design 132573 60471 13158 149825
0+ASHRAE Walls 139144 61356 12793 151225
0+ASHRAE Window 150893 65522 12194 154333
0+ASHRAE C factor 133043 57580 13080 149926
0+ASHRAE Fridge 131315 61327 13252 150119
0+ASHRAE Dishwasher 132258 60717 13184 149868
0+ASHRAE Rf 136384 60390 12972 150420
0+ASHRAE fan speed 132572 60474 13158 150275
0+ASHRAE fan power 136552 58833 13019 90551
0+Single Speed Compressor 143919 74848 12806 149825
0+ASHRAE Clng Eff 134399 72331 13821 147832
0+ASHRAE Htng Eff 136286 60471 13025 150450
0+ASHRAE Ext light 132573 60471 13158 149825
0+ASHRAE DHW 132573 60471 13158 149825
0+ASHRAE DHW eff 132573 60471 13158 149825
@lee1043
Copy link

lee1043 commented Sep 14, 2018

Hi @mostaphaRoudsari, I am very interested to use this capability for my project, but couldn't find below dependent files:

<link rel="stylesheet" type="text/css" href="http://mostapharoudsari.github.io/Honeybee/pc_source_files/css/d3.parcoords.css">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src = "http://mostapharoudsari.github.io/Honeybee/pc_source_files/d3/d3.parcoords.js"></script>

It seems the link is broken, wondering if you could help me to get these files.

Thank you very much.

@mostaphaRoudsari
Copy link
Author

Hi @lee1043, Sorry for the late reply. I renamed Honeybee repository to honeybee and that's why it was failing. It should work fine now.

@kbclingen
Copy link

Hi @mostaphaRoudsari I'm coming back to this example, using a tree species dataset in Boston, and I was wondering if you could relink to your site to work with this code.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment