t-viSNE: Interactive Assessment and Interpretation of t-SNE Projections
https://doi.org/10.1109/TVCG.2020.2986996
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
8.3 KiB
299 lines
8.3 KiB
d3v3.ringNote = function() {
|
|
var draggable = false,
|
|
controlRadius = 15;
|
|
|
|
var dragCenter = d3v3.behavior.drag()
|
|
.origin(function(d) { return { x: 0, y: 0}; })
|
|
.on("drag", dragmoveCenter);
|
|
|
|
var dragRadius = d3v3.behavior.drag()
|
|
.origin(function(d) { return { x: 0, y: 0 }; })
|
|
.on("drag", dragmoveRadius);
|
|
|
|
var dragText = d3v3.behavior.drag()
|
|
.origin(function(d) { return { x: 0, y: 0 }; })
|
|
.on("drag", dragmoveText);
|
|
|
|
var path = d3v3.svg.line();
|
|
|
|
function draw(selection, annotation) {
|
|
|
|
selection.selectAll(".ring-note").remove();
|
|
|
|
var gRingNote = selection.selectAll(".ring-note")
|
|
.data(annotation)
|
|
.enter().append("g")
|
|
.attr("class", "ring-note")
|
|
.attr("transform", function(d) {
|
|
return "translate(" + d.cx + "," + d.cy + ")";
|
|
});
|
|
|
|
var gAnnotation = gRingNote.append("g")
|
|
.attr("class", "annotation");
|
|
|
|
var circle = gAnnotation.append("circle")
|
|
.attr("r", function(d) { return d.r; });
|
|
|
|
var line = gAnnotation.append("path")
|
|
.call(updateLine);
|
|
|
|
var text = gAnnotation.append("text")
|
|
.call(updateText);
|
|
|
|
if (draggable) {
|
|
|
|
var gControls = gRingNote.append("g")
|
|
.attr("class", "controls");
|
|
|
|
// Draggable circle that moves the circle's location
|
|
var center = gControls.append("circle")
|
|
.attr("class", "center")
|
|
.call(styleControl)
|
|
.call(dragCenter);
|
|
|
|
// Draggable circle that changes the circle's radius
|
|
var radius = gControls.append("circle")
|
|
.attr("class", "radius")
|
|
.attr("cx", function(d) { return d.r; })
|
|
.call(styleControl)
|
|
.call(dragRadius);
|
|
|
|
// Make text draggble
|
|
text
|
|
.style("cursor", "move")
|
|
.call(dragText);
|
|
}
|
|
|
|
return selection;
|
|
}
|
|
|
|
draw.draggable = function(_) {
|
|
if (!arguments.length) return draggable;
|
|
draggable = _;
|
|
return draw;
|
|
};
|
|
|
|
// Region in relation to circle, e.g., N, NW, W, SW, etc.
|
|
function getRegion(x, y, r) {
|
|
var px = r * Math.cos(Math.PI/4),
|
|
py = r * Math.sin(Math.PI/4);
|
|
|
|
var distance = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
|
|
|
if (distance < r) {
|
|
return null;
|
|
}
|
|
else {
|
|
if (x > px) {
|
|
// East
|
|
if (y > py) return "SE";
|
|
if (y < -py) return "NE";
|
|
if (x > r) return "E";
|
|
return null;
|
|
}
|
|
else if (x < -px) {
|
|
// West
|
|
if (y > py) return "SW";
|
|
if (y < -py) return "NW";
|
|
if (x < -r) return "W";
|
|
return null;
|
|
}
|
|
else {
|
|
// Center
|
|
if (y > r) return "S";
|
|
if (y < -r) return "N";
|
|
}
|
|
}
|
|
}
|
|
|
|
function dragmoveCenter(d) {
|
|
var gRingNote = d3v3.select(this.parentNode.parentNode);
|
|
|
|
d.cx += d3v3.event.x;
|
|
d.cy += d3v3.event.y;
|
|
|
|
gRingNote
|
|
.attr("transform", function(d) {
|
|
return "translate(" + d.cx + "," + d.cy + ")";
|
|
});
|
|
}
|
|
|
|
function dragmoveRadius(d) {
|
|
var gRingNote = d3v3.select(this.parentNode.parentNode),
|
|
gAnnotation = gRingNote.select(".annotation"),
|
|
circle = gAnnotation.select("circle"),
|
|
line = gAnnotation.select("path"),
|
|
text = gAnnotation.select("text"),
|
|
radius = d3v3.select(this);
|
|
|
|
d.r += d3v3.event.dx;
|
|
|
|
circle.attr("r", function(d) { return d.r; });
|
|
radius.attr("cx", function(d) { return d.r; });
|
|
line.call(updateLine);
|
|
text.call(updateText);
|
|
}
|
|
|
|
function dragmoveText(d) {
|
|
var gAnnotation = d3v3.select(this.parentNode),
|
|
line = gAnnotation.select("path"),
|
|
text = d3v3.select(this);
|
|
|
|
d.textOffset[0] += d3v3.event.dx;
|
|
d.textOffset[1] += d3v3.event.dy;
|
|
|
|
text.call(updateText);
|
|
line.call(updateLine);
|
|
}
|
|
|
|
function updateLine(selection) {
|
|
return selection.attr("d", function(d) {
|
|
var x = d.textOffset[0],
|
|
y = d.textOffset[1],
|
|
lineData = getLineData(x, y, d.r);
|
|
return path(lineData);
|
|
});
|
|
}
|
|
|
|
function getLineData(x, y, r) {
|
|
var region = getRegion(x, y, r);
|
|
|
|
if (region == null) {
|
|
// No line if text is inside the circle
|
|
return [];
|
|
}
|
|
else {
|
|
// Cardinal directions
|
|
if (region == "N") return [[0, -r], [0, y]];
|
|
if (region == "E") return [[r, 0], [x, 0]];
|
|
if (region == "S") return [[0, r], [0, y]];
|
|
if (region == "W") return [[-r, 0],[x, 0]];
|
|
|
|
var d0 = r * Math.cos(Math.PI/4),
|
|
d1 = Math.min(Math.abs(x), Math.abs(y)) - d0;
|
|
|
|
// Intermediate directions
|
|
if (region == "NE") return [[ d0, -d0], [ d0 + d1, -d0 - d1], [x, y]];
|
|
if (region == "SE") return [[ d0, d0], [ d0 + d1, d0 + d1], [x, y]];
|
|
if (region == "SW") return [[-d0, d0], [-d0 - d1, d0 + d1], [x, y]];
|
|
if (region == "NW") return [[-d0, -d0], [-d0 - d1, -d0 - d1], [x, y]];
|
|
}
|
|
}
|
|
|
|
function updateText(selection) {
|
|
return selection.each(function(d) {
|
|
var x = d.textOffset[0],
|
|
y = d.textOffset[1],
|
|
region = getRegion(x, y, d.r),
|
|
textCoords = getTextCoords(x, y, region);
|
|
|
|
d3v3.select(this)
|
|
.attr("x", textCoords.x)
|
|
.attr("y", textCoords.y)
|
|
.text(d.text)
|
|
.each(function(d) {
|
|
var x = d.textOffset[0],
|
|
y = d.textOffset[1],
|
|
textAnchor = getTextAnchor(x, y, region);
|
|
|
|
var dx = textAnchor == "start" ? "0.33em" :
|
|
textAnchor == "end" ? "-0.33em" : "0";
|
|
|
|
var dy = textAnchor !== "middle" ? ".33em" :
|
|
["NW", "N", "NE"].indexOf(region) !== -1 ? "-.33em" : "1em";
|
|
|
|
var orientation = textAnchor !== "middle" ? undefined :
|
|
["NW", "N", "NE"].indexOf(region) !== -1 ? "bottom" : "top";
|
|
|
|
d3v3.select(this)
|
|
.style("text-anchor", textAnchor)
|
|
.attr("dx", dx)
|
|
.attr("dy", dy)
|
|
.call(wrapText, d.textWidth || 960, orientation);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getTextCoords(x, y, region) {
|
|
if (region == "N") return { x: 0, y: y };
|
|
if (region == "E") return { x: x, y: 0 };
|
|
if (region == "S") return { x: 0, y: y };
|
|
if (region == "W") return { x: x, y: 0 };
|
|
return { x: x, y: y };
|
|
}
|
|
|
|
function getTextAnchor(x, y, region) {
|
|
if (region == null) {
|
|
return "middle";
|
|
}
|
|
else {
|
|
// Cardinal directions
|
|
if (region == "N") return "middle";
|
|
if (region == "E") return "start";
|
|
if (region == "S") return "middle";
|
|
if (region == "W") return "end";
|
|
|
|
var xLonger = Math.abs(x) > Math.abs(y);
|
|
|
|
// Intermediate directions`
|
|
if (region == "NE") return xLonger ? "start" : "middle";
|
|
if (region == "SE") return xLonger ? "start" : "middle";
|
|
if (region == "SW") return xLonger ? "end" : "middle";
|
|
if (region == "NW") return xLonger ? "end" : "middle";
|
|
}
|
|
}
|
|
|
|
// Adapted from: https://bl.ocks.org/mbostock/7555321
|
|
function wrapText(text, width, orientation) {
|
|
text.each(function(d) {
|
|
var text = d3v3.select(this),
|
|
words = text.text().split(/\s+/).reverse(),
|
|
word,
|
|
line = [],
|
|
lineNumber = 1,
|
|
lineHeight = 1.1, // ems
|
|
x = text.attr("x"),
|
|
dx = text.attr("dx"),
|
|
tspan = text.text(null).append("tspan").attr("x", x).attr("dx", dx);
|
|
while (word = words.pop()) {
|
|
line.push(word);
|
|
tspan.text(line.join(" "));
|
|
if (tspan.node().getComputedTextLength() > width) {
|
|
line.pop();
|
|
tspan.text(line.join(" "));
|
|
line = [word];
|
|
tspan = text.append("tspan")
|
|
.attr("x", x)
|
|
.attr("dx", dx)
|
|
.attr("dy", lineHeight + "em")
|
|
.text(word);
|
|
lineNumber++;
|
|
}
|
|
}
|
|
|
|
var dy;
|
|
if (orientation == "bottom") {
|
|
dy = -lineHeight * (lineNumber-1) - .33;
|
|
}
|
|
else if (orientation == "top") {
|
|
dy = 1;
|
|
}
|
|
else {
|
|
dy = -lineHeight * ((lineNumber-1) / 2) + .33;
|
|
}
|
|
text.attr("dy", dy + "em");
|
|
|
|
});
|
|
}
|
|
|
|
function styleControl(selection) {
|
|
selection
|
|
.attr("r", controlRadius)
|
|
.style("fill-opacity", "0")
|
|
.style("stroke", "black")
|
|
.style("stroke-dasharray", "3, 3")
|
|
.style("cursor", "move");
|
|
}
|
|
|
|
return draw;
|
|
}; |