diff options
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | components/Graph/drawLabels.ts | 17 | ||||
-rw-r--r-- | components/Sidebar/Backlinks.tsx | 2 | ||||
-rw-r--r-- | components/Sidebar/Link.tsx | 17 | ||||
-rw-r--r-- | components/Sidebar/Note.tsx | 2 | ||||
-rw-r--r-- | components/Sidebar/OrgImage.tsx | 4 | ||||
-rw-r--r-- | components/Sidebar/Section.tsx | 16 | ||||
-rw-r--r-- | components/Sidebar/Title.tsx | 1 | ||||
-rw-r--r-- | components/Sidebar/Toolbar.tsx | 12 | ||||
-rw-r--r-- | org-roam-ui.el | 24 | ||||
-rw-r--r-- | pages/index.tsx | 278 | ||||
-rw-r--r-- | util/findNthNeighbour.ts | 42 | ||||
-rw-r--r-- | util/getLinkColor.ts | 82 | ||||
-rw-r--r-- | util/getLinkNodeColor.ts | 23 | ||||
-rw-r--r-- | util/getNodeColor.ts | 75 | ||||
-rw-r--r-- | util/getNodeColorById.ts | 25 | ||||
-rw-r--r-- | util/getThemeColor.ts | 3 | ||||
-rw-r--r-- | util/hexToRGBA.ts | 11 | ||||
-rw-r--r-- | util/isLinkRelatedToNode.ts | 7 | ||||
-rw-r--r-- | util/nodeSize.ts | 34 | ||||
-rw-r--r-- | util/normalizeLinkEnds.ts | 12 | ||||
-rw-r--r-- | util/numberWithinRange.ts | 3 | ||||
-rw-r--r-- | util/processOrg.tsx | 4 | ||||
-rw-r--r-- | util/uniorg.tsx | 1 |
24 files changed, 467 insertions, 250 deletions
@@ -18,7 +18,7 @@ - [Supporting org-roam-ui](#supporting-org-roam-ui) - [Feedback](#feedback) - [Contribute 💪](#contribute-) - - [Hacktoberfest](#hacktoberfest) + - [Hacktoberfest](#hacktoberfest) - [Donate](#donate) - [Sponsors](#sponsors) @@ -110,13 +110,13 @@ NOTE: This is quite janky at the moment and will change in the future. Consider #### Moving around ```emacs-lisp -(orui-node-zoom) +(org-roam-ui-node-zoom) ``` Zooms to the current node in the global view _ignoring local mode_. ```emacs-lisp -(orui-node-local) +(org-roam-ui-node-local) ``` Opens the current node in local view. @@ -129,6 +129,21 @@ You can optionally give these command three parameters: These options might not work at the moment, please configure them in the UI for the time being. +#### Manipulating graph + +```emacs-lisp +(org-roam-ui-add-to-local-graph &optional id) +``` + +Adds the node with the given id to the local graph. If no id is given, the current node is used. +If the local graph is not open, it will be opened. + +```emacs-lisp +(org-roam-ui-remove-from-local-graph &optional id) +``` + +Removes the node with the given id from the local graph. If no id is given, the current node is used. + ### Configuration Org-Roam-UI exposes a few variables, but most of the customization is done in the web app. @@ -369,7 +384,6 @@ If you are interested in being more closely involved with the project, go [here] We would ❤️ to have you on board! - ## Donate If you really really like org-roam-ui, you can make a [one-time donation or sponsor one of us monthly!](https://github.com/sponsors/ThomasFKJorna/) diff --git a/components/Graph/drawLabels.ts b/components/Graph/drawLabels.ts index fa19270..46790e6 100644 --- a/components/Graph/drawLabels.ts +++ b/components/Graph/drawLabels.ts @@ -1,8 +1,10 @@ import { OrgRoamNode } from '../../api' import { NodeObject } from 'force-graph' import { initialVisuals } from '../config' -import { hexToRGBA, LinksByNodeId } from '../../pages' +import { LinksByNodeId } from '../../pages' import wrap from 'word-wrap' +import { nodeSize } from '../../util/nodeSize' +import { hexToRGBA } from '../../util/hexToRGBA' export interface drawLabelsProps { labelBackgroundColor: string @@ -14,7 +16,6 @@ export interface drawLabelsProps { previouslyHighlightedNodes: { [id: string]: {} } visuals: typeof initialVisuals opacity: number - nodeSize: (node: NodeObject) => number filteredLinksByNodeId: LinksByNodeId nodeRel: number hoverNode: NodeObject | null @@ -44,7 +45,6 @@ export function drawLabels(props: drawLabelsProps) { previouslyHighlightedNodes, visuals, opacity, - nodeSize, filteredLinksByNodeId, nodeRel, hoverNode, @@ -78,7 +78,16 @@ export function drawLabels(props: drawLabelsProps) { const label = nodeTitle.substring(0, visuals.labelLength) const nodeS = Math.cbrt( - (visuals.nodeRel * nodeSize(node)) / Math.pow(globalScale, visuals.nodeZoomSize), + (visuals.nodeRel * + nodeSize({ + node, + highlightedNodes, + linksByNodeId: filteredLinksByNodeId, + opacity, + previouslyHighlightedNodes, + visuals, + })) / + Math.pow(globalScale, visuals.nodeZoomSize), ) const fontSize = visuals.labelFontSize / Math.cbrt(Math.pow(globalScale, visuals.nodeZoomSize)) // ? Math.max((visuals.labelFontSize * nodeS) / 2, (visuals.labelFontSize * nodeS) / 3) diff --git a/components/Sidebar/Backlinks.tsx b/components/Sidebar/Backlinks.tsx index 0bbf415..5a8f6fc 100644 --- a/components/Sidebar/Backlinks.tsx +++ b/components/Sidebar/Backlinks.tsx @@ -2,7 +2,6 @@ import { LinksByNodeId, NodeByCite, NodeById } from '../../pages/index' import { GraphData, NodeObject, LinkObject } from 'force-graph' -import { normalizeLinkEnds } from '../../pages/index' import { VStack, Box, Button, Heading, StackDivider } from '@chakra-ui/react' import React from 'react' import { ProcessedOrg } from '../../util/processOrg' @@ -24,6 +23,7 @@ export interface BacklinksProps { import { PreviewLink } from './Link' import { OrgRoamNode } from '../../api' import { Section } from './Section' +import { normalizeLinkEnds } from '../../util/normalizeLinkEnds' export const Backlinks = (props: BacklinksProps) => { const { diff --git a/components/Sidebar/Link.tsx b/components/Sidebar/Link.tsx index 2f1ad64..73830ce 100644 --- a/components/Sidebar/Link.tsx +++ b/components/Sidebar/Link.tsx @@ -14,17 +14,17 @@ import { Text, useTheme, } from '@chakra-ui/react' -import React, { ReactElement, useContext, useEffect, useMemo, useState } from 'react' +import React, { useContext, useEffect, useMemo, useState } from 'react' import { ProcessedOrg } from '../../util/processOrg' -import unified from 'unified' +// import unified from 'unified' //import createStream from 'unified-stream' -import uniorgParse from 'uniorg-parse' -import uniorg2rehype from 'uniorg-rehype' +// import uniorgParse from 'uniorg-parse' +// import uniorg2rehype from 'uniorg-rehype' //import highlight from 'rehype-highlight' -import katex from 'rehype-katex' +// import katex from 'rehype-katex' import 'katex/dist/katex.css' -import rehype2react from 'rehype-react' +// import rehype2react from 'rehype-react' import { ThemeContext } from '../../util/themecontext' import { LinksByNodeId, NodeByCite, NodeById } from '../../pages' @@ -63,13 +63,10 @@ export interface NormalLinkProps { children: string } -import { hexToRGBA, getThemeColor } from '../../pages/index' import { defaultNoteStyle, viewerNoteStyle, outlineNoteStyle } from './noteStyle' -import { OrgImage } from './OrgImage' import { Scrollbars } from 'react-custom-scrollbars-2' import { ExternalLinkIcon } from '@chakra-ui/icons' -import { Section } from './Section' -import { OrgRoamLink } from '../../api' +import { getThemeColor } from '../../util/getThemeColor' export const NodeLink = (props: NodeLinkProps) => { const { diff --git a/components/Sidebar/Note.tsx b/components/Sidebar/Note.tsx index 638b43a..3d55816 100644 --- a/components/Sidebar/Note.tsx +++ b/components/Sidebar/Note.tsx @@ -1,7 +1,7 @@ import React from 'react' import { NodeObject } from 'force-graph' -import { NodeById, NodeByCite, LinksByNodeId, normalizeLinkEnds } from '../../pages' +import { NodeById, NodeByCite, LinksByNodeId } from '../../pages' import { Box, Flex } from '@chakra-ui/react' import { UniOrg } from '../../util/uniorg' import { Backlinks } from '../../components/Sidebar/Backlinks' diff --git a/components/Sidebar/OrgImage.tsx b/components/Sidebar/OrgImage.tsx index 0922834..f371f49 100644 --- a/components/Sidebar/OrgImage.tsx +++ b/components/Sidebar/OrgImage.tsx @@ -1,5 +1,5 @@ /* eslint-disable @next/next/no-img-element */ -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import Image from 'next/image' import path from 'path' import { Container } from '@chakra-ui/react' @@ -13,7 +13,7 @@ export interface OrgImageProps { export const OrgImage = (props: OrgImageProps) => { const { src, file } = props - const [image, setImage] = useState<any>(null) + // const [image, setImage] = useState<any>(null) /* ) * .then((res) => res.blob()) diff --git a/components/Sidebar/Section.tsx b/components/Sidebar/Section.tsx index adaf00e..7e99f4f 100644 --- a/components/Sidebar/Section.tsx +++ b/components/Sidebar/Section.tsx @@ -1,17 +1,7 @@ -import { Box, Collapse, Flex, IconButton } from '@chakra-ui/react' -import React, { - JSXElementConstructor, - ReactChild, - ReactElement, - ReactNode, - useContext, - useEffect, - useState, -} from 'react' -import { BiCaretDownCircle, BiChevronDownCircle, BiCircle } from 'react-icons/bi' -import { ComponentLike, ComponentPropsWithoutNode } from 'rehype-react' +import { Box, Flex, IconButton } from '@chakra-ui/react' +import React, { ReactChild, useContext, useEffect, useState } from 'react' import { VscCircleFilled, VscCircleOutline } from 'react-icons/vsc' -import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronUpIcon } from '@chakra-ui/icons' +import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons' import { NoteContext } from '../../util/NoteContext' export interface SectionProps { diff --git a/components/Sidebar/Title.tsx b/components/Sidebar/Title.tsx index 2c8d8b7..791cbb8 100644 --- a/components/Sidebar/Title.tsx +++ b/components/Sidebar/Title.tsx @@ -1,6 +1,5 @@ import { Flex, Heading } from '@chakra-ui/react' import React from 'react' -import { BiFile } from 'react-icons/bi' import { OrgRoamNode } from '../../api' export interface TitleProps { diff --git a/components/Sidebar/Toolbar.tsx b/components/Sidebar/Toolbar.tsx index 71f3807..f606837 100644 --- a/components/Sidebar/Toolbar.tsx +++ b/components/Sidebar/Toolbar.tsx @@ -1,17 +1,9 @@ import React from 'react' -import { Text, Flex, IconButton, ButtonGroup, Tooltip } from '@chakra-ui/react' -import { - BiAlignJustify, - BiAlignLeft, - BiAlignMiddle, - BiAlignRight, - BiFont, - BiRightIndent, -} from 'react-icons/bi' +import { Flex, IconButton, ButtonGroup, Tooltip } from '@chakra-ui/react' +import { BiAlignJustify, BiAlignLeft, BiAlignMiddle, BiAlignRight } from 'react-icons/bi' import { MdOutlineExpand, MdOutlineCompress } from 'react-icons/md' import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons' import { IoIosListBox, IoMdListBox } from 'react-icons/io' -import { NodeObject } from 'force-graph' export interface ToolbarProps { setJustification: any diff --git a/org-roam-ui.el b/org-roam-ui.el index ce2eeec..e13349b 100644 --- a/org-roam-ui.el +++ b/org-roam-ui.el @@ -690,6 +690,30 @@ Optionally with ID (string), SPEED (number, ms) and PADDING (number, px)." (padding . ,padding)))))) (message "No node found."))) + +(defun org-roam-ui-change-local-graph (&optional id manipulation) + "Add or remove current node to the local graph. If not in local mode, open local-graph for this node." + (interactive) + (if-let ((node (or id (org-roam-id-at-point)))) + (websocket-send-text org-roam-ui-ws-socket + (json-encode `((type . "command") + (data . ((commandName . "change-local-graph") + (id . ,node) + (manipulation . ,(or manipulation "add"))))))) + (message "No node found."))) + +;;;###autoload +(defun org-roam-ui-add-to-local-graph (&optional id) + "Add current node to the local graph. If not in local mode, open local-graph for this node." + (interactive) + (org-roam-ui-change-local-graph id "add")) + +;;;###autoload +(defun org-roam-ui-remove-from-local-graph (&optional id) + "Remove current node from the local graph. If not in local mode, open local-graph for this node." + (interactive) + (org-roam-ui-change-local-graph id "remove")) + ;;;###autoload (defun org-roam-ui-sync-theme () "Sync your current Emacs theme with org-roam-ui." diff --git a/pages/index.tsx b/pages/index.tsx index fdbabc3..8770283 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -17,7 +17,6 @@ import { GraphData, LinkObject, NodeObject } from 'force-graph' import Head from 'next/head' import React, { ComponentPropsWithoutRef, - forwardRef, useContext, useEffect, useMemo, @@ -30,12 +29,11 @@ import type { ForceGraph2D as TForceGraph2D, ForceGraph3D as TForceGraph3D, } from 'react-force-graph' -import { BiChart, BiNetworkChart } from 'react-icons/bi' +import { BiNetworkChart } from 'react-icons/bi' import { BsReverseLayoutSidebarInsetReverse } from 'react-icons/bs' import ReconnectingWebSocket from 'reconnecting-websocket' import SpriteText from 'three-spritetext' import useUndo from 'use-undo' -import wrap from 'word-wrap' import { OrgRoamGraphReponse, OrgRoamLink, OrgRoamNode } from '../api' import { algos, @@ -57,6 +55,13 @@ import { ThemeContext, ThemeContextProps } from '../util/themecontext' import { openNodeInEmacs } from '../util/webSocketFunctions' import { drawLabels } from '../components/Graph/drawLabels' import { VariablesContext } from '../util/variablesContext' +import { findNthNeighbors } from '../util/findNthNeighbour' +import { getThemeColor } from '../util/getThemeColor' +import { normalizeLinkEnds } from '../util/normalizeLinkEnds' +import { nodeSize } from '../util/nodeSize' +import { getNodeColor } from '../util/getNodeColor' +import { isLinkRelatedToNode } from '../util/isLinkRelatedToNode' +import { getLinkColor } from '../util/getLinkColor' const d3promise = import('d3-force-3d') @@ -79,7 +84,7 @@ export interface EmacsVariables { dailyDir?: string katexMacros?: { [key: string]: string } attachDir?: string - useInheritance?: boolean, + useInheritance?: boolean subDirs: string[] } export type Tags = string[] @@ -461,6 +466,13 @@ export function GraphPage() { setEmacsNodeId(message.data.id) break } + case 'change-local-graph': { + const node = nodeByIdRef.current[message.data.id as string] + if (!node) break + console.log(message) + handleLocal(node, message.data.manipulation) + break + } default: return console.error('unknown message type', message.type) } @@ -518,7 +530,7 @@ export function GraphPage() { const handleLocal = (node: OrgRoamNode, command: string) => { if (command === 'remove') { setScope((currentScope: Scope) => ({ - ...currentScope, + nodeIds: currentScope.nodeIds.filter((id: string) => id !== node.id), excludedNodeIds: [...currentScope.excludedNodeIds, node.id as string], })) return @@ -531,24 +543,23 @@ export function GraphPage() { return } setScope((currentScope: Scope) => ({ - ...currentScope, + excludedNodeIds: currentScope.excludedNodeIds.filter((id: string) => id !== node.id), nodeIds: [...currentScope.nodeIds, node.id as string], })) return } - const [mainItem, setMainItem] = useState({ - type: 'Graph', - title: 'Graph', - icon: <BiNetworkChart />, - }) + // const [mainItem, setMainItem] = useState({ + // type: 'Graph', + // title: 'Graph', + // icon: <BiNetworkChart />, + // }) const [mainWindowWidth, setMainWindowWidth] = usePersistantState<number>( 'mainWindowWidth', windowWidth, ) - console.log(emacsVariables) return ( <VariablesContext.Provider value={{ ...emacsVariables }}> <Box @@ -808,36 +819,6 @@ export const Graph = function (props: GraphProps) { } } - const findNthNeighbors = (ids: string[], excludedIds: string[], n: number) => { - let queue = [ids[0]] - let todo: string[] = [] - const completed = [ids[0]] - Array.from({ length: n }, () => { - queue.forEach((node) => { - const links = filteredLinksByNodeIdRef.current[node as string] ?? [] - links.forEach((link) => { - const [sourceId, targetId] = normalizeLinkEnds(link) - if (excludedIds.some((id) => [sourceId, targetId].includes(id))) { - return - } - if (!completed.includes(sourceId)) { - todo.push(sourceId) - return - } - if (!completed.includes(targetId)) { - todo.push(targetId) - return - } - return - }) - }) - queue = todo - todo.forEach((neighbor) => neighbor && completed.push(neighbor)) - todo = [] - }) - return completed - } - const centralHighlightedNode = useRef<NodeObject | null>(null) useEffect(() => { @@ -983,7 +964,12 @@ export const Graph = function (props: GraphProps) { ? scopedGraphData.nodes.filter((n) => !scope.excludedNodeIds.includes(n.id as string)) : [] const oldScopedNodeIds = oldScopedNodes.map((node) => node.id as string) - const neighbs = findNthNeighbors(scope.nodeIds, scope.excludedNodeIds, local.neighbors) + const neighbs = findNthNeighbors({ + ids: scope.nodeIds, + excludedIds: scope.excludedNodeIds, + n: local.neighbors, + linksByNodeId: filteredLinksByNodeIdRef.current, + }) const newScopedNodes = filteredGraphData.nodes .filter((node) => { if (oldScopedNodes.length) { @@ -993,7 +979,11 @@ export const Graph = function (props: GraphProps) { const links = filteredLinksByNodeIdRef.current[node.id as string] ?? [] return links.some((link) => { const [source, target] = normalizeLinkEnds(link) - return scope.nodeIds.includes(source) || scope.nodeIds.includes(target) + return ( + !scope.excludedNodeIds.includes(source) && + !scope.excludedNodeIds.includes(target) && + (scope.nodeIds.includes(source) || scope.nodeIds.includes(target)) + ) }) } return neighbs.includes(node.id as string) @@ -1037,8 +1027,10 @@ export const Graph = function (props: GraphProps) { }, [ local.neighbors, filter, - JSON.stringify(scope), - JSON.stringify(graphData), + scope, + scope.excludedNodeIds, + scope.nodeIds, + graphData, filteredGraphData.links, filteredGraphData.nodes, ]) @@ -1078,6 +1070,7 @@ export const Graph = function (props: GraphProps) { // shitty handler to check for doubleClicks const lastNodeClickRef = useRef(0) + // this is for animations, it's a bit hacky and can definitely be optimized const [opacity, setOpacity] = useState(1) const [fadeIn, cancel] = useAnimation((x) => setOpacity(x), { duration: visuals.animationSpeed, @@ -1106,10 +1099,7 @@ export const Graph = function (props: GraphProps) { ...links.flatMap((link) => [link.source, link.target]), ].map((nodeId) => [nodeId, {}]), ) - }, [ - JSON.stringify(centralHighlightedNode.current), - JSON.stringify(filteredLinksByNodeIdRef.current), - ]) + }, [centralHighlightedNode.current, filteredLinksByNodeIdRef.current]) useEffect(() => { if (sidebarHighlightedNode?.id) { @@ -1120,6 +1110,7 @@ export const Graph = function (props: GraphProps) { }, [sidebarHighlightedNode]) const lastHoverNode = useRef<OrgRoamNode | null>(null) + useEffect(() => { centralHighlightedNode.current = hoverNode if (hoverNode) { @@ -1162,102 +1153,6 @@ export const Graph = function (props: GraphProps) { ) }, [JSON.stringify(hoverNode), lastHoverNode.current, filteredLinksByNodeIdRef.current]) - const getNodeColorById = (id: string) => { - const linklen = filteredLinksByNodeIdRef.current[id!]?.length ?? 0 - if (coloring.method === 'degree') { - return visuals.nodeColorScheme[ - numberWithinRange(linklen, 0, visuals.nodeColorScheme.length - 1) - ] - } - return visuals.nodeColorScheme[ - linklen && clusterRef.current[id] % visuals.nodeColorScheme.length - ] - } - - const getLinkNodeColor = (sourceId: string, targetId: string) => { - return filteredLinksByNodeIdRef.current[sourceId]!.length > - filteredLinksByNodeIdRef.current[targetId]!.length - ? getNodeColorById(sourceId) - : getNodeColorById(targetId) - } - - const getLinkColor = ( - sourceId: string, - targetId: string, - needsHighlighting: boolean, - theme: any, - ) => { - if (!visuals.linkHighlight && !visuals.linkColorScheme && !needsHighlighting) { - const nodeColor = getLinkNodeColor(sourceId, targetId) - return getThemeColor(nodeColor, theme) - } - - if (!needsHighlighting && !visuals.linkColorScheme) { - const nodeColor = getLinkNodeColor(sourceId, targetId) - return highlightColors[nodeColor][visuals.backgroundColor](visuals.highlightFade * opacity) - } - - if (!needsHighlighting) { - return highlightColors[visuals.linkColorScheme][visuals.backgroundColor]( - visuals.highlightFade * opacity, - ) - } - - if (!visuals.linkHighlight && !visuals.linkColorScheme) { - const nodeColor = getLinkNodeColor(sourceId, targetId) - return getThemeColor(nodeColor, theme) - } - - if (!visuals.linkHighlight) { - return getThemeColor(visuals.linkColorScheme, theme) - } - - if (!visuals.linkColorScheme) { - return highlightColors[getLinkNodeColor(sourceId, targetId)][visuals.linkHighlight](opacity) - } - - return highlightColors[visuals.linkColorScheme][visuals.linkHighlight](opacity) - } - - const getNodeColor = (node: OrgRoamNode, theme: any) => { - const needsHighlighting = highlightedNodes[node.id!] || previouslyHighlightedNodes[node.id!] - //const needsHighlighting = hoverNode?.id === node.id! || lastHoverNode?.current?.id === node.id - // if we are matching the node color and don't have a highlight color - // or we don't have our own scheme and we're not being highlighted - if (visuals.emacsNodeColor && node.id === emacsNodeId) { - return getThemeColor(visuals.emacsNodeColor, theme) - } - if (tagColors && node?.tags.some((tag) => tagColors[tag])) { - const tagColor = tagColors[node?.tags.filter((tag) => tagColors[tag])[0]] - return needsHighlighting - ? highlightColors[tagColor][tagColor](visuals.highlightFade * opacity) - : highlightColors[tagColor][visuals.backgroundColor](visuals.highlightFade * opacity) - } - if (visuals.citeNodeColor && node?.properties?.ROAM_REFS && node?.properties?.FILELESS) { - return needsHighlighting - ? getThemeColor(visuals.citeNodeColor, theme) - : highlightColors[visuals.citeNodeColor][visuals.backgroundColor]( - visuals.highlightFade * opacity, - ) - } - if (visuals.refNodeColor && node.properties.ROAM_REFS) { - return needsHighlighting - ? getThemeColor(visuals.refNodeColor, theme) - : highlightColors[visuals.refNodeColor][visuals.backgroundColor]( - visuals.highlightFade * opacity, - ) - } - if (!needsHighlighting) { - return highlightColors[getNodeColorById(node.id as string)][visuals.backgroundColor]( - visuals.highlightFade * opacity, - ) - } - if (!visuals.nodeHighlight) { - return getThemeColor(getNodeColorById(node.id as string), theme) - } - return highlightColors[getNodeColorById(node.id as string)][visuals.nodeHighlight](opacity) - } - const labelTextColor = useMemo( () => getThemeColor(visuals.labelTextColor, theme), [visuals.labelTextColor, emacsTheme], @@ -1268,21 +1163,6 @@ export const Graph = function (props: GraphProps) { [visuals.labelBackgroundColor, emacsTheme], ) - const nodeSize = (node: NodeObject) => { - const links = filteredLinksByNodeIdRef.current[node.id!] ?? [] - const parentNeighbors = links.length ? links.filter((link) => link.type === 'parent').length : 0 - const basicSize = - 3 + links.length * visuals.nodeSizeLinks - (!filter.parent ? parentNeighbors : 0) - if (visuals.highlightNodeSize === 1) { - return basicSize - } - const highlightSize = - highlightedNodes[node.id!] || previouslyHighlightedNodes[node.id!] - ? 1 + opacity * (visuals.highlightNodeSize - 1) - : 1 - return basicSize * highlightSize - } - const [dragging, setDragging] = useState(false) const scaleRef = useRef(1) @@ -1294,11 +1174,33 @@ export const Graph = function (props: GraphProps) { warmupTicks: scope.nodeIds.length === 1 ? 100 : scope.nodeIds.length > 1 ? 20 : 0, onZoom: ({ k, x, y }) => (scaleRef.current = k), nodeColor: (node) => { - return getNodeColor(node as OrgRoamNode, theme) + return getNodeColor({ + node: node as OrgRoamNode, + theme, + visuals, + cluster: clusterRef.current, + coloring, + emacsNodeId, + highlightColors, + highlightedNodes, + previouslyHighlightedNodes, + linksByNodeId: filteredLinksByNodeIdRef.current, + opacity, + tagColors, + }) }, nodeRelSize: visuals.nodeRel, nodeVal: (node) => { - return nodeSize(node) / Math.pow(scaleRef.current, visuals.nodeZoomSize) + return ( + nodeSize({ + node, + highlightedNodes, + linksByNodeId: filteredLinksByNodeIdRef.current, + opacity, + previouslyHighlightedNodes, + visuals, + }) / Math.pow(scaleRef.current, visuals.nodeZoomSize) + ) }, nodeCanvasObject: (node, ctx, globalScale) => { drawLabels({ @@ -1313,7 +1215,6 @@ export const Graph = function (props: GraphProps) { previouslyHighlightedNodes, visuals, opacity, - nodeSize, labelTextColor, labelBackgroundColor, hoverNode, @@ -1355,7 +1256,18 @@ export const Graph = function (props: GraphProps) { ) } - return getLinkColor(sourceId as string, targetId as string, needsHighlighting, theme) + return getLinkColor({ + sourceId: sourceId as string, + targetId: targetId as string, + needsHighlighting, + theme, + cluster: clusterRef.current, + coloring, + highlightColors, + linksByNodeId: filteredLinksByNodeIdRef.current, + opacity, + visuals, + }) }, linkWidth: (link) => { if (visuals.highlightLinkSize === 1) { @@ -1480,39 +1392,3 @@ export const Graph = function (props: GraphProps) { </Box> ) } - -function isLinkRelatedToNode(link: LinkObject, node: NodeObject | null) { - return ( - (link.source as NodeObject)?.id! === node?.id! || (link.target as NodeObject)?.id! === node?.id! - ) -} - -function numberWithinRange(num: number, min: number, max: number) { - return Math.min(Math.max(num, min), max) -} - -export function normalizeLinkEnds(link: OrgRoamLink | LinkObject): [string, string] { - // we need to cover both because force-graph modifies the original data - // but if we supply the original data on each render, the graph will re-render sporadically - const sourceId = - typeof link.source === 'object' ? (link.source.id! as string) : (link.source as string) - const targetId = - typeof link.target === 'object' ? (link.target.id! as string) : (link.target as string) - return [sourceId, targetId] -} - -export function getThemeColor(name: string, theme: any) { - return name.split('.').reduce((o, i) => o[i], theme.colors) -} - -export function hexToRGBA(hex: string, opacity: number) { - return ( - 'rgba(' + - (hex = hex.replace('#', '')) - .match(new RegExp('(.{' + hex.length / 3 + '})', 'g'))! - .map((l) => parseInt(hex.length % 2 ? l + l : l, 16)) - .concat(isFinite(opacity) ? opacity : 1) - .join(',') + - ')' - ) -} diff --git a/util/findNthNeighbour.ts b/util/findNthNeighbour.ts new file mode 100644 index 0000000..d48adbc --- /dev/null +++ b/util/findNthNeighbour.ts @@ -0,0 +1,42 @@ +import { LinksByNodeId } from '../pages' +import { normalizeLinkEnds } from './normalizeLinkEnds' + +export const findNthNeighbors = ({ + ids, + excludedIds, + n, + linksByNodeId, +}: { + ids: string[] + excludedIds: string[] + n: number + linksByNodeId: LinksByNodeId +}) => { + let queue = [ids[0]] + let todo: string[] = [] + const completed = [ids[0]] + Array.from({ length: n }, () => { + queue.forEach((node) => { + const links = linksByNodeId[node as string] ?? [] + links.forEach((link) => { + const [sourceId, targetId] = normalizeLinkEnds(link) + if (excludedIds.some((id) => [sourceId, targetId].includes(id))) { + return + } + if (!completed.includes(sourceId)) { + todo.push(sourceId) + return + } + if (!completed.includes(targetId)) { + todo.push(targetId) + return + } + return + }) + }) + queue = todo + todo.forEach((neighbor) => neighbor && completed.push(neighbor)) + todo = [] + }) + return completed +} diff --git a/util/getLinkColor.ts b/util/getLinkColor.ts new file mode 100644 index 0000000..33f7094 --- /dev/null +++ b/util/getLinkColor.ts @@ -0,0 +1,82 @@ +import { initialColoring, initialVisuals } from '../components/config' +import { LinksByNodeId } from '../pages' +import { getLinkNodeColor } from './getLinkNodeColor' +import { getThemeColor } from './getThemeColor' + +export const getLinkColor = ({ + sourceId, + targetId, + needsHighlighting, + theme, + visuals, + highlightColors, + opacity, + linksByNodeId, + coloring, + cluster, +}: { + sourceId: string + targetId: string + needsHighlighting: boolean + theme: any + visuals: typeof initialVisuals + highlightColors: Record<string, any> + opacity: number + linksByNodeId: LinksByNodeId + coloring: typeof initialColoring + cluster: any +}) => { + if (!visuals.linkHighlight && !visuals.linkColorScheme && !needsHighlighting) { + const nodeColor = getLinkNodeColor({ + sourceId, + targetId, + linksByNodeId, + visuals, + coloring, + cluster, + }) + return getThemeColor(nodeColor, theme) + } + + if (!needsHighlighting && !visuals.linkColorScheme) { + const nodeColor = getLinkNodeColor({ + sourceId, + targetId, + linksByNodeId, + visuals, + coloring, + cluster, + }) + return highlightColors[nodeColor][visuals.backgroundColor](visuals.highlightFade * opacity) + } + + if (!needsHighlighting) { + return highlightColors[visuals.linkColorScheme][visuals.backgroundColor]( + visuals.highlightFade * opacity, + ) + } + + if (!visuals.linkHighlight && !visuals.linkColorScheme) { + const nodeColor = getLinkNodeColor({ + sourceId, + targetId, + linksByNodeId, + visuals, + coloring, + cluster, + }) + return getThemeColor(nodeColor, theme) + } + + if (!visuals.linkHighlight) { + return getThemeColor(visuals.linkColorScheme, theme) + } + + if (!visuals.linkColorScheme) { + return highlightColors[ + getLinkNodeColor({ sourceId, targetId, linksByNodeId, visuals, coloring, cluster }) + ][visuals.linkHighlight](opacity) + } + + return highlightColors[visuals.linkColorScheme][visuals.linkHighlight](opacity) +} diff --git a/util/getLinkNodeColor.ts b/util/getLinkNodeColor.ts new file mode 100644 index 0000000..86cfa4b --- /dev/null +++ b/util/getLinkNodeColor.ts @@ -0,0 +1,23 @@ +import { initialColoring, initialVisuals } from '../components/config' +import { LinksByNodeId } from '../pages' +import { getNodeColorById } from './getNodeColorById' + +export const getLinkNodeColor = ({ + sourceId, + targetId, + linksByNodeId, + visuals, + coloring, + cluster, +}: { + sourceId: string + targetId: string + linksByNodeId: LinksByNodeId + visuals: typeof initialVisuals + coloring: typeof initialColoring + cluster: any +}) => { + return linksByNodeId[sourceId]!.length > linksByNodeId[targetId]!.length + ? getNodeColorById({ id: sourceId, linksByNodeId, visuals, cluster, coloring }) + : getNodeColorById({ id: targetId, visuals, linksByNodeId, cluster, coloring }) +} diff --git a/util/getNodeColor.ts b/util/getNodeColor.ts new file mode 100644 index 0000000..f25a15b --- /dev/null +++ b/util/getNodeColor.ts @@ -0,0 +1,75 @@ +import { OrgRoamNode } from '../api' +import { initialColoring, initialVisuals } from '../components/config' +import { LinksByNodeId } from '../pages' +import { getNodeColorById } from './getNodeColorById' +import { getThemeColor } from './getThemeColor' + +export const getNodeColor = ({ + node, + theme, + highlightedNodes, + previouslyHighlightedNodes, + visuals, + tagColors, + highlightColors, + opacity, + emacsNodeId, + linksByNodeId, + cluster, + coloring, +}: { + node: OrgRoamNode + theme: any + visuals: typeof initialVisuals + highlightedNodes: Record<string, any> + previouslyHighlightedNodes: Record<string, any> + tagColors: Record<string, any> + highlightColors: Record<string, any> + opacity: number + emacsNodeId: string | null + linksByNodeId: LinksByNodeId + cluster: any + coloring: typeof initialColoring +}) => { + const needsHighlighting = highlightedNodes[node.id!] || previouslyHighlightedNodes[node.id!] + //const needsHighlighting = hoverNode?.id === node.id! || lastHoverNode?.current?.id === node.id + // if we are matching the node color and don't have a highlight color + // or we don't have our own scheme and we're not being highlighted + if (visuals.emacsNodeColor && node.id === emacsNodeId) { + return getThemeColor(visuals.emacsNodeColor, theme) + } + if (tagColors && node?.tags.some((tag) => tagColors[tag])) { + const tagColor = tagColors[node?.tags.filter((tag) => tagColors[tag])[0]] + return needsHighlighting + ? highlightColors[tagColor][tagColor](visuals.highlightFade * opacity) + : highlightColors[tagColor][visuals.backgroundColor](visuals.highlightFade * opacity) + } + if (visuals.citeNodeColor && node?.properties?.ROAM_REFS && node?.properties?.FILELESS) { + return needsHighlighting + ? getThemeColor(visuals.citeNodeColor, theme) + : highlightColors[visuals.citeNodeColor][visuals.backgroundColor]( + visuals.highlightFade * opacity, + ) + } + if (visuals.refNodeColor && node.properties.ROAM_REFS) { + return needsHighlighting + ? getThemeColor(visuals.refNodeColor, theme) + : highlightColors[visuals.refNodeColor][visuals.backgroundColor]( + visuals.highlightFade * opacity, + ) + } + if (!needsHighlighting) { + return highlightColors[ + getNodeColorById({ id: node.id as string, cluster, coloring, linksByNodeId, visuals }) + ][visuals.backgroundColor](visuals.highlightFade * opacity) + } + if (!visuals.nodeHighlight) { + return getThemeColor( + getNodeColorById({ id: node.id as string, cluster, coloring, linksByNodeId, visuals }), + theme, + ) + } + return highlightColors[ + getNodeColorById({ id: node.id as string, cluster, coloring, linksByNodeId, visuals }) + ][visuals.nodeHighlight](opacity) +} diff --git a/util/getNodeColorById.ts b/util/getNodeColorById.ts new file mode 100644 index 0000000..d2c198b --- /dev/null +++ b/util/getNodeColorById.ts @@ -0,0 +1,25 @@ +import { initialColoring, initialVisuals } from '../components/config' +import { LinksByNodeId } from '../pages' +import { numberWithinRange } from './numberWithinRange' + +export const getNodeColorById = ({ + id, + linksByNodeId, + visuals, + coloring, + cluster, +}: { + id: string + linksByNodeId: LinksByNodeId + visuals: typeof initialVisuals + cluster: any + coloring: typeof initialColoring +}) => { + const linklen = linksByNodeId[id!]?.length ?? 0 + if (coloring.method === 'degree') { + return visuals.nodeColorScheme[ + numberWithinRange(linklen, 0, visuals.nodeColorScheme.length - 1) + ] + } + return visuals.nodeColorScheme[linklen && cluster[id] % visuals.nodeColorScheme.length] +} diff --git a/util/getThemeColor.ts b/util/getThemeColor.ts new file mode 100644 index 0000000..1175a52 --- /dev/null +++ b/util/getThemeColor.ts @@ -0,0 +1,3 @@ +export const getThemeColor = (name: string, theme: any) => { + return name.split('.').reduce((o, i) => o[i], theme.colors) +} diff --git a/util/hexToRGBA.ts b/util/hexToRGBA.ts new file mode 100644 index 0000000..bacb601 --- /dev/null +++ b/util/hexToRGBA.ts @@ -0,0 +1,11 @@ +export function hexToRGBA(hex: string, opacity: number) { + return ( + 'rgba(' + + (hex = hex.replace('#', '')) + .match(new RegExp('(.{' + hex.length / 3 + '})', 'g'))! + .map((l) => parseInt(hex.length % 2 ? l + l : l, 16)) + .concat(isFinite(opacity) ? opacity : 1) + .join(',') + + ')' + ) +} diff --git a/util/isLinkRelatedToNode.ts b/util/isLinkRelatedToNode.ts new file mode 100644 index 0000000..eeacab7 --- /dev/null +++ b/util/isLinkRelatedToNode.ts @@ -0,0 +1,7 @@ +import { NodeObject, LinkObject } from 'force-graph' + +export const isLinkRelatedToNode = (link: LinkObject, node: NodeObject | null) => { + return ( + (link.source as NodeObject)?.id! === node?.id! || (link.target as NodeObject)?.id! === node?.id! + ) +} diff --git a/util/nodeSize.ts b/util/nodeSize.ts new file mode 100644 index 0000000..3601c1e --- /dev/null +++ b/util/nodeSize.ts @@ -0,0 +1,34 @@ +import { filter } from '@chakra-ui/react' +import { initialVisuals } from '../components/config' +import { LinksByNodeId } from '../pages' +import { NodeObject } from 'force-graph' + +export const nodeSize = ({ + linksByNodeId, + visuals, + highlightedNodes, + previouslyHighlightedNodes, + opacity, + node, +}: { + node: NodeObject + visuals: typeof initialVisuals + + highlightedNodes: Record<string, any> + previouslyHighlightedNodes: Record<string, any> + opacity: number + linksByNodeId: LinksByNodeId +}) => { + const links = linksByNodeId[node.id!] ?? [] + const parentNeighbors = links.length ? links.filter((link) => link.type === 'parent').length : 0 + const basicSize = + 3 + links.length * visuals.nodeSizeLinks - (!filter.parent ? parentNeighbors : 0) + if (visuals.highlightNodeSize === 1) { + return basicSize + } + const highlightSize = + highlightedNodes[node.id!] || previouslyHighlightedNodes[node.id!] + ? 1 + opacity * (visuals.highlightNodeSize - 1) + : 1 + return basicSize * highlightSize +} diff --git a/util/normalizeLinkEnds.ts b/util/normalizeLinkEnds.ts new file mode 100644 index 0000000..43eee9c --- /dev/null +++ b/util/normalizeLinkEnds.ts @@ -0,0 +1,12 @@ +import { OrgRoamLink } from '../api' +import { LinkObject } from 'force-graph' + +export function normalizeLinkEnds(link: OrgRoamLink | LinkObject): [string, string] { + // we need to cover both because force-graph modifies the original data + // but if we supply the original data on each render, the graph will re-render sporadically + const sourceId = + typeof link.source === 'object' ? (link.source.id! as string) : (link.source as string) + const targetId = + typeof link.target === 'object' ? (link.target.id! as string) : (link.target as string) + return [sourceId, targetId] +} diff --git a/util/numberWithinRange.ts b/util/numberWithinRange.ts new file mode 100644 index 0000000..ae435ce --- /dev/null +++ b/util/numberWithinRange.ts @@ -0,0 +1,3 @@ +export const numberWithinRange = (num: number, min: number, max: number) => { + return Math.min(Math.max(num, min), max) +} diff --git a/util/processOrg.tsx b/util/processOrg.tsx index ea868ee..2abc256 100644 --- a/util/processOrg.tsx +++ b/util/processOrg.tsx @@ -26,7 +26,7 @@ import remarkSectionize from 'remark-sectionize' import remarkRehype from 'remark-rehype' import { PreviewLink } from '../components/Sidebar/Link' -import { LinksByNodeId, NodeByCite, NodeById, normalizeLinkEnds } from '../pages' +import { LinksByNodeId, NodeByCite, NodeById } from '../pages' import React, { createContext, ReactNode, useMemo } from 'react' import { OrgImage } from '../components/Sidebar/OrgImage' import { Section } from '../components/Sidebar/Section' @@ -36,6 +36,7 @@ import { OrgRoamLink, OrgRoamNode } from '../api' // @ts-expect-error non-ESM unified means no types import { toString } from 'hast-util-to-string' import { Box, chakra } from '@chakra-ui/react' +import { normalizeLinkEnds } from './normalizeLinkEnds' export interface ProcessedOrgProps { nodeById: NodeById @@ -126,7 +127,6 @@ export const ProcessedOrg = (props: ProcessedOrgProps) => { const isMarkdown = previewNode?.file?.slice(-3) === '.md' const baseProcessor = isMarkdown ? mdProcessor : orgProcessor - console.log(macros) const processor = useMemo( () => baseProcessor diff --git a/util/uniorg.tsx b/util/uniorg.tsx index 4172d7a..8e285e6 100644 --- a/util/uniorg.tsx +++ b/util/uniorg.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useMemo, useState } from 'react' -import { OrgRoamLink, OrgRoamNode } from '../api' import { LinksByNodeId, NodeByCite, NodeById } from '../pages/index' import { ProcessedOrg } from './processOrg' |