2024-11-30 19:57:06 +01:00
import server from "../services/server.js" ;
import attributeService from "../services/attributes.js" ;
import hoistedNoteService from "../services/hoisted_note.js" ;
2025-01-17 21:25:36 +02:00
import appContext , { type EventData } from "../components/app_context.js" ;
2024-11-30 19:57:06 +01:00
import NoteContextAwareWidget from "./note_context_aware_widget.js" ;
import linkContextMenuService from "../menus/link_context_menu.js" ;
import utils from "../services/utils.js" ;
import { t } from "../services/i18n.js" ;
2025-01-17 21:25:36 +02:00
import type ForceGraph from "force-graph" ;
import type { GraphData , LinkObject , NodeObject } from "force-graph" ;
import type FNote from "../entities/fnote.js" ;
2022-12-26 10:52:28 +01:00
const esc = utils . escapeHtml ;
2021-09-22 21:11:36 +02:00
2025-02-23 06:17:57 +02:00
const TPL = ` <div class="note-map-widget">
2021-05-28 23:52:42 +02:00
< style >
2022-08-02 17:17:27 +02:00
. note - detail - note - map {
2021-09-22 21:11:36 +02:00
height : 100 % ;
2022-08-02 17:17:27 +02:00
overflow : hidden ;
2021-05-28 23:52:42 +02:00
}
2025-01-09 00:04:09 +02:00
2021-09-22 21:11:36 +02:00
. map - type - switcher {
2025-01-09 00:04:09 +02:00
position : absolute ;
top : 10px ;
left : 10px ;
2021-10-21 21:28:44 +02:00
z - index : 10 ; /* should be below dropdown (note actions) */
2021-05-28 23:52:42 +02:00
}
2025-01-09 00:04:09 +02:00
2022-08-05 16:44:26 +02:00
. map - type - switcher button . bx {
font - size : 130 % ;
padding : 1px 10 px 1 px 10 px ;
2021-06-02 21:23:40 +02:00
}
2024-11-30 19:20:07 +01:00
2024-11-30 19:57:06 +01:00
/* Style Ui Element to Drag Nodes */
2024-11-30 19:20:07 +01:00
. fixnodes - type - switcher {
2025-01-09 00:04:09 +02:00
position : absolute ;
2025-02-23 06:17:57 +02:00
display : flex ;
align - items : center ;
bottom : 10px ;
left : 10px ;
2024-11-30 19:20:07 +01:00
z - index : 10 ; /* should be below dropdown (note actions) */
2025-02-23 06:17:57 +02:00
border - radius : .2rem ;
}
. fixnodes - type - switcher button . toggled {
background : var ( -- active - item - background - color ) ;
color : var ( -- active - item - text - color ) ;
2024-11-30 19:20:07 +01:00
}
2024-11-30 19:57:06 +01:00
/* Start of styling the slider */
2025-02-23 06:17:57 +02:00
. fixnodes - type - switcher input [ type = "range" ] {
2025-01-09 00:04:09 +02:00
2024-11-30 19:20:07 +01:00
/* removing default appearance */
- webkit - appearance : none ;
2025-01-09 00:04:09 +02:00
appearance : none ;
2024-11-30 19:20:07 +01:00
margin - left : 15px ;
2025-02-23 06:17:57 +02:00
width : 150px ;
2024-11-30 19:20:07 +01:00
}
2024-11-30 19:57:06 +01:00
/* Changing slider tracker */
2025-02-23 06:17:57 +02:00
. fixnodes - type - switcher input [ type = "range" ] : : - webkit - slider - runnable - track {
height : 4px ;
background - color : var ( -- main - border - color ) ;
border - radius : 4px ;
}
/* Changing Slider Thumb */
. fixnodes - type - switcher input [ type = "range" ] : : - webkit - slider - thumb {
/* removing default appearance */
- webkit - appearance : none ;
appearance : none ;
/* creating a custom design */
height : 15px ;
width : 15px ;
margin - top : - 5 px ;
background - color : var ( -- accented - background - color ) ;
border : 1px solid var ( -- main - text - color ) ;
border - radius : 50 % ;
2024-11-30 19:20:07 +01:00
}
2025-02-23 06:17:57 +02:00
. fixnodes - type - switcher input [ type = "range" ] : : - moz - range - track {
background - color : var ( -- main - border - color ) ;
border - radius : 4px ;
}
2024-11-30 19:20:07 +01:00
2025-02-23 06:17:57 +02:00
. fixnodes - type - switcher input [ type = "range" ] : : - moz - range - thumb {
background - color : var ( -- accented - background - color ) ;
border - color : var ( -- main - text - color ) ;
height : 10px ;
width : 10px ;
}
2024-11-30 19:57:06 +01:00
/* End of styling the slider */
2021-05-28 23:52:42 +02:00
< / style >
2025-01-09 00:04:09 +02:00
2022-07-24 14:30:42 +02:00
< div class = "btn-group btn-group-sm map-type-switcher" role = "group" >
2025-02-23 06:17:57 +02:00
< button type = "button" class = "btn bx bx-network-chart tn-tool-button" title = "${t(" note - map.button - link - map ")}" data-type = "link" > < / button >
< button type = "button" class = "btn bx bx-sitemap tn-tool-button" title = "${t(" note - map.button - tree - map ")}" data-type = "tree" > < / button >
2024-11-30 19:20:07 +01:00
< / div >
2024-11-30 19:57:06 +01:00
< ! UI for dragging Notes and link force >
2025-02-23 06:17:57 +02:00
< div class = " btn-group-sm fixnodes-type-switcher" role = "group" >
< button type = "button" data-toggle = "button" class = "btn bx bx-lock-alt tn-tool-button" title = "${t(" note_map.fix - nodes ")}" data-type = "moveable" > < / button >
< input type = "range" class = "slider" min = "1" title = "${t(" note_map.link - distance ")}" max = "100" value = "40" >
2021-09-22 21:11:36 +02:00
< / div >
2021-06-27 12:53:05 +02:00
2021-09-22 22:25:39 +02:00
< div class = "style-resolver" > < / div >
2021-09-22 21:11:36 +02:00
< div class = "note-map-container" > < / div >
< / div > ` ;
2021-05-28 23:19:11 +02:00
2025-01-17 21:25:36 +02:00
type WidgetMode = "type" | "ribbon" ;
type MapType = "tree" | "link" ;
type Data = GraphData < NodeObject , LinkObject < NodeObject > > ;
interface Node extends NodeObject {
id : string ;
name : string ;
type : string ;
color : string ;
}
interface Link extends LinkObject < NodeObject > {
id : string ;
name : string ;
x : number ;
y : number ;
source : Node ;
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 < string , number > ;
}
interface GroupedLink {
id : string ;
sourceNoteId : string ;
targetNoteId : string ;
names : string [ ]
}
interface CssData {
fontFamily : string ;
textColor : string ;
mutedTextColor : string ;
}
2021-09-22 21:11:36 +02:00
export default class NoteMapWidget extends NoteContextAwareWidget {
2025-01-17 21:25:36 +02:00
private fixNodes : boolean ;
private widgetMode : WidgetMode ;
private mapType? : MapType ;
private cssData ! : CssData ;
private themeStyle ! : string ;
private $container ! : JQuery < HTMLElement > ;
private $styleResolver ! : JQuery < HTMLElement > ;
2025-02-23 06:17:57 +02:00
private $fixNodesButton ! : JQuery < HTMLElement > ;
2025-01-19 21:21:13 +02:00
graph ! : ForceGraph ;
2025-01-17 21:25:36 +02:00
private noteIdToSizeMap ! : Record < string , number > ;
private zoomLevel ! : number ;
private nodes ! : Node [ ] ;
constructor ( widgetMode : WidgetMode ) {
2021-09-22 22:25:39 +02:00
super ( ) ;
2024-11-30 19:57:06 +01:00
this . fixNodes = false ; // needed to save the status of the UI element. Is set later in the code
2021-09-22 22:25:39 +02:00
this . widgetMode = widgetMode ; // 'type' or 'ribbon'
}
2021-05-28 23:19:11 +02:00
doRender() {
this . $widget = $ ( TPL ) ;
2021-05-31 21:20:30 +02:00
2021-10-08 16:38:37 +02:00
const documentStyle = window . getComputedStyle ( document . documentElement ) ;
2025-01-09 18:07:02 +02:00
this . themeStyle = documentStyle . getPropertyValue ( "--theme-style" ) ? . trim ( ) ;
2021-10-08 16:38:37 +02:00
2024-11-30 19:57:06 +01:00
this . $container = this . $widget . find ( ".note-map-container" ) ;
2025-01-09 18:07:02 +02:00
this . $styleResolver = this . $widget . find ( ".style-resolver" ) ;
2025-02-23 06:17:57 +02:00
this . $fixNodesButton = this . $widget . find ( ".fixnodes-type-switcher > button" ) ;
2021-05-31 23:38:47 +02:00
2023-08-15 22:50:13 +02:00
new ResizeObserver ( ( ) = > this . setDimensions ( ) ) . observe ( this . $container [ 0 ] ) ;
2021-05-31 21:20:30 +02:00
2025-01-09 18:07:02 +02:00
this . $widget . find ( ".map-type-switcher button" ) . on ( "click" , async ( e ) = > {
2024-11-30 19:57:06 +01:00
const type = $ ( e . target ) . closest ( "button" ) . attr ( "data-type" ) ;
2025-01-17 21:25:36 +02:00
await attributeService . setLabel ( this . noteId ? ? "" , "mapType" , type ) ;
2021-05-31 21:20:30 +02:00
} ) ;
2025-02-23 06:17:57 +02:00
// Reading the status of the Drag nodes Ui element. Changing it´ s color when activated.
// Reading Force value of the link distance.
this . $fixNodesButton . on ( "click" , async ( event ) = > {
2024-11-30 19:20:07 +01:00
this . fixNodes = ! this . fixNodes ;
2025-02-23 06:17:57 +02:00
this . $fixNodesButton . toggleClass ( "toggled" , this . fixNodes ) ;
2024-11-30 19:20:07 +01:00
} ) ;
2021-09-22 21:11:36 +02:00
super . doRender ( ) ;
2021-05-31 23:38:47 +02:00
}
2022-08-02 17:17:27 +02:00
setDimensions() {
2025-01-09 18:07:02 +02:00
if ( ! this . graph ) {
// no graph has been even rendered
2021-09-22 21:11:36 +02:00
return ;
}
2021-09-22 22:25:39 +02:00
const $parent = this . $widget . parent ( ) ;
2021-05-31 23:38:47 +02:00
2025-01-17 21:25:36 +02:00
this . graph
. height ( $parent . height ( ) || 0 )
. width ( $parent . width ( ) || 0 ) ;
2021-05-28 23:19:11 +02:00
}
2025-01-17 21:25:36 +02:00
async refreshWithNote ( note : FNote ) {
2021-09-22 21:11:36 +02:00
this . $widget . show ( ) ;
2021-05-31 21:31:07 +02:00
2025-01-17 21:25:36 +02:00
this . cssData = {
2024-11-30 19:57:06 +01:00
fontFamily : this.$container.css ( "font-family" ) ,
textColor : this.rgb2hex ( this . $container . css ( "color" ) ) ,
mutedTextColor : this.rgb2hex ( this . $styleResolver . css ( "color" ) )
2021-09-22 22:25:39 +02:00
} ;
2025-01-17 21:25:36 +02:00
this . mapType = note . getLabelValue ( "mapType" ) === "tree" ? "tree" : "link" ;
2021-06-01 22:03:38 +02:00
2024-11-30 19:57:06 +01:00
//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
2024-11-30 19:20:07 +01:00
2025-01-17 21:25:36 +02:00
let hoverNode : NodeObject | null = null ;
2024-11-30 19:20:07 +01:00
const highlightLinks = new Set ( ) ;
const neighbours = new Set ( ) ;
2025-01-17 20:21:31 +02:00
const ForceGraph = ( await import ( "force-graph" ) ) . default ;
2025-01-17 21:25:36 +02:00
this . graph = new ForceGraph ( this . $container [ 0 ] )
. width ( this . $container . width ( ) || 0 )
. height ( this . $container . height ( ) || 0 )
2025-01-09 18:07:02 +02:00
. onZoom ( ( zoom ) = > this . setZoomLevel ( zoom . k ) )
2021-09-22 21:11:36 +02:00
. d3AlphaDecay ( 0.01 )
. d3VelocityDecay ( 0.08 )
2024-11-30 19:20:07 +01:00
//Code to fixate nodes when dragged
2025-01-09 18:07:02 +02:00
. onNodeDragEnd ( ( node ) = > {
2024-11-30 19:20:07 +01:00
if ( this . fixNodes ) {
node . fx = node . x ;
node . fy = node . y ;
} else {
2025-01-17 21:25:36 +02:00
node . fx = undefined ;
node . fy = undefined ;
2024-11-30 19:20:07 +01:00
}
} )
2024-11-30 19:57:06 +01:00
//check if hovered and set the hovernode variable, saving the hovered node object into it. Clear links variable everytime you hover. Without clearing links will stay highlighted
2025-01-09 18:07:02 +02:00
. onNodeHover ( ( node ) = > {
2024-11-30 19:20:07 +01:00
hoverNode = node || null ;
highlightLinks . clear ( ) ;
} )
2025-01-09 00:04:09 +02:00
2024-11-30 19:57:06 +01:00
// set link width to immitate a highlight effekt. Checking the condition if any links are saved in the previous defined set highlightlinks
2025-01-09 18:07:02 +02:00
. linkWidth ( ( link ) = > ( highlightLinks . has ( link ) ? 3 : 0.4 ) )
2025-01-17 21:25:36 +02:00
. linkColor ( ( link ) = > ( highlightLinks . has ( link ) ? "white" : this . cssData . mutedTextColor ) )
2024-11-30 19:57:06 +01:00
. linkDirectionalArrowLength ( 4 )
. linkDirectionalArrowRelPos ( 0.95 )
// main code for highlighting hovered nodes and neighbours. here we "style" the nodes. the nodes are rendered several hundred times per second.
2025-01-17 21:25:36 +02:00
. nodeCanvasObject ( ( _node , ctx ) = > {
const node = _node as Node ;
2025-01-09 18:07:02 +02:00
if ( hoverNode == node ) {
//paint only hovered node
this . paintNode ( node , "#661822" , ctx ) ;
2024-11-30 19:57:06 +01:00
neighbours . clear ( ) ; //clearing neighbours or the effect would be maintained after hovering is over
2025-01-17 21:25:36 +02:00
for ( const _link of data . links ) {
const link = _link as unknown as Link ;
2025-01-09 18:07:02 +02:00
//check if node is part of a link in the canvas, if so add it´ s neighbours and related links to the previous defined variables to paint the nodes
2024-11-30 19:20:07 +01:00
if ( link . source . id == node . id || link . target . id == node . id ) {
neighbours . add ( link . source ) ;
neighbours . add ( link . target ) ;
highlightLinks . add ( link ) ;
neighbours . delete ( node ) ;
}
}
2025-01-09 18:07:02 +02:00
} else if ( neighbours . has ( node ) && hoverNode != null ) {
//paint neighbours
this . paintNode ( node , "#9d6363" , ctx ) ;
2024-11-30 19:20:07 +01:00
} else {
2024-11-30 19:57:06 +01:00
this . paintNode ( node , this . getColorForNode ( node ) , ctx ) ; //paint rest of nodes in canvas
2024-11-30 19:20:07 +01:00
}
} )
2024-11-30 19:57:06 +01:00
2025-01-17 21:25:36 +02:00
. nodePointerAreaPaint ( ( node , _ , ctx ) = > this . paintNode ( node as Node , this . getColorForNode ( node as Node ) , ctx ) )
2021-05-31 21:31:07 +02:00
. nodePointerAreaPaint ( ( node , color , ctx ) = > {
2025-01-17 21:25:36 +02:00
if ( ! node . id ) {
return ;
}
2021-05-31 21:31:07 +02:00
ctx . fillStyle = color ;
ctx . beginPath ( ) ;
2025-01-17 21:25:36 +02:00
if ( node . x && node . y ) {
ctx . arc ( node . x , node . y ,
this . noteIdToSizeMap [ node . id ] , 0 ,
2 * Math . PI , false ) ;
}
2021-05-31 21:31:07 +02:00
ctx . fill ( ) ;
} )
2025-01-17 21:25:36 +02:00
. nodeLabel ( ( node ) = > esc ( ( node as Node ) . name ) )
2021-09-22 21:11:36 +02:00
. maxZoom ( 7 )
2021-09-29 12:39:28 +02:00
. warmupTicks ( 30 )
2025-01-17 21:25:36 +02:00
. onNodeClick ( ( node ) = > {
if ( node . id ) {
appContext . tabManager . getActiveContext ( ) . setNote ( ( node as Node ) . id ) ;
}
} )
. onNodeRightClick ( ( node , e ) = > {
if ( node . id ) {
linkContextMenuService . openContextMenu ( ( node as Node ) . id , e ) ;
}
} ) ;
2021-05-31 21:31:07 +02:00
2025-01-09 18:07:02 +02:00
if ( this . mapType === "link" ) {
2021-09-22 21:11:36 +02:00
this . graph
2025-01-17 21:25:36 +02:00
. linkLabel ( ( l ) = > ` ${ esc ( ( l as Link ) . source . name ) } - <strong> ${ esc ( ( l as Link ) . name ) } </strong> - ${ esc ( ( l as Link ) . target . name ) } ` )
. linkCanvasObject ( ( link , ctx ) = > this . paintLink ( link as Link , ctx ) )
2024-11-30 19:57:06 +01:00
. linkCanvasObjectMode ( ( ) = > "after" ) ;
2021-09-22 21:11:36 +02:00
}
2021-05-31 21:31:07 +02:00
2022-11-05 22:32:50 +01:00
const mapRootNoteId = this . getMapRootNoteId ( ) ;
2021-09-28 13:27:21 +02:00
const data = await this . loadNotesAndRelations ( mapRootNoteId ) ;
2024-11-30 19:57:06 +01:00
2021-09-28 13:27:21 +02:00
const nodeLinkRatio = data . nodes . length / data . links . length ;
2021-10-03 11:04:56 +02:00
const magnifiedRatio = Math . pow ( nodeLinkRatio , 1.5 ) ;
const charge = - 20 / magnifiedRatio ;
const boundedCharge = Math . min ( - 3 , charge ) ;
2024-11-30 19:57:06 +01:00
let distancevalue = 40 ; // default value for the link force of the nodes
2024-11-30 19:20:07 +01:00
2025-01-09 18:07:02 +02:00
this . $widget . find ( ".fixnodes-type-switcher input" ) . on ( "change" , async ( e ) = > {
2025-01-17 21:25:36 +02:00
distancevalue = parseInt ( e . target . closest ( "input" ) ? . value ? ? "0" ) ;
this . graph . d3Force ( "link" ) ? . distance ( distancevalue ) ;
2024-11-30 19:20:07 +01:00
this . renderData ( data ) ;
} ) ;
2021-09-28 13:27:21 +02:00
2025-01-17 21:25:36 +02:00
this . graph . d3Force ( "center" ) ? . strength ( 0.2 ) ;
this . graph . d3Force ( "charge" ) ? . strength ( boundedCharge ) ;
this . graph . d3Force ( "charge" ) ? . distanceMax ( 1000 ) ;
2024-11-30 19:57:06 +01:00
2021-09-22 22:25:39 +02:00
this . renderData ( data ) ;
}
2025-01-17 21:25:36 +02:00
getMapRootNoteId ( ) : string {
if ( this . noteId && this . widgetMode === "ribbon" ) {
2021-09-22 22:25:39 +02:00
return this . noteId ;
}
2025-01-17 21:25:36 +02:00
let mapRootNoteId = this . note ? . getLabelValue ( "mapRootNoteId" ) ;
2021-05-31 21:31:07 +02:00
2025-01-09 18:07:02 +02:00
if ( mapRootNoteId === "hoisted" ) {
2021-09-22 21:11:36 +02:00
mapRootNoteId = hoistedNoteService . getHoistedNoteId ( ) ;
2021-09-22 22:25:39 +02:00
} else if ( ! mapRootNoteId ) {
2021-09-22 21:11:36 +02:00
mapRootNoteId = appContext . tabManager . getActiveContext ( ) . parentNoteId ;
}
2025-01-17 21:25:36 +02:00
return mapRootNoteId ? ? "" ;
2021-05-31 21:31:07 +02:00
}
2025-01-17 21:25:36 +02:00
getColorForNode ( node : Node ) {
2022-12-20 20:41:51 +01:00
if ( node . color ) {
return node . color ;
2025-01-09 18:07:02 +02:00
} else if ( this . widgetMode === "ribbon" && node . id === this . noteId ) {
return "red" ; // subtree root mark as red
2022-12-20 20:41:51 +01:00
} else {
return this . generateColorFromString ( node . type ) ;
}
}
2025-01-17 21:25:36 +02:00
generateColorFromString ( str : string ) {
2024-11-30 19:57:06 +01:00
if ( this . themeStyle === "dark" ) {
2023-06-23 00:26:47 +08:00
str = ` 0 ${ str } ` ; // magic lightning modifier
2021-10-08 16:45:21 +02:00
}
2021-09-22 21:11:36 +02:00
let hash = 0 ;
for ( let i = 0 ; i < str . length ; i ++ ) {
hash = str . charCodeAt ( i ) + ( ( hash << 5 ) - hash ) ;
2021-06-01 22:03:38 +02:00
}
2021-10-08 16:38:37 +02:00
2025-01-09 18:07:02 +02:00
let color = "#" ;
2021-10-08 16:45:21 +02:00
for ( let i = 0 ; i < 3 ; i ++ ) {
2025-01-09 18:07:02 +02:00
const value = ( hash >> ( i * 8 ) ) & 0xff ;
2021-10-08 16:38:37 +02:00
2025-01-09 18:07:02 +02:00
color += ` 00 ${ value . toString ( 16 ) } ` . substr ( - 2 ) ;
2021-05-31 21:31:07 +02:00
}
2021-10-08 16:45:21 +02:00
return color ;
2021-05-31 21:31:07 +02:00
}
2021-05-28 23:19:11 +02:00
2025-01-17 21:25:36 +02:00
rgb2hex ( rgb : string ) {
return ` # ${ ( rgb . match ( /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/ ) || [ ] )
2021-09-22 21:11:36 +02:00
. slice ( 1 )
2025-01-09 18:07:02 +02:00
. map ( ( n ) = > parseInt ( n , 10 ) . toString ( 16 ) . padStart ( 2 , "0" ) )
. join ( "" ) } ` ;
2021-09-22 21:11:36 +02:00
}
2021-05-28 23:19:11 +02:00
2025-01-17 21:25:36 +02:00
setZoomLevel ( level : number ) {
2021-09-22 21:11:36 +02:00
this . zoomLevel = level ;
}
2021-06-01 22:03:38 +02:00
2025-01-17 21:25:36 +02:00
paintNode ( node : Node , color : string , ctx : CanvasRenderingContext2D ) {
2025-01-09 18:07:02 +02:00
const { x , y } = node ;
2025-01-17 21:25:36 +02:00
if ( ! x || ! y ) {
return ;
}
2021-09-22 21:11:36 +02:00
const size = this . noteIdToSizeMap [ node . id ] ;
2021-06-01 22:03:38 +02:00
2022-12-20 20:41:51 +01:00
ctx . fillStyle = color ;
2021-09-22 21:11:36 +02:00
ctx . beginPath ( ) ;
2025-01-09 18:07:02 +02:00
ctx . arc ( x , y , size * 0.8 , 0 , 2 * Math . PI , false ) ;
2021-09-22 21:11:36 +02:00
ctx . fill ( ) ;
2021-05-31 23:38:47 +02:00
2025-01-09 18:07:02 +02:00
const toRender = this . zoomLevel > 2 || ( this . zoomLevel > 1 && size > 6 ) || ( this . zoomLevel > 0.3 && size > 10 ) ;
2021-05-31 21:31:07 +02:00
2021-09-22 21:11:36 +02:00
if ( ! toRender ) {
return ;
}
2021-05-31 23:38:47 +02:00
2025-01-17 21:25:36 +02:00
ctx . fillStyle = this . cssData . textColor ;
ctx . font = ` ${ size } px ${ this . cssData . fontFamily } ` ;
2025-01-09 18:07:02 +02:00
ctx . textAlign = "center" ;
ctx . textBaseline = "middle" ;
2021-05-31 21:31:07 +02:00
2021-09-22 21:11:36 +02:00
let title = node . name ;
if ( title . length > 15 ) {
2022-12-21 15:19:05 +01:00
title = ` ${ title . substr ( 0 , 15 ) } ... ` ;
2021-06-01 22:03:38 +02:00
}
2021-05-31 21:31:07 +02:00
2021-09-22 21:11:36 +02:00
ctx . fillText ( title , x , y + Math . round ( size * 1.5 ) ) ;
2021-05-28 23:19:11 +02:00
}
2025-01-17 21:25:36 +02:00
paintLink ( link : Link , ctx : CanvasRenderingContext2D ) {
2021-06-02 21:39:18 +02:00
if ( this . zoomLevel < 5 ) {
2021-05-31 21:31:07 +02:00
return ;
}
2025-01-17 21:25:36 +02:00
ctx . font = ` 3px ${ this . cssData . fontFamily } ` ;
2025-01-09 18:07:02 +02:00
ctx . textAlign = "center" ;
ctx . textBaseline = "middle" ;
2025-01-17 21:25:36 +02:00
ctx . fillStyle = this . cssData . mutedTextColor ;
2021-05-31 21:31:07 +02:00
2025-01-09 18:07:02 +02:00
const { source , target } = link ;
2025-01-17 21:25:36 +02:00
if ( typeof source !== "object" || typeof target !== "object" ) {
return ;
}
2021-05-31 21:31:07 +02:00
2025-01-17 21:25:36 +02:00
if ( source . x && source . y && target . x && target . y ) {
const x = ( ( source . x ) + ( target . x ) ) / 2 ;
const y = ( ( source . y ) + ( target . y ) ) / 2 ;
ctx . save ( ) ;
ctx . translate ( x , y ) ;
2024-11-30 19:57:06 +01:00
2025-01-17 21:25:36 +02:00
const deltaY = ( source . y ) - ( target . y ) ;
const deltaX = ( source . x ) - ( target . x ) ;
2021-05-31 21:31:07 +02:00
2025-01-17 21:25:36 +02:00
let angle = Math . atan2 ( deltaY , deltaX ) ;
let moveY = 2 ;
2021-05-31 21:31:07 +02:00
2025-01-17 21:25:36 +02:00
if ( angle < - Math . PI / 2 || angle > Math . PI / 2 ) {
angle += Math . PI ;
moveY = - 2 ;
}
2021-05-31 21:31:07 +02:00
2025-01-17 21:25:36 +02:00
ctx . rotate ( angle ) ;
ctx . fillText ( link . name , 0 , moveY ) ;
2021-05-31 21:31:07 +02:00
}
ctx . restore ( ) ;
}
2025-01-17 21:25:36 +02:00
async loadNotesAndRelations ( mapRootNoteId : string ) : Promise < NotesAndRelationsData > {
const resp = await server . post < PostNotesMapResponse > ( ` note-map/ ${ mapRootNoteId } / ${ this . mapType } ` ) ;
2021-05-31 21:31:07 +02:00
2021-09-22 23:02:38 +02:00
this . calculateNodeSizes ( resp ) ;
2021-09-22 21:11:36 +02:00
2021-09-22 22:41:31 +02:00
const links = this . getGroupedLinks ( resp . links ) ;
2024-11-30 19:57:06 +01:00
2022-12-20 20:41:51 +01:00
this . nodes = resp . notes . map ( ( [ noteId , title , type , color ] ) = > ( {
2021-09-22 22:41:31 +02:00
id : noteId ,
name : title ,
type : type ,
2022-12-20 20:41:51 +01:00
color : color
2021-09-22 22:41:31 +02:00
} ) ) ;
2021-05-31 21:31:07 +02:00
2021-09-22 22:41:31 +02:00
return {
nodes : this.nodes ,
2025-01-09 18:07:02 +02:00
links : links.map ( ( link ) = > ( {
2022-11-05 22:32:50 +01:00
id : ` ${ link . sourceNoteId } - ${ link . targetNoteId } ` ,
2021-09-22 22:41:31 +02:00
source : link.sourceNoteId ,
target : link.targetNoteId ,
2024-11-30 19:57:06 +01:00
name : link.names.join ( ", " )
2021-09-22 22:41:31 +02:00
} ) )
} ;
}
2021-05-31 21:31:07 +02:00
2025-01-17 21:25:36 +02:00
getGroupedLinks ( links : ResponseLink [ ] ) : GroupedLink [ ] {
const linksGroupedBySourceTarget : Record < string , GroupedLink > = { } ;
2021-09-22 21:11:36 +02:00
2021-09-22 22:41:31 +02:00
for ( const link of links ) {
2021-09-22 21:11:36 +02:00
const key = ` ${ link . sourceNoteId } - ${ link . targetNoteId } ` ;
if ( key in linksGroupedBySourceTarget ) {
if ( ! linksGroupedBySourceTarget [ key ] . names . includes ( link . name ) ) {
linksGroupedBySourceTarget [ key ] . names . push ( link . name ) ;
}
2021-09-22 22:41:31 +02:00
} else {
2021-09-22 21:11:36 +02:00
linksGroupedBySourceTarget [ key ] = {
id : key ,
sourceNoteId : link.sourceNoteId ,
targetNoteId : link.targetNoteId ,
names : [ link . name ]
2025-01-09 18:07:02 +02:00
} ;
2021-09-22 21:11:36 +02:00
}
2021-05-31 21:31:07 +02:00
}
2021-09-22 22:41:31 +02:00
return Object . values ( linksGroupedBySourceTarget ) ;
2021-05-31 21:31:07 +02:00
}
2025-01-17 21:25:36 +02:00
calculateNodeSizes ( resp : PostNotesMapResponse ) {
2021-09-22 21:11:36 +02:00
this . noteIdToSizeMap = { } ;
2025-01-09 18:07:02 +02:00
if ( this . mapType === "tree" ) {
const { noteIdToDescendantCountMap } = resp ;
2021-09-22 23:02:38 +02:00
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 ) ) ;
}
}
2025-01-09 18:07:02 +02:00
} else if ( this . mapType === "link" ) {
2025-01-17 21:25:36 +02:00
const noteIdToLinkCount : Record < string , number > = { } ;
2021-09-22 23:02:38 +02:00
for ( const link of resp . links ) {
2024-11-30 19:57:06 +01:00
noteIdToLinkCount [ link . targetNoteId ] = 1 + ( noteIdToLinkCount [ link . targetNoteId ] || 0 ) ;
2021-09-22 23:02:38 +02:00
}
2021-09-22 21:11:36 +02:00
2021-09-22 23:02:38 +02:00
for ( const [ noteId ] of resp . notes ) {
this . noteIdToSizeMap [ noteId ] = 4 ;
2021-09-22 21:11:36 +02:00
2021-09-22 23:02:38 +02:00
if ( noteId in noteIdToLinkCount ) {
2024-11-30 19:57:06 +01:00
this . noteIdToSizeMap [ noteId ] += Math . min ( Math . pow ( noteIdToLinkCount [ noteId ] , 0.5 ) , 15 ) ;
2021-09-22 23:02:38 +02:00
}
2021-09-22 21:11:36 +02:00
}
2021-05-31 21:31:07 +02:00
}
2021-09-22 21:11:36 +02:00
}
2025-01-17 21:25:36 +02:00
renderData ( data : Data ) {
2021-09-22 21:11:36 +02:00
this . graph . graphData ( data ) ;
2024-11-30 19:57:06 +01:00
2025-01-09 18:07:02 +02:00
if ( this . widgetMode === "ribbon" && this . note ? . type !== "search" ) {
2021-09-22 22:25:39 +02:00
setTimeout ( ( ) = > {
2022-11-05 22:32:50 +01:00
this . setDimensions ( ) ;
2021-10-21 22:52:52 +02:00
const subGraphNoteIds = this . getSubGraphConnectedToCurrentNote ( data ) ;
2021-09-22 22:25:39 +02:00
2025-01-09 18:07:02 +02:00
this . graph . zoomToFit ( 400 , 50 , ( node ) = > subGraphNoteIds . has ( node . id ) ) ;
2021-11-13 22:48:30 +01:00
if ( subGraphNoteIds . size < 30 ) {
this . graph . d3VelocityDecay ( 0.4 ) ;
}
2021-09-22 22:25:39 +02:00
} , 1000 ) ;
2025-01-09 18:07:02 +02:00
} else {
2021-09-22 22:25:39 +02:00
if ( data . nodes . length > 1 ) {
2021-11-13 22:48:30 +01:00
setTimeout ( ( ) = > {
2022-11-05 22:32:50 +01:00
this . setDimensions ( ) ;
2022-11-07 23:19:38 +01:00
const noteIdsWithLinks = this . getNoteIdsWithLinks ( data ) ;
if ( noteIdsWithLinks . size > 0 ) {
2025-01-17 21:25:36 +02:00
this . graph . zoomToFit ( 400 , 30 , ( node ) = > noteIdsWithLinks . has ( node . id ? ? "" ) ) ;
2022-11-07 23:19:38 +01:00
}
2021-11-13 22:48:30 +01:00
2022-11-07 23:19:38 +01:00
if ( noteIdsWithLinks . size < 30 ) {
2021-11-13 22:48:30 +01:00
this . graph . d3VelocityDecay ( 0.4 ) ;
}
} , 1000 ) ;
2021-09-22 22:25:39 +02:00
}
2021-05-28 23:19:11 +02:00
}
}
2025-01-17 21:25:36 +02:00
getNoteIdsWithLinks ( data : Data ) {
const noteIds = new Set < string | number > ( ) ;
2022-11-07 23:19:38 +01:00
for ( const link of data . links ) {
2025-01-17 21:25:36 +02:00
if ( typeof link . source === "object" && link . source . id ) {
noteIds . add ( link . source . id ) ;
}
if ( typeof link . target === "object" && link . target . id ) {
noteIds . add ( link . target . id ) ;
}
2022-11-07 23:19:38 +01:00
}
return noteIds ;
}
2025-01-17 21:25:36 +02:00
getSubGraphConnectedToCurrentNote ( data : Data ) {
function getGroupedLinks ( links : LinkObject < NodeObject > [ ] , type : "source" | "target" ) {
const map : Record < string | number , LinkObject < NodeObject > [ ] > = { } ;
2021-10-21 22:52:52 +02:00
for ( const link of links ) {
2025-01-17 21:25:36 +02:00
if ( typeof link [ type ] !== "object" ) {
continue ;
}
2021-11-13 22:48:30 +01:00
const key = link [ type ] . id ;
2025-01-17 21:25:36 +02:00
if ( key ) {
map [ key ] = map [ key ] || [ ] ;
map [ key ] . push ( link ) ;
}
2021-10-21 22:52:52 +02:00
}
return map ;
}
2024-11-30 19:57:06 +01:00
const linksBySource = getGroupedLinks ( data . links , "source" ) ;
const linksByTarget = getGroupedLinks ( data . links , "target" ) ;
2021-10-21 22:52:52 +02:00
const subGraphNoteIds = new Set ( ) ;
2025-01-17 21:25:36 +02:00
function traverseGraph ( noteId? : string | number ) {
if ( ! noteId || subGraphNoteIds . has ( noteId ) ) {
2021-10-21 22:52:52 +02:00
return ;
}
subGraphNoteIds . add ( noteId ) ;
for ( const link of linksBySource [ noteId ] || [ ] ) {
2025-01-17 21:25:36 +02:00
if ( typeof link . target === "object" ) {
traverseGraph ( link . target ? . id ) ;
}
2021-10-21 22:52:52 +02:00
}
2021-11-13 22:48:30 +01:00
for ( const link of linksByTarget [ noteId ] || [ ] ) {
2025-01-17 21:25:36 +02:00
if ( typeof link . source === "object" ) {
traverseGraph ( link . source ? . id ) ;
}
2021-11-13 22:48:30 +01:00
}
2021-10-21 22:52:52 +02:00
}
traverseGraph ( this . noteId ) ;
return subGraphNoteIds ;
}
2021-09-22 21:11:36 +02:00
cleanup() {
2025-01-09 18:07:02 +02:00
this . $container . html ( "" ) ;
2021-06-02 21:23:40 +02:00
}
2025-01-17 21:25:36 +02:00
entitiesReloadedEvent ( { loadResults } : EventData < "entitiesReloaded" > ) {
if ( loadResults . getAttributeRows ( this . componentId )
. find ( ( attr ) = > attr . type === "label" && [ "mapType" , "mapRootNoteId" ] . includes ( attr . name || "" ) && attributeService . isAffecting ( attr , this . note ) ) ) {
2021-06-01 22:03:38 +02:00
this . refresh ( ) ;
2021-05-28 23:19:11 +02:00
}
}
}