Skip to content

Commit 35d1653

Browse files
committed
Link Colors
There is now an optional setting to add colors to links.
1 parent 8ef3a89 commit 35d1653

File tree

4 files changed

+296
-20
lines changed

4 files changed

+296
-20
lines changed

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "graph-link-types",
33
"name": "Graph Link Types",
4-
"version": "0.2.4",
4+
"version": "0.3.0",
55
"minAppVersion": "1.5.0",
66
"description": "Link types for graph view.",
77
"author": "natefrisch01",

src/linkManager.ts

Lines changed: 238 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
import { ObsidianRenderer, ObsidianLink, LinkPair, GltLink, DataviewLinkType } from 'src/types';
33
import { getAPI } from 'obsidian-dataview';
4-
import { Text, TextStyle } from 'pixi.js';
4+
import { Text, TextStyle , Graphics} from 'pixi.js';
55
// @ts-ignore
66
import extractLinks from 'markdown-link-extractor';
77

@@ -11,9 +11,42 @@ export class LinkManager {
1111
api = getAPI();
1212
currentTheme : string;
1313
textColor : string;
14+
tagColors: Map<string, number>;
15+
categoricalColors: number[] = [
16+
0xF44336, // Red
17+
0x03A9F4, // Light Blue
18+
0xFF9800, // Orange
19+
0x9C27B0, // Purple
20+
0xCDDC39, // Lime
21+
0x3F51B5, // Indigo
22+
0xFFC107, // Amber
23+
0x00BCD4, // Cyan
24+
0xE91E63, // Pink
25+
0x4CAF50, // Green
26+
0xFF5722, // Deep Orange
27+
0x673AB7, // Deep Purple
28+
0x9E9E9E, // Grey
29+
0x2196F3, // Blue
30+
0x8BC34A, // Light Green
31+
0x795548, // Brown
32+
0x009688, // Teal
33+
0x607D8B, // Blue Grey
34+
0xFFEB3B, // Yellow
35+
0x000000 // Black for contrast
36+
]
37+
38+
39+
currentTagColorIndex = 0;
40+
yOffset = 5; // To increment the y position for each legend item
41+
xOffset = 20;
42+
lineHeight = 17; // Height of each line in the legend
43+
lineLength = 40; // Width of the color line
44+
spaceBetweenTextAndLine = 1; // Space between the text and the start of the line
45+
1446

1547
constructor() {
1648
this.linksMap = new Map<string, GltLink>();
49+
this.tagColors = new Map<string, number>();
1750

1851
// Detect changes to the theme.
1952
this.detectThemeChange();
@@ -73,14 +106,15 @@ export class LinkManager {
73106
}
74107
}
75108

76-
addLink(renderer: ObsidianRenderer, obLink: ObsidianLink): void {
109+
addLink(renderer: ObsidianRenderer, obLink: ObsidianLink, tagColors: boolean): void {
77110
const key = this.generateKey(obLink.source.id, obLink.target.id);
78111
const reverseKey = this.generateKey(obLink.target.id, obLink.source.id);
79112
const pairStatus = (obLink.source.id !== obLink.target.id) && this.linksMap.has(reverseKey) ? LinkPair.Second : LinkPair.None;
80113
const newLink: GltLink = {
81114
obsidianLink: obLink,
82115
pairStatus: pairStatus,
83-
pixiText: this.createTextForLink(renderer, obLink, pairStatus)
116+
pixiText: this.initializeLinkText(renderer, obLink, pairStatus),
117+
pixiGraphics: tagColors ? this.initializeLinkGraphics(renderer, obLink, pairStatus) : null,
84118
};
85119

86120
this.linksMap.set(key, newLink);
@@ -99,11 +133,27 @@ export class LinkManager {
99133

100134
const gltLink = this.linksMap.get(key);
101135

136+
let text;
102137
if (gltLink && gltLink.pixiText && renderer.px && renderer.px.stage && renderer.px.stage.children && renderer.px.stage.children.includes(gltLink.pixiText)) {
138+
text = gltLink.pixiText.text;
103139
renderer.px.stage.removeChild(gltLink.pixiText);
104140
gltLink.pixiText.destroy();
105141
}
106142

143+
if (gltLink && gltLink.pixiGraphics && renderer.px && renderer.px.stage && renderer.px.stage.children && renderer.px.stage.children.includes(gltLink.pixiGraphics)) {
144+
renderer.px.stage.removeChild(gltLink.pixiGraphics);
145+
gltLink.pixiGraphics.destroy();
146+
}
147+
148+
let colorKey = text?.replace(/\r?\n/g, "");
149+
if (colorKey) {
150+
if (this.tagColors.has(colorKey)) {
151+
this.tagColors.delete(colorKey);
152+
this.yOffset -= this.lineHeight;
153+
this.currentTagColorIndex -= 1;
154+
}
155+
}
156+
107157
this.linksMap.delete(key);
108158

109159
const reverseLink = this.linksMap.get(reverseKey);
@@ -131,7 +181,7 @@ export class LinkManager {
131181
}
132182

133183
// Update the position of the text on the graph
134-
updateTextPosition(renderer: ObsidianRenderer, link: ObsidianLink): void {
184+
updateLinkText(renderer: ObsidianRenderer, link: ObsidianLink): void {
135185
if (!renderer || !link || !link.source || !link.target) {
136186
// If any of these are null, exit the function
137187
return;
@@ -160,8 +210,73 @@ export class LinkManager {
160210
}
161211
}
162212

213+
// Update the position of the text on the graph
214+
updateLinkGraphics(renderer: ObsidianRenderer, link: ObsidianLink): void {
215+
if (!renderer || !link || !link.source || !link.target) {
216+
// If any of these are null, exit the function
217+
return;
218+
}
219+
const linkKey = this.generateKey(link.source.id, link.target.id);
220+
const gltLink = this.linksMap.get(linkKey);
221+
let graphics;
222+
if (gltLink) {
223+
graphics = gltLink.pixiGraphics;
224+
} else {
225+
return
226+
};
227+
let {nx, ny} = this.calculateNormal(link.source.x, link.source.y, link.target.x, link.target.y);
228+
let {px, py} = this.calculateParallel(link.source.x, link.source.y, link.target.x, link.target.y);
229+
230+
nx *= Math.sqrt(renderer.scale);
231+
ny *= Math.sqrt(renderer.scale);
232+
233+
px *= 8*Math.sqrt(renderer.scale);
234+
py *= 8*Math.sqrt(renderer.scale);
235+
236+
237+
let { x:x1, y:y1 } = this.getLinkToTextCoordinates(link.source.x, link.source.y, renderer.panX, renderer.panY, renderer.scale);
238+
let { x:x2, y:y2 } = this.getLinkToTextCoordinates(link.target.x, link.target.y, renderer.panX, renderer.panY, renderer.scale);
239+
x1 += nx + px;
240+
x2 += nx - px;
241+
y1 += ny + py;
242+
y2 += ny - py;
243+
244+
245+
246+
let color;
247+
248+
// Get the text to display for the link
249+
let linkString: string | null = this.getMetadataKeyForLink(link.source.id, link.target.id);
250+
if (linkString === null) {
251+
252+
} else {
253+
254+
255+
if (link.source.id === link.target.id) {
256+
linkString = "";
257+
}
258+
259+
260+
if (!this.tagColors.has(linkString)) {
261+
color = 0x000000;
262+
} else {
263+
color = this.tagColors.get(linkString);
264+
}
265+
}
266+
267+
268+
if (graphics && renderer.px && renderer.px.stage && renderer.px.stage.children && renderer.px.stage.children.includes(graphics)) {
269+
// Now, update the line whenever needed without creating a new graphics object each time
270+
graphics.clear(); // Clear the previous drawing to prepare for the update
271+
graphics.lineStyle(3/Math.sqrt(renderer.nodeScale), color); // Set the line style (width: 2px, color: black, alpha: 1)
272+
graphics.alpha = .6;
273+
graphics.moveTo(x1, y1); // Move to the starting point of the line (source node)
274+
graphics.lineTo(x2, y2); // Draw the line to the ending point (target node)
275+
}
276+
}
277+
163278
// Create or update text for a given link
164-
private createTextForLink(renderer: ObsidianRenderer, link: ObsidianLink, pairStatus : LinkPair): Text | null{
279+
private initializeLinkText(renderer: ObsidianRenderer, link: ObsidianLink, pairStatus : LinkPair): Text | null{
165280

166281
// Get the text to display for the link
167282
let linkString: string | null = this.getMetadataKeyForLink(link.source.id, link.target.id);
@@ -181,6 +296,8 @@ export class LinkManager {
181296
} else {
182297

183298
}
299+
300+
184301
// Define the style for the text
185302
const textStyle: TextStyle = new TextStyle({
186303
fontFamily: 'Arial',
@@ -189,15 +306,76 @@ export class LinkManager {
189306
});
190307
// Create new text node
191308
const text: Text = new Text(linkString, textStyle);
192-
text.alpha = 0.7;
309+
310+
text.zIndex = 1;
311+
text.alpha = 0.9;
193312
text.anchor.set(0.5, 0.5);
194313

195314

196-
this.updateTextPosition(renderer, link);
315+
316+
this.updateLinkText(renderer, link);
197317
renderer.px.stage.addChild(text);
318+
198319
return text
199320
}
200321

322+
// Create or update text for a given link
323+
private initializeLinkGraphics(renderer: ObsidianRenderer, link: ObsidianLink, pairStatus : LinkPair): Graphics | null{
324+
325+
// Get the text to display for the link
326+
let linkString: string | null = this.getMetadataKeyForLink(link.source.id, link.target.id);
327+
if (linkString === null) {
328+
return null;
329+
} //doesn't add if link is null
330+
331+
332+
if (link.source.id === link.target.id) {
333+
linkString = "";
334+
} else {
335+
336+
let color;
337+
338+
339+
340+
341+
if (!this.tagColors.has(linkString)) {
342+
color = this.categoricalColors[this.currentTagColorIndex];
343+
this.tagColors.set(linkString, color);
344+
// Increment and wrap the index to cycle through colors
345+
this.currentTagColorIndex = (this.currentTagColorIndex + 1) % this.categoricalColors.length;
346+
347+
// Create and add the label
348+
const textL = new Text(linkString, { fontFamily: 'Arial', fontSize: 14, fill: 0x000000 });
349+
textL.x = this.xOffset;
350+
textL.y = this.yOffset;
351+
renderer.px.stage.addChild(textL);
352+
353+
// Calculate the starting x-coordinate for the line, based on the text width
354+
const lineStartX = this.xOffset + textL.width + this.spaceBetweenTextAndLine;
355+
356+
const graphicsL = new Graphics();
357+
graphicsL.lineStyle(2, color, 1); // Assuming 'color' is in a PIXI-compatible format
358+
graphicsL.moveTo(lineStartX, this.yOffset + (this.lineHeight / 2)); // Start a little below the text
359+
graphicsL.lineTo(lineStartX + this.lineLength, this.yOffset + (this.lineHeight / 2)); // 40 pixels wide line
360+
renderer.px.stage.addChild(graphicsL);
361+
this.yOffset += this.lineHeight;
362+
} else {
363+
color = this.tagColors.get(linkString);
364+
}
365+
}
366+
367+
368+
const graphics = new Graphics();
369+
graphics.zIndex = 0;
370+
renderer.px.stage.addChild(graphics); // Add the line to the stage
371+
372+
373+
this.updateLinkGraphics(renderer, link);
374+
375+
376+
return graphics
377+
}
378+
201379
// Utility function to extract the file path from a Markdown link
202380
private extractPathFromMarkdownLink(markdownLink: string | unknown): string {
203381
const links = extractLinks(markdownLink).links;
@@ -223,11 +401,28 @@ export class LinkManager {
223401
// Remove all text nodes from the graph
224402
destroyMap(renderer: ObsidianRenderer): void {
225403
if (this.linksMap.size > 0) {
226-
this.linksMap.forEach((gltLink, linkKey) => {
227-
if (gltLink.pixiText && renderer.px && renderer.px.stage && renderer.px.stage.children && renderer.px.stage.children.includes(gltLink.pixiText)) {
404+
this.linksMap.forEach((gltLink, linkKey) => {
405+
let text;
406+
if (gltLink && gltLink.pixiText && renderer.px && renderer.px.stage && renderer.px.stage.children && renderer.px.stage.children.includes(gltLink.pixiText)) {
407+
text = gltLink.pixiText.text;
228408
renderer.px.stage.removeChild(gltLink.pixiText);
229409
gltLink.pixiText.destroy();
230410
}
411+
412+
if (gltLink && gltLink.pixiGraphics && renderer.px && renderer.px.stage && renderer.px.stage.children && renderer.px.stage.children.includes(gltLink.pixiGraphics)) {
413+
renderer.px.stage.removeChild(gltLink.pixiGraphics);
414+
gltLink.pixiGraphics.destroy();
415+
}
416+
417+
let colorKey = text?.replace(/\r?\n/g, "");
418+
if (colorKey) {
419+
if (this.tagColors.has(colorKey)) {
420+
this.tagColors.delete(colorKey);
421+
this.yOffset -= this.lineHeight;
422+
this.currentTagColorIndex -= 1;
423+
}
424+
}
425+
231426
this.linksMap.delete(linkKey);
232427
});
233428
}
@@ -277,5 +472,38 @@ export class LinkManager {
277472
// Apply scaling and panning to calculate the actual position.
278473
return { x: linkX * scale + panX, y: linkY * scale + panY };
279474
}
280-
475+
476+
private calculateNormal(sourceX: number, sourceY: number, targetX: number, targetY: number): { nx: number; ny: number; } {
477+
// Calculate the direction vector D
478+
const dx = targetX - sourceX;
479+
const dy = targetY - sourceY;
480+
481+
// Calculate the normal vector N by rotating D by 90 degrees
482+
let nx = -dy;
483+
let ny = dx;
484+
485+
// Normalize the normal vector to get a unit vector
486+
const length = Math.sqrt(nx * nx + ny * ny);
487+
nx /= length; // Normalize the x component
488+
ny /= length; // Normalize the y component
489+
490+
491+
return { nx, ny };
492+
}
493+
494+
private calculateParallel(sourceX: number, sourceY: number, targetX: number, targetY: number): { px: number; py: number; } {
495+
// Calculate the direction vector D from source to target
496+
const dx = targetX - sourceX;
497+
const dy = targetY - sourceY;
498+
499+
// No need to rotate the vector for a parallel vector
500+
501+
// Normalize the direction vector to get a unit vector
502+
const length = Math.sqrt(dx * dx + dy * dy);
503+
const px = dx / length; // Normalize the x component
504+
const py = dy / length; // Normalize the y component
505+
506+
return { px, py };
507+
}
508+
281509
}

0 commit comments

Comments
 (0)