diff --git a/apps/client/src/widgets/note_map.ts b/apps/client/src/widgets/note_map.ts
index 6259157c6..c06e50465 100644
--- a/apps/client/src/widgets/note_map.ts
+++ b/apps/client/src/widgets/note_map.ts
@@ -30,16 +30,8 @@ const TPL = /*html*/`
`;
type WidgetMode = "type" | "ribbon";
-type MapType = "tree" | "link";
type Data = GraphData>;
-interface Node extends NodeObject {
- id: string;
- name: string;
- type: string;
- color: string;
-}
-
interface Link extends LinkObject {
id: string;
name: string;
@@ -50,43 +42,10 @@ interface Link extends LinkObject {
target: Node;
}
-interface NotesAndRelationsData {
- nodes: Node[];
- links: {
- id: string;
- source: string;
- target: string;
- name: string;
- }[];
-}
-
-// Replace
-interface ResponseLink {
- key: string;
- sourceNoteId: string;
- targetNoteId: string;
- name: string;
-}
-
-interface PostNotesMapResponse {
- notes: string[];
- links: ResponseLink[];
- noteIdToDescendantCountMap: Record;
-}
-
-interface GroupedLink {
- id: string;
- sourceNoteId: string;
- targetNoteId: string;
- names: string[];
-}
-
export default class NoteMapWidget extends NoteContextAwareWidget {
private fixNodes: boolean;
private widgetMode: WidgetMode;
- private mapType?: MapType;
- private cssData!: CssData;
private themeStyle!: string;
private $container!: JQuery;
@@ -147,8 +106,6 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
async refreshWithNote(note: FNote) {
this.$widget.show();
- this.mapType = note.getLabelValue("mapType") === "tree" ? "tree" : "link";
-
//variables for the hover effekt. We have to save the neighbours of a hovered node in a set. Also we need to save the links as well as the hovered node itself
let hoverNode: NodeObject | null = null;
@@ -157,8 +114,6 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
const ForceGraph = (await import("force-graph")).default;
this.graph = new ForceGraph(this.$container[0])
- .width(this.$container.width() || 0)
- .height(this.$container.height() || 0)
.onZoom((zoom) => this.setZoomLevel(zoom.k))
.d3AlphaDecay(0.01)
.d3VelocityDecay(0.08)
@@ -244,15 +199,6 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
.linkCanvasObjectMode(() => "after");
}
- const mapRootNoteId = this.getMapRootNoteId();
-
- const labelValues = (name: string) => this.note?.getLabels(name).map(l => l.value) ?? [];
-
- const excludeRelations = labelValues("mapExcludeRelation");
- const includeRelations = labelValues("mapIncludeRelation");
-
- const data = await this.loadNotesAndRelations(mapRootNoteId, excludeRelations, includeRelations);
-
const nodeLinkRatio = data.nodes.length / data.links.length;
const magnifiedRatio = Math.pow(nodeLinkRatio, 1.5);
const charge = -20 / magnifiedRatio;
@@ -273,22 +219,6 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
this.renderData(data);
}
- getMapRootNoteId(): string {
- if (this.noteId && this.widgetMode === "ribbon") {
- return this.noteId;
- }
-
- let mapRootNoteId = this.note?.getLabelValue("mapRootNoteId");
-
- if (mapRootNoteId === "hoisted") {
- mapRootNoteId = hoistedNoteService.getHoistedNoteId();
- } else if (!mapRootNoteId) {
- mapRootNoteId = appContext.tabManager.getActiveContext()?.parentNoteId;
- }
-
- return mapRootNoteId ?? "";
- }
-
getColorForNode(node: Node) {
if (node.color) {
return node.color;
@@ -393,91 +323,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
ctx.restore();
}
- async loadNotesAndRelations(mapRootNoteId: string, excludeRelations: string[], includeRelations: string[]): Promise {
- const resp = await server.post(`note-map/${mapRootNoteId}/${this.mapType}`, {
- excludeRelations, includeRelations
- });
-
- this.calculateNodeSizes(resp);
-
- const links = this.getGroupedLinks(resp.links);
-
- this.nodes = resp.notes.map(([noteId, title, type, color]) => ({
- id: noteId,
- name: title,
- type: type,
- color: color
- }));
-
- return {
- nodes: this.nodes,
- links: links.map((link) => ({
- id: `${link.sourceNoteId}-${link.targetNoteId}`,
- source: link.sourceNoteId,
- target: link.targetNoteId,
- name: link.names.join(", ")
- }))
- };
- }
-
- getGroupedLinks(links: ResponseLink[]): GroupedLink[] {
- const linksGroupedBySourceTarget: Record = {};
-
- for (const link of links) {
- const key = `${link.sourceNoteId}-${link.targetNoteId}`;
-
- if (key in linksGroupedBySourceTarget) {
- if (!linksGroupedBySourceTarget[key].names.includes(link.name)) {
- linksGroupedBySourceTarget[key].names.push(link.name);
- }
- } else {
- linksGroupedBySourceTarget[key] = {
- id: key,
- sourceNoteId: link.sourceNoteId,
- targetNoteId: link.targetNoteId,
- names: [link.name]
- };
- }
- }
-
- return Object.values(linksGroupedBySourceTarget);
- }
-
- calculateNodeSizes(resp: PostNotesMapResponse) {
- this.noteIdToSizeMap = {};
-
- if (this.mapType === "tree") {
- const { noteIdToDescendantCountMap } = resp;
-
- for (const noteId in noteIdToDescendantCountMap) {
- this.noteIdToSizeMap[noteId] = 4;
-
- const count = noteIdToDescendantCountMap[noteId];
-
- if (count > 0) {
- this.noteIdToSizeMap[noteId] += 1 + Math.round(Math.log(count) / Math.log(1.5));
- }
- }
- } else if (this.mapType === "link") {
- const noteIdToLinkCount: Record = {};
-
- for (const link of resp.links) {
- noteIdToLinkCount[link.targetNoteId] = 1 + (noteIdToLinkCount[link.targetNoteId] || 0);
- }
-
- for (const [noteId] of resp.notes) {
- this.noteIdToSizeMap[noteId] = 4;
-
- if (noteId in noteIdToLinkCount) {
- this.noteIdToSizeMap[noteId] += Math.min(Math.pow(noteIdToLinkCount[noteId], 0.5), 15);
- }
- }
- }
- }
-
renderData(data: Data) {
- this.graph.graphData(data);
-
if (this.widgetMode === "ribbon" && this.note?.type !== "search") {
setTimeout(() => {
this.setDimensions();
diff --git a/apps/client/src/widgets/note_map/NoteMap.tsx b/apps/client/src/widgets/note_map/NoteMap.tsx
index a53893cb3..bf9e2b7b1 100644
--- a/apps/client/src/widgets/note_map/NoteMap.tsx
+++ b/apps/client/src/widgets/note_map/NoteMap.tsx
@@ -1,6 +1,11 @@
import { useEffect, useRef, useState } from "preact/hooks";
import "./NoteMap.css";
-import { rgb2hex } from "./utils";
+import { getMapRootNoteId, NoteMapWidgetMode, rgb2hex } from "./utils";
+import { RefObject } from "preact";
+import FNote from "../../entities/fnote";
+import { useNoteContext, useNoteLabel } from "../react/hooks";
+import ForceGraph, { LinkObject, NodeObject } from "force-graph";
+import { loadNotesAndRelations, NotesAndRelationsData } from "./data";
interface CssData {
fontFamily: string;
@@ -8,11 +13,20 @@ interface CssData {
mutedTextColor: string;
}
-export default function NoteMap() {
+interface NoteMapProps {
+ note: FNote;
+ widgetMode: NoteMapWidgetMode;
+}
+
+type MapType = "tree" | "link";
+
+export default function NoteMap({ note, widgetMode }: NoteMapProps) {
+ console.log("Got note", note);
const containerRef = useRef(null);
const styleResolverRef = useRef(null);
const [ cssData, setCssData ] = useState();
- console.log("Got CSS ", cssData);
+ const [ mapTypeRaw ] = useNoteLabel(note, "mapType");
+ const mapType: MapType = mapTypeRaw === "tree" ? "tree" : "link";
useEffect(() => {
if (!containerRef.current || !styleResolverRef.current) return;
@@ -22,14 +36,54 @@ export default function NoteMap() {
return (
-
-
- Container goes here.
-
+
)
}
+function NoteGraph({ containerRef, note, widgetMode, mapType }: {
+ containerRef: RefObject;
+ note: FNote;
+ widgetMode: NoteMapWidgetMode;
+ mapType: MapType;
+}) {
+ const graphRef = useRef>>();
+ const [ data, setData ] = useState();
+ console.log("Got data ", data);
+
+ // Build the note graph instance.
+ useEffect(() => {
+ const container = containerRef.current;
+ if (!container) return;
+ const { width, height } = container.getBoundingClientRect();
+ const graph = new ForceGraph(container)
+ .width(width)
+ .height(height);
+ graphRef.current = graph;
+
+ const mapRootId = getMapRootNoteId(note.noteId, note, widgetMode);
+ if (!mapRootId) return;
+
+ const labelValues = (name: string) => note.getLabels(name).map(l => l.value) ?? [];
+ const excludeRelations = labelValues("mapExcludeRelation");
+ const includeRelations = labelValues("mapIncludeRelation");
+ loadNotesAndRelations(mapRootId, excludeRelations, includeRelations, mapType).then((data) => {
+ console.log("Got data ", data);
+ });
+
+ return () => container.replaceChildren();
+ }, [ note ]);
+
+ // Render the data.
+ useEffect(() => {
+ if (!graphRef.current || !data) return;
+ graphRef.current.graphData(data);
+ }, [ data ]);
+
+
+ return ;
+}
+
function getCssData(container: HTMLElement, styleResolver: HTMLElement): CssData {
const containerStyle = window.getComputedStyle(container);
const styleResolverStyle = window.getComputedStyle(styleResolver);
diff --git a/apps/client/src/widgets/note_map/data.ts b/apps/client/src/widgets/note_map/data.ts
new file mode 100644
index 000000000..1909f1d49
--- /dev/null
+++ b/apps/client/src/widgets/note_map/data.ts
@@ -0,0 +1,113 @@
+import { NoteMapLink, NoteMapPostResponse } from "@triliumnext/commons";
+import server from "../../services/server";
+import { NodeObject } from "force-graph";
+
+type MapType = "tree" | "link";
+
+interface GroupedLink {
+ id: string;
+ sourceNoteId: string;
+ targetNoteId: string;
+ names: string[];
+}
+
+interface Node extends NodeObject {
+ id: string;
+ name: string;
+ type: string;
+ color: string;
+}
+
+export interface NotesAndRelationsData {
+ nodes: Node[];
+ links: {
+ id: string;
+ source: string;
+ target: string;
+ name: string;
+ }[];
+ noteIdToSizeMap: Record;
+}
+
+export async function loadNotesAndRelations(mapRootNoteId: string, excludeRelations: string[], includeRelations: string[], mapType: MapType): Promise {
+ const resp = await server.post(`note-map/${mapRootNoteId}/${mapType}`, {
+ excludeRelations, includeRelations
+ });
+
+ const noteIdToSizeMap = calculateNodeSizes(resp, mapType);
+ const links = getGroupedLinks(resp.links);
+ const nodes = resp.notes.map(([noteId, title, type, color]) => ({
+ id: noteId,
+ name: title,
+ type: type,
+ color: color
+ }));
+
+ return {
+ noteIdToSizeMap,
+ nodes,
+ links: links.map((link) => ({
+ id: `${link.sourceNoteId}-${link.targetNoteId}`,
+ source: link.sourceNoteId,
+ target: link.targetNoteId,
+ name: link.names.join(", ")
+ }))
+ };
+}
+
+function calculateNodeSizes(resp: NoteMapPostResponse, mapType: MapType) {
+ const noteIdToSizeMap: Record = {};
+
+ if (mapType === "tree") {
+ const { noteIdToDescendantCountMap } = resp;
+
+ for (const noteId in noteIdToDescendantCountMap) {
+ noteIdToSizeMap[noteId] = 4;
+
+ const count = noteIdToDescendantCountMap[noteId];
+
+ if (count > 0) {
+ noteIdToSizeMap[noteId] += 1 + Math.round(Math.log(count) / Math.log(1.5));
+ }
+ }
+ } else if (mapType === "link") {
+ const noteIdToLinkCount: Record = {};
+
+ for (const link of resp.links) {
+ noteIdToLinkCount[link.targetNoteId] = 1 + (noteIdToLinkCount[link.targetNoteId] || 0);
+ }
+
+ for (const [noteId] of resp.notes) {
+ noteIdToSizeMap[noteId] = 4;
+
+ if (noteId in noteIdToLinkCount) {
+ noteIdToSizeMap[noteId] += Math.min(Math.pow(noteIdToLinkCount[noteId], 0.5), 15);
+ }
+ }
+ }
+
+ return noteIdToSizeMap;
+}
+
+function getGroupedLinks(links: NoteMapLink[]): GroupedLink[] {
+ const linksGroupedBySourceTarget: Record = {};
+
+ for (const link of links) {
+ const key = `${link.sourceNoteId}-${link.targetNoteId}`;
+
+ if (key in linksGroupedBySourceTarget) {
+ if (!linksGroupedBySourceTarget[key].names.includes(link.name)) {
+ linksGroupedBySourceTarget[key].names.push(link.name);
+ }
+ } else {
+ linksGroupedBySourceTarget[key] = {
+ id: key,
+ sourceNoteId: link.sourceNoteId,
+ targetNoteId: link.targetNoteId,
+ names: [link.name]
+ };
+ }
+ }
+
+ return Object.values(linksGroupedBySourceTarget);
+}
diff --git a/apps/client/src/widgets/note_map/utils.ts b/apps/client/src/widgets/note_map/utils.ts
index 70d1caab6..8014df414 100644
--- a/apps/client/src/widgets/note_map/utils.ts
+++ b/apps/client/src/widgets/note_map/utils.ts
@@ -1,6 +1,28 @@
+import appContext from "../../components/app_context";
+import FNote from "../../entities/fnote";
+import hoisted_note from "../../services/hoisted_note";
+
+export type NoteMapWidgetMode = "ribbon" | "hoisted";
+
export function rgb2hex(rgb: string) {
return `#${(rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) || [])
.slice(1)
.map((n) => parseInt(n, 10).toString(16).padStart(2, "0"))
.join("")}`;
}
+
+export function getMapRootNoteId(noteId: string, note: FNote, widgetMode: NoteMapWidgetMode): string | null {
+ if (noteId && widgetMode === "ribbon") {
+ return noteId;
+ }
+
+ let mapRootNoteId = note?.getLabelValue("mapRootNoteId");
+
+ if (mapRootNoteId === "hoisted") {
+ mapRootNoteId = hoisted_note.getHoistedNoteId();
+ } else if (!mapRootNoteId) {
+ mapRootNoteId = appContext.tabManager.getActiveContext()?.parentNoteId ?? null;
+ }
+
+ return mapRootNoteId;
+}
diff --git a/apps/client/src/widgets/ribbon/NoteMapTab.tsx b/apps/client/src/widgets/ribbon/NoteMapTab.tsx
index 93317bef7..88ee108ad 100644
--- a/apps/client/src/widgets/ribbon/NoteMapTab.tsx
+++ b/apps/client/src/widgets/ribbon/NoteMapTab.tsx
@@ -7,7 +7,7 @@ import NoteMap from "../note_map/NoteMap";
const SMALL_SIZE_HEIGHT = "300px";
-export default function NoteMapTab({ noteContext }: TabContext) {
+export default function NoteMapTab({ note }: TabContext) {
const [ isExpanded, setExpanded ] = useState(false);
const [ height, setHeight ] = useState(SMALL_SIZE_HEIGHT);
const containerRef = useRef(null);
@@ -26,7 +26,7 @@ export default function NoteMapTab({ noteContext }: TabContext) {
return (
-
+ {note &&
}
{!isExpanded ? (
;
}
+
+export interface NoteMapLink {
+ key: string;
+ sourceNoteId: string;
+ targetNoteId: string;
+ name: string;
+}
+
+export interface NoteMapPostResponse {
+ notes: string[];
+ links: NoteMapLink[];
+ noteIdToDescendantCountMap: Record;
+}