From b3c247c7dcfe04e0e8e036ab663582360dd67499 Mon Sep 17 00:00:00 2001 From: juju Date: Sat, 30 Nov 2024 19:20:07 +0100 Subject: [PATCH] new features for note_map --- src/public/app/widgets/note_map.js | 264 +++++++++++++++++++++-------- 1 file changed, 193 insertions(+), 71 deletions(-) diff --git a/src/public/app/widgets/note_map.js b/src/public/app/widgets/note_map.js index f4b00dc81..b397c2487 100644 --- a/src/public/app/widgets/note_map.js +++ b/src/public/app/widgets/note_map.js @@ -1,12 +1,11 @@ -import libraryLoader from "../services/library_loader.js"; -import server from "../services/server.js"; -import attributeService from "../services/attributes.js"; -import hoistedNoteService from "../services/hoisted_note.js"; -import appContext from "../components/app_context.js"; -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"; +import libraryLoader from '../services/library_loader.js'; +import server from '../services/server.js'; +import attributeService from '../services/attributes.js'; +import hoistedNoteService from '../services/hoisted_note.js'; +import appContext from '../components/app_context.js'; +import NoteContextAwareWidget from './note_context_aware_widget.js'; +import linkContextMenuService from '../menus/link_context_menu.js'; +import utils from '../services/utils.js'; const esc = utils.escapeHtml; @@ -24,15 +23,62 @@ const TPL = `
z-index: 10; /* should be below dropdown (note actions) */ } + .map-type-switcher button.bx { font-size: 130%; padding: 1px 10px 1px 10px; } + + .fixnodes-type-switcher { + position: absolute; + top: 10px; + left: 45%; + z-index: 10; /* should be below dropdown (note actions) */ + border-radius:0.2rem; + } + + input[type="range"] { + + /* removing default appearance */ + -webkit-appearance: none; + appearance: none; + margin-left: 15px; + width:50% + + } + + + + /* Track: webkit browsers */ + input[type="range"]::-webkit-slider-runnable-track { + height: 6px; + background: #ccc; + border-radius: 16px; + } + + + /* Thumb: webkit */ + input[type="range"]::-webkit-slider-thumb { + /* removing default appearance */ + -webkit-appearance: none; + appearance: none; + /* creating a custom design */ + height: 15px; + width: 15px; + margin-top:-4px; + background-color: #661822; + border-radius: 50%; +
- - + + +
+
+ + +
@@ -43,7 +89,7 @@ const TPL = `
export default class NoteMapWidget extends NoteContextAwareWidget { constructor(widgetMode) { super(); - + this.fixNodes = false; //sets a variable to fix the nodes when dragged this.widgetMode = widgetMode; // 'type' or 'ribbon' } @@ -53,53 +99,112 @@ export default class NoteMapWidget extends NoteContextAwareWidget { const documentStyle = window.getComputedStyle(document.documentElement); this.themeStyle = documentStyle.getPropertyValue('--theme-style')?.trim(); - this.$container = this.$widget.find(".note-map-container"); + this.$container = this.$widget.find('.note-map-container'); this.$styleResolver = this.$widget.find('.style-resolver'); new ResizeObserver(() => this.setDimensions()).observe(this.$container[0]); - this.$widget.find(".map-type-switcher button").on("click", async e => { - const type = $(e.target).closest("button").attr("data-type"); - + this.$widget.find('.map-type-switcher button').on('click', async e => { + const type = $(e.target).closest('button').attr('data-type'); await attributeService.setLabel(this.noteId, 'mapType', type); }); + // Code for the fix node after Dragging. Later in the script is more to fix the nodes in the canvas. This code here is to control the ui element + + this.$widget.find('.fixnodes-type-switcher').on('click', async event => { + this.fixNodes = !this.fixNodes; + console.log(this.fixNodes); + event.target.style.backgroundColor = this.fixNodes ? '#661822' : 'transparent'; + let Distancevalue1 = 40; + this.$widget.find('.fixnodes-type-switcher input').on('change', async e => { + Distancevalue1 = e.target.closest('input').value; + + return e.target.closest('input').value; + }); + }); + super.doRender(); } setDimensions() { - if (!this.graph) { // no graph has been even rendered + if (!this.graph) { + // no graph has been even rendered return; } const $parent = this.$widget.parent(); - this.graph - .height($parent.height()) - .width($parent.width()); + this.graph.height($parent.height()).width($parent.width()); } async refreshWithNote(note) { this.$widget.show(); this.css = { - fontFamily: this.$container.css("font-family"), - textColor: this.rgb2hex(this.$container.css("color")), - mutedTextColor: this.rgb2hex(this.$styleResolver.css("color")) + fontFamily: this.$container.css('font-family'), + textColor: this.rgb2hex(this.$container.css('color')), + mutedTextColor: this.rgb2hex(this.$styleResolver.css('color')) }; - this.mapType = this.note.getLabelValue("mapType") === "tree" ? "tree" : "link"; + this.mapType = this.note.getLabelValue('mapType') === 'tree' ? 'tree' : 'link'; await libraryLoader.requireLibrary(libraryLoader.FORCE_GRAPH); + //Variablen for hoverfeature + + let hoverNode = null; + const highlightLinks = new Set(); + const neighbours = new Set(); + this.graph = ForceGraph()(this.$container[0]) + .width(this.$container.width()) .height(this.$container.height()) .onZoom(zoom => this.setZoomLevel(zoom.k)) .d3AlphaDecay(0.01) .d3VelocityDecay(0.08) - .nodeCanvasObject((node, ctx) => this.paintNode(node, this.getColorForNode(node), ctx)) - .nodePointerAreaPaint((node, ctx) => this.paintNode(node, this.getColorForNode(node), ctx)) + + //Code to fixate nodes when dragged + .onNodeDragEnd(node => { + if (this.fixNodes) { + node.fx = node.x; + node.fy = node.y; + } else { + node.fx = null; + node.fy = null; + } + }) + //saves the hovered node in a variable to paint it then yellow in the if clause of the .nodeCanvasObject function + .onNodeHover(node => { + hoverNode = node || null; + highlightLinks.clear(); + }) + // set link width to show connections on hover. + .linkWidth(link => (highlightLinks.has(link) ? 3 : 0.4)) + .linkColor(link => (highlightLinks.has(link) ? 'white' : this.css.mutedTextColor)) + //Code for painting the node when hovered + .nodeCanvasObject((node, ctx) => { + if (hoverNode == node) { + this.paintNode(node, '#661822', ctx); + neighbours.clear(); + for (const link of data.links) { + 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); + console.log(data); + } + } + } else if (neighbours.has(node) && hoverNode != null) { + this.paintNode(node, '#9d6363', ctx); + } else { + this.paintNode(node, this.getColorForNode(node), ctx); + } + }) + .nodePointerAreaPaint((node, ctx) => + this.paintNode(node, this.getColorForNode(node), ctx) + ) .nodePointerAreaPaint((node, color, ctx) => { ctx.fillStyle = color; ctx.beginPath(); @@ -109,33 +214,44 @@ export default class NoteMapWidget extends NoteContextAwareWidget { .nodeLabel(node => esc(node.name)) .maxZoom(7) .warmupTicks(30) - .linkDirectionalArrowLength(5) - .linkDirectionalArrowRelPos(1) - .linkWidth(1) - .linkColor(() => this.css.mutedTextColor) + .linkDirectionalArrowLength(4) + .linkDirectionalArrowRelPos(0.95) + + //Julien Code Ende + .onNodeClick(node => appContext.tabManager.getActiveContext().setNote(node.id)) .onNodeRightClick((node, e) => linkContextMenuService.openContextMenu(node.id, e)); if (this.mapType === 'link') { this.graph - .linkLabel(l => `${esc(l.source.name)} - ${esc(l.name)} - ${esc(l.target.name)}`) + .linkLabel( + l => + `${esc(l.source.name)} - ${esc(l.name)} - ${esc( + l.target.name + )}` + ) .linkCanvasObject((link, ctx) => this.paintLink(link, ctx)) - .linkCanvasObjectMode(() => "after"); + .linkCanvasObjectMode(() => 'after'); } const mapRootNoteId = this.getMapRootNoteId(); const data = await this.loadNotesAndRelations(mapRootNoteId); - const nodeLinkRatio = data.nodes.length / data.links.length; const magnifiedRatio = Math.pow(nodeLinkRatio, 1.5); const charge = -20 / magnifiedRatio; const boundedCharge = Math.min(-3, charge); + let Distancevalue = 40; // Feature für liveänderungen in note_map wie link distance + + this.$widget.find('.fixnodes-type-switcher input').on('change', async e => { + Distancevalue = e.target.closest('input').value; + this.graph.d3Force('link').distance(Distancevalue); + + this.renderData(data); + }); - this.graph.d3Force('link').distance(40); this.graph.d3Force('center').strength(0.2); this.graph.d3Force('charge').strength(boundedCharge); this.graph.d3Force('charge').distanceMax(1000); - this.renderData(data); } @@ -144,7 +260,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { return this.noteId; } - let mapRootNoteId = this.note.getLabelValue("mapRootNoteId"); + let mapRootNoteId = this.note.getLabelValue('mapRootNoteId'); if (mapRootNoteId === 'hoisted') { mapRootNoteId = hoistedNoteService.getHoistedNoteId(); @@ -166,7 +282,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { } generateColorFromString(str) { - if (this.themeStyle === "dark") { + if (this.themeStyle === 'dark') { str = `0${str}`; // magic lightning modifier } @@ -177,18 +293,19 @@ export default class NoteMapWidget extends NoteContextAwareWidget { let color = '#'; for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xFF; + const value = (hash >> (i * 8)) & 0xff; - color += (`00${value.toString(16)}`).substr(-2); + color += `00${value.toString(16)}`.substr(-2); } return color; } rgb2hex(rgb) { - return `#${rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) + return `#${rgb + .match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) .slice(1) .map(n => parseInt(n, 10).toString(16).padStart(2, '0')) - .join('')}` + .join('')}`; } setZoomLevel(level) { @@ -196,17 +313,18 @@ export default class NoteMapWidget extends NoteContextAwareWidget { } paintNode(node, color, ctx) { - const {x, y} = node; + const { x, y } = node; const size = this.noteIdToSizeMap[node.id]; ctx.fillStyle = color; ctx.beginPath(); - ctx.arc(x, y, size, 0, 2 * Math.PI, false); + ctx.arc(x, y, size * 0.8, 0, 2 * Math.PI, false); ctx.fill(); - const toRender = this.zoomLevel > 2 - || (this.zoomLevel > 1 && size > 6) - || (this.zoomLevel > 0.3 && size > 10); + const toRender = + this.zoomLevel > 2 || + (this.zoomLevel > 1 && size > 6) || + (this.zoomLevel > 0.3 && size > 10); if (!toRender) { return; @@ -231,16 +349,16 @@ export default class NoteMapWidget extends NoteContextAwareWidget { return; } - ctx.font = `3px ${this.css.fontFamily}`; + ctx.font = `2px ${this.css.fontFamily}`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = this.css.mutedTextColor; - const {source, target} = link; + const { source, target } = link; const x = (source.x + target.x) / 2; const y = (source.y + target.y) / 2; - + console.log(x); ctx.save(); ctx.translate(x, y); @@ -266,7 +384,6 @@ export default class NoteMapWidget extends NoteContextAwareWidget { this.calculateNodeSizes(resp); const links = this.getGroupedLinks(resp.links); - this.nodes = resp.notes.map(([noteId, title, type, color]) => ({ id: noteId, name: title, @@ -280,7 +397,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { id: `${link.sourceNoteId}-${link.targetNoteId}`, source: link.sourceNoteId, target: link.targetNoteId, - name: link.names.join(", ") + name: link.names.join(', ') })) }; } @@ -301,7 +418,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { sourceNoteId: link.sourceNoteId, targetNoteId: link.targetNoteId, names: [link.name] - } + }; } } @@ -312,7 +429,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { this.noteIdToSizeMap = {}; if (this.mapType === 'tree') { - const {noteIdToDescendantCountMap} = resp; + const { noteIdToDescendantCountMap } = resp; for (const noteId in noteIdToDescendantCountMap) { this.noteIdToSizeMap[noteId] = 4; @@ -323,19 +440,22 @@ export default class NoteMapWidget extends NoteContextAwareWidget { this.noteIdToSizeMap[noteId] += 1 + Math.round(Math.log(count) / Math.log(1.5)); } } - } - else if (this.mapType === 'link') { + } else if (this.mapType === 'link') { const noteIdToLinkCount = {}; for (const link of resp.links) { - noteIdToLinkCount[link.targetNoteId] = 1 + (noteIdToLinkCount[link.targetNoteId] || 0); + 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); + this.noteIdToSizeMap[noteId] += Math.min( + Math.pow(noteIdToLinkCount[noteId], 0.5), + 15 + ); } } } @@ -343,21 +463,19 @@ export default class NoteMapWidget extends NoteContextAwareWidget { renderData(data) { this.graph.graphData(data); - if (this.widgetMode === 'ribbon' && this.note?.type !== 'search') { setTimeout(() => { this.setDimensions(); const subGraphNoteIds = this.getSubGraphConnectedToCurrentNote(data); - this.graph.zoomToFit(400, 50, node => subGraphNoteIds.has(node.id)); + this.graph.zoomToFit(400, 50, node => subGraphNoteIds.has(node.id)); // zoomed immer doof, ggf ausklammern if (subGraphNoteIds.size < 30) { this.graph.d3VelocityDecay(0.4); } }, 1000); - } - else { + } else { if (data.nodes.length > 1) { setTimeout(() => { this.setDimensions(); @@ -365,7 +483,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { const noteIdsWithLinks = this.getNoteIdsWithLinks(data); if (noteIdsWithLinks.size > 0) { - this.graph.zoomToFit(400, 30, node => noteIdsWithLinks.has(node.id)); + this.graph.zoomToFit(400, 30, node => noteIdsWithLinks.has(node.id)); // zoomed immer doof, ggf ausklammern } if (noteIdsWithLinks.size < 30) { @@ -400,8 +518,8 @@ export default class NoteMapWidget extends NoteContextAwareWidget { return map; } - const linksBySource = getGroupedLinks(data.links, "source"); - const linksByTarget = getGroupedLinks(data.links, "target"); + const linksBySource = getGroupedLinks(data.links, 'source'); + const linksByTarget = getGroupedLinks(data.links, 'target'); const subGraphNoteIds = new Set(); @@ -429,13 +547,17 @@ export default class NoteMapWidget extends NoteContextAwareWidget { this.$container.html(''); } - entitiesReloadedEvent({loadResults}) { - if (loadResults.getAttributeRows(this.componentId).find( - attr => - attr.type === 'label' - && ['mapType', 'mapRootNoteId'].includes(attr.name) - && attributeService.isAffecting(attr, this.note) - )) { + entitiesReloadedEvent({ loadResults }) { + if ( + loadResults + .getAttributeRows(this.componentId) + .find( + attr => + attr.type === 'label' && + ['mapType', 'mapRootNoteId'].includes(attr.name) && + attributeService.isAffecting(attr, this.note) + ) + ) { this.refresh(); } }