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.
 
 
 
 
 
t-viSNE/modules/d3-legend/src/legend.js

234 lines
6.3 KiB

import { select } from 'd3-selection'
import { format, formatPrefix } from 'd3-format'
const d3_identity = (d) => d
const d3_reverse = (arr) => {
const mirror = [];
for (let i = 0, l = arr.length; i < l; i++) {
mirror[i] = arr[l-i-1];
}
return mirror;
}
//Text wrapping code adapted from Mike Bostock
const d3_textWrapping = (text, width) => {
text.each(function() {
var text = select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.2, //ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")) || 0,
tspan = text.text(null)
.append("tspan")
.attr("x", 0)
.attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width && line.length > 1) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan")
.attr("x", 0)
.attr("dy", lineHeight + dy + "em").text(word);
}
}
});
}
const d3_mergeLabels = (gen=[], labels, domain, range) => {
if (typeof labels === "object"){
if(labels.length === 0) return gen;
let i = labels.length;
for (; i < gen.length; i++) {
labels.push(gen[i]);
}
return labels;
} else if (typeof labels === "function") {
const customLabels = []
const genLength = gen.length
for (let i=0; i < genLength; i++){
customLabels.push(labels({
i,
genLength,
generatedLabels : gen,
domain,
range }))
}
return customLabels
}
return gen;
}
const d3_linearLegend = (scale, cells, labelFormat) => {
let data = [];
if (cells.length > 1){
data = cells;
} else {
const domain = scale.domain(),
increment = (domain[domain.length - 1] - domain[0])/(cells - 1)
let i = 0;
for (; i < cells; i++){
data.push(domain[0] + i*increment);
}
}
const labels = data.map(labelFormat);
return {data: data,
labels: labels,
feature: d => scale(d)};
}
const d3_quantLegend = (scale, labelFormat, labelDelimiter) => {
const labels = scale.range().map( d => {
const invert = scale.invertExtent(d);
return labelFormat(invert[0]) + " " + labelDelimiter + " " + labelFormat(invert[1]);
});
return {data: scale.range(),
labels: labels,
feature: d3_identity
};
}
const d3_ordinalLegend= scale => ({data: scale.domain(),
labels: scale.domain(),
feature: d => scale(d) }
)
const d3_cellOver = (cellDispatcher, d, obj) => {
cellDispatcher.call("cellover", obj, d);
}
const d3_cellOut = (cellDispatcher, d, obj) => {
cellDispatcher.call("cellout", obj, d);
}
const d3_cellClick = (cellDispatcher, d, obj) => {
cellDispatcher.call("cellclick", obj, d);
}
export default {
d3_drawShapes: (shape, shapes, shapeHeight, shapeWidth, shapeRadius, path) => {
if (shape === "rect"){
shapes.attr("height", shapeHeight)
.attr("width", shapeWidth);
} else if (shape === "circle") {
shapes.attr("r", shapeRadius)
} else if (shape === "line") {
shapes.attr("x1", 0).attr("x2", shapeWidth).attr("y1", 0).attr("y2", 0);
} else if (shape === "path") {
shapes.attr("d", path);
}
},
d3_addText: function (svg, enter, labels, classPrefix, labelWidth){
enter.append("text").attr("class", classPrefix + "label");
const text = svg.selectAll(`g.${classPrefix}cell text.${classPrefix}label`)
.data(labels)
.text(d3_identity);
if (labelWidth){
svg.selectAll(`g.${classPrefix}cell text.${classPrefix}label`)
.call(d3_textWrapping, labelWidth)
}
return text
},
d3_calcType: function (scale, ascending, cells, labels, labelFormat, labelDelimiter){
const type = scale.invertExtent ?
d3_quantLegend(scale, labelFormat, labelDelimiter) : scale.ticks ?
d3_linearLegend(scale, cells, labelFormat) : d3_ordinalLegend(scale);
//for d3.scaleSequential that doesn't have a range function
const range = scale.range && scale.range() || scale.domain()
type.labels = d3_mergeLabels(type.labels, labels, scale.domain(), range);
if (ascending) {
type.labels = d3_reverse(type.labels);
type.data = d3_reverse(type.data);
}
return type;
},
d3_filterCells: (type, cellFilter) => {
let filterCells = type.data.map((d, i) => ({ data: d, label: type.labels[i] }))
.filter(cellFilter)
const dataValues = filterCells.map(d => d.data)
const labelValues = filterCells.map(d => d.label)
type.data = type.data.filter(d => dataValues.indexOf(d) !== -1)
type.labels = type.labels.filter(d => labelValues.indexOf(d) !== -1)
return type
},
d3_placement: (orient, cell, cellTrans, text, textTrans, labelAlign) => {
cell.attr("transform", cellTrans);
text.attr("transform", textTrans);
if (orient === "horizontal"){
text.style("text-anchor", labelAlign);
}
},
d3_addEvents: function(cells, dispatcher){
cells.on("mouseover.legend", function (d) { d3_cellOver(dispatcher, d, this); })
.on("mouseout.legend", function (d) { d3_cellOut(dispatcher, d, this); })
.on("click.legend", function (d) { d3_cellClick(dispatcher, d, this); });
},
d3_title: (svg, title, classPrefix, titleWidth) => {
if (title !== ""){
const titleText = svg.selectAll('text.' + classPrefix + 'legendTitle');
titleText.data([title])
.enter()
.append('text')
.attr('class', classPrefix + 'legendTitle');
svg.selectAll('text.' + classPrefix + 'legendTitle')
.text(title)
if (titleWidth){
svg.selectAll('text.' + classPrefix + 'legendTitle')
.call(d3_textWrapping, titleWidth)
}
const cellsSvg = svg.select('.' + classPrefix + 'legendCells')
const yOffset = svg.select('.' + classPrefix + 'legendTitle').nodes()
.map(d => d.getBBox().height)[0],
xOffset = -cellsSvg.nodes().map(function(d) { return d.getBBox().x})[0];
cellsSvg.attr('transform', 'translate(' + xOffset + ',' + (yOffset) + ')');
}
},
d3_defaultLocale: {
format,
formatPrefix
},
d3_defaultFormatSpecifier: '.01f',
d3_defaultDelimiter: 'to'
}