-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
Copy pathlabels.ts
156 lines (127 loc) · 4.28 KB
/
labels.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/**
* Sigma.js Labels Heuristics
* ===========================
*
* Miscelleneous heuristics related to label display.
* @module
*/
import Graph from "graphology-types";
import { Dimensions, Coordinates } from "../types";
/**
* Class representing a single candidate for the label grid selection.
*
* It also describes a deterministic way to compare two candidates to assess
* which one is better.
*/
class LabelCandidate {
key: string;
size: number;
constructor(key: string, size: number) {
this.key = key;
this.size = size;
}
static compare(first: LabelCandidate, second: LabelCandidate): number {
// First we compare by size
if (first.size > second.size) return -1;
if (first.size < second.size) return 1;
// Then since no two nodes can have the same key, we use it to
// deterministically tie-break by key
if (first.key > second.key) return 1;
// NOTE: this comparator cannot return 0
return -1;
}
}
/**
* Class representing a 2D spatial grid divided into constant-size cells.
*/
export class LabelGrid {
width = 0;
height = 0;
cellSize = 0;
columns = 0;
rows = 0;
cells: Record<number, Array<LabelCandidate>> = {};
resizeAndClear(dimensions: Dimensions, cellSize: number): void {
this.width = dimensions.width;
this.height = dimensions.height;
this.cellSize = cellSize;
this.columns = Math.ceil(dimensions.width / cellSize);
this.rows = Math.ceil(dimensions.height / cellSize);
this.cells = {};
}
private getIndex(pos: Coordinates): number {
const xIndex = Math.floor(pos.x / this.cellSize);
const yIndex = Math.floor(pos.y / this.cellSize);
return yIndex * this.columns + xIndex;
}
add(key: string, size: number, pos: Coordinates): void {
const candidate = new LabelCandidate(key, size);
const index = this.getIndex(pos);
let cell = this.cells[index];
if (!cell) {
cell = [];
this.cells[index] = cell;
}
cell.push(candidate);
}
organize(): void {
for (const k in this.cells) {
const cell = this.cells[k];
cell.sort(LabelCandidate.compare);
}
}
getLabelsToDisplay(ratio: number, density: number): Array<string> {
// TODO: work on visible nodes to optimize? ^ -> threshold outside so that memoization works?
// TODO: adjust threshold lower, but increase cells a bit?
// TODO: hunt for geom issue in disguise
// TODO: memoize while ratio does not move. method to force recompute
const cellArea = this.cellSize * this.cellSize;
const scaledCellArea = cellArea / ratio / ratio;
const scaledDensity = (scaledCellArea * density) / cellArea;
const labelsToDisplayPerCell = Math.ceil(scaledDensity);
const labels = [];
for (const k in this.cells) {
const cell = this.cells[k];
for (let i = 0; i < Math.min(labelsToDisplayPerCell, cell.length); i++) {
labels.push(cell[i].key);
}
}
return labels;
}
}
/**
* Label heuristic selecting edge labels to display, based on displayed node
* labels
*
* @param {object} params - Parameters:
* @param {Set} displayedNodeLabels - Currently displayed node labels.
* @param {Set} highlightedNodes - Highlighted nodes.
* @param {Graph} graph - The rendered graph.
* @param {string} hoveredNode - Hovered node (optional)
* @return {Array} - The selected labels.
*/
export function edgeLabelsToDisplayFromNodes(params: {
displayedNodeLabels: Set<string>;
highlightedNodes: Set<string>;
graph: Graph;
hoveredNode: string | null;
}): Array<string> {
const { graph, hoveredNode, highlightedNodes, displayedNodeLabels } = params;
const worthyEdges: Array<string> = [];
// TODO: the code below can be optimized using #.forEach and batching the code per adj
// We should display an edge's label if:
// - Any of its extremities is highlighted or hovered
// - Both of its extremities has its label shown
graph.forEachEdge((edge, _, source, target) => {
if (
source === hoveredNode ||
target === hoveredNode ||
highlightedNodes.has(source) ||
highlightedNodes.has(target) ||
(displayedNodeLabels.has(source) && displayedNodeLabels.has(target))
) {
worthyEdges.push(edge);
}
});
return worthyEdges;
}