diff options
Diffstat (limited to 'pages')
-rw-r--r-- | pages/index.tsx | 358 |
1 files changed, 199 insertions, 159 deletions
diff --git a/pages/index.tsx b/pages/index.tsx index 70bb568..3237785 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,13 +1,16 @@ -import React, { useEffect, useRef, useState } from 'react' -import dynamic from 'next/dynamic' +import React, { ComponentPropsWithoutRef, useEffect, useRef, useState } from 'react' import { usePersistantState } from '../util/persistant-state' const d3promise = import('d3-force-3d') -import type { ForceGraph2D as TForceGraph2D } from 'react-force-graph' +import type { + ForceGraph2D as TForceGraph2D, + ForceGraph3D as TForceGraph3D, +} from 'react-force-graph' import { OrgRoamGraphReponse, OrgRoamLink, OrgRoamNode } from '../api' import { GraphData, NodeObject } from 'force-graph' import { useWindowSize } from '@react-hook/window-size' +import useConstant from 'use-constant' import { Accordion, @@ -51,13 +54,21 @@ const ForceGraph2D = ( !!global.window ? require('react-force-graph').ForceGraph2D : null ) as typeof TForceGraph2D +const ForceGraph3D = ( + !!global.window ? require('react-force-graph').ForceGraph3D : null +) as typeof TForceGraph3D + export type NodeById = { [nodeId: string]: OrgRoamNode | undefined } export type LinksByNodeId = { [nodeId: string]: OrgRoamLink[] | undefined } +export type Scope = { + nodeIds: string[] +} const initialPhysics = { enabled: true, charge: -350, collision: true, + collisionStrength: 0, linkStrength: 0.1, linkIts: 1, particles: 0, @@ -79,6 +90,7 @@ const initialPhysics = { hover: 'highlight', click: 'select', doubleClick: 'local', + iterations: 0, } const initialTheme = { @@ -159,7 +171,6 @@ export function GraphPage() { }, []) const [threeDim, setThreeDim] = useState(false) - const [local, setLocal] = useState(false) if (!graphData) { return null @@ -172,7 +183,6 @@ export function GraphPage() { physics, setPhysics, threeDim, - local, }} /> <Graph @@ -182,7 +192,6 @@ export function GraphPage() { physics, graphData, threeDim, - local, }} /> </div> @@ -283,10 +292,9 @@ export interface TweakProps { physics: typeof initialPhysics setPhysics: any threeDim: boolean - local: boolean } export const Tweaks = function (props: TweakProps) { - const { physics, setPhysics, threeDim, local } = props + const { physics, setPhysics, threeDim} = props return ( <Container zIndex="overlay" position="absolute" bg="white" w="xs"> @@ -478,13 +486,13 @@ export interface GraphProps { graphData: GraphData physics: typeof initialPhysics threeDim: boolean - local: boolean } export const Graph = function (props: GraphProps) { - const { physics, graphData, threeDim, local, linksByNodeId, nodeById } = props + const { physics, graphData, threeDim, linksByNodeId } = props - const forceGraphRef = useRef<any>(null) + const graph2dRef = useRef<any>(null) + const graph3dRef = useRef<any>(null) // react-force-graph does not track window size // https://github.com/vasturiano/react-force-graph/issues/233 @@ -492,9 +500,9 @@ export const Graph = function (props: GraphProps) { const [windowWidth, windowHeight] = useWindowSize() const [hoverNode, setHoverNode] = useState<NodeObject | null>(null) - const [selectedNode, setSelectedNode] = useState<NodeObject | null>() + const [scope, setScope] = useState<Scope>({ nodeIds: [] }) - const centralHighlightedNode = selectedNode ?? hoverNode + const centralHighlightedNode = hoverNode const highlightedNodes = (() => { if (!centralHighlightedNode) { return {} @@ -513,11 +521,39 @@ export const Graph = function (props: GraphProps) { ) })() + const scopedNodes = graphData.nodes.filter((node) => { + const links = linksByNodeId[node.id as string] ?? [] + return ( + scope.nodeIds.includes(node.id as string) || + links.some((link) => { + return scope.nodeIds.includes(link.source) || scope.nodeIds.includes(link.target) + }) + ) + }) + + const scopedNodeIds = scopedNodes.map((node) => node.id as string) + + const scopedLinks = graphData.links.filter((link) => { + // 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! : (link.source as string) + const targetId = typeof link.target === 'object' ? link.target.id! : (link.target as string) + + return scopedNodeIds.includes(sourceId as string) && scopedNodeIds.includes(targetId as string) + }) + + const scopedGraphData = + scope.nodeIds.length === 0 + ? graphData + : { + nodes: scopedNodes, + links: scopedLinks, + } + useEffect(() => { ;(async () => { - const fg = forceGraphRef.current + const fg = threeDim ? graph3dRef.current : graph2dRef.current const d3 = await d3promise - //fg.d3Force('center').strength(0.05); if (physics.gravityOn) { fg.d3Force('x', d3.forceX().strength(physics.gravity)) fg.d3Force('y', d3.forceY().strength(physics.gravity)) @@ -549,162 +585,166 @@ export const Graph = function (props: GraphProps) { // Normally the graph doesn't update when you just change the physics parameters // This forces the graph to make a small update when you do useEffect(() => { - forceGraphRef.current?.d3ReheatSimulation() + graph2dRef.current?.d3ReheatSimulation() }, [physics]) //shitty handler to check for doubleClicks - const [doubleClick, setDoubleClick] = useState(0) - const [localGraphData, setLocalGraphData] = useState<any>({ - nodes: [], - links: [], - }) + const lastNodeClickRef = useRef(0) - const selectClick = (node: NodeObject, event: any) => { - window.open('org-protocol://roam-node?node=' + node.id, '_self') + const onNodeClick = (node: NodeObject, event: any) => { + const isDoubleClick = event.timeStamp - lastNodeClickRef.current < 400 + lastNodeClickRef.current = event.timeStamp - if (event.timeStamp - doubleClick < 400) { - // getLocalGraphData(node) - } - // setDoubleClick(event.timeStamp) - if (node) { - return setSelectedNode(node) + if (isDoubleClick) { + window.open('org-protocol://roam-node?node=' + node.id, '_self') + return } - } - return ( - <div style={{ position: 'absolute' }}> - <ForceGraph2D - ref={forceGraphRef} - graphData={local ? localGraphData : graphData} - width={windowWidth} - height={windowHeight} - nodeColor={(node) => { - if (!physics.colorful) { - if (Object.keys(highlightedNodes).length === 0) { - return 'rgb(100, 100, 100, 1)' - } - return highlightedNodes[node.id!] ? '#a991f1' : 'rgb(50, 50, 50, 0.5)' - } - - const palette = [ - '#ff665c', - '#e69055', - '#7bc275', - '#4db5bd', - '#FCCE7B', - '#51afef', - '#1f5582', - '#C57BDB', - '#a991f1', - '#5cEfFF', - '#6A8FBF', - ] - - // random - return palette[0] - if (node.neighbors.length === 1 || node.neighbors.length === 2) { - return palette[node.neighbors[0].index % 11] - } + setScope((currentScope) => ({ + ...currentScope, + nodeIds: [...currentScope.nodeIds, node.id as string], + })) + return + } - return palette[node.index % 11] - }} - linkColor={(link) => { - if (Object.keys(highlightedNodes).length === 0) { - return 'rgb(50, 50, 50, 0.8)' - } + const graphCommonProps: ComponentPropsWithoutRef<typeof TForceGraph2D> = { + graphData: scopedGraphData, + width: windowWidth, + height: windowHeight, + backgroundColor: '#242730', + nodeLabel: (node) => (node as OrgRoamNode).title, + nodeColor: (node) => { + if (!physics.colorful) { + if (Object.keys(highlightedNodes).length === 0) { + return 'rgb(100, 100, 100)' + } + return highlightedNodes[node.id!] ? '#a991f1' : 'rgb(50, 50, 50)' + } - const linkIsHighlighted = - (link.source as NodeObject).id! === centralHighlightedNode?.id! || - (link.target as NodeObject).id! === centralHighlightedNode?.id! + const palette = [ + '#ff665c', + '#e69055', + '#7bc275', + '#4db5bd', + '#FCCE7B', + '#51afef', + '#1f5582', + '#C57BDB', + '#a991f1', + '#5cEfFF', + '#6A8FBF', + ] + + return palette[ + numbereWithinRange(linksByNodeId[node.id!]?.length ?? 0, 0, palette.length - 1) + ] + }, + nodeRelSize: physics.nodeRel, + nodeVal: (node) => { + const links = linksByNodeId[node.id!] ?? [] + const basicSize = 3 + links.length + const highlightSize = highlightedNodes[node.id!] ? 2 : 0 + return basicSize + highlightSize + }, + nodeCanvasObject: (node, ctx, globalScale) => { + if (!physics.labels) { + return + } - return linkIsHighlighted ? '#a991f1' : 'rgb(50, 50, 50, 0.2)' - }} - linkDirectionalParticles={physics.particles} - linkDirectionalParticleWidth={physics.particleWidth} - nodeLabel={(node) => (node as OrgRoamNode).title} - linkWidth={(link) => { - const linkIsHighlighted = - (link.source as NodeObject).id! === centralHighlightedNode?.id! || - (link.target as NodeObject).id! === centralHighlightedNode?.id! - - return linkIsHighlighted ? 3 * physics.linkWidth : physics.linkWidth - }} - nodeRelSize={physics.nodeRel} - nodeVal={(node) => { - const links = props.linksByNodeId[node.id!] ?? [] - const basicSize = 3 + links.length - const highlightSize = highlightedNodes[node.id!] ? 2 : 0 - return basicSize + highlightSize - }} - nodeCanvasObject={(node, ctx, globalScale) => { - if (!physics.labels) { - return - } + if (globalScale <= physics.labelScale && !highlightedNodes[node.id!]) { + return + } - if (globalScale <= physics.labelScale && !highlightedNodes[node.id!]) { - return - } + const nodeTitle = (node as OrgRoamNode).title + const label = nodeTitle.substring(0, Math.min(nodeTitle.length, 30)) + // const label = 'label' + const fontSize = 12 / globalScale + const textWidth = ctx.measureText(label).width + const bckgDimensions = [textWidth * 1.1, fontSize].map((n) => n + fontSize * 0.5) as [ + number, + number, + ] // some padding + + const fadeFactor = Math.min((3 * (globalScale - physics.labelScale)) / physics.labelScale, 1) + + // draw label background + ctx.fillStyle = + 'rgba(20, 20, 20, ' + + (highlightedNodes.length === 0 + ? 0.5 * fadeFactor + : highlightedNodes[node.id!] + ? 0.5 + : 0.15 * fadeFactor) + + ')' + ctx.fillRect( + node.x! - bckgDimensions[0] / 2, + node.y! - bckgDimensions[1] / 2, + ...bckgDimensions, + ) + + // draw label text + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillStyle = + 'rgb(255, 255, 255, ' + + (highlightedNodes.length === 0 + ? fadeFactor + : highlightedNodes[node.id!] + ? 1 + : 0.3 * fadeFactor) + + ')' + ctx.font = `${fontSize}px Sans-Serif` + ctx.fillText(label, node.x!, node.y!) + }, + nodeCanvasObjectMode: () => 'after', + + linkDirectionalParticles: physics.particles, + linkColor: (link) => { + const linkIsHighlighted = + (link.source as NodeObject).id! === centralHighlightedNode?.id! || + (link.target as NodeObject).id! === centralHighlightedNode?.id! + + return linkIsHighlighted ? '#a991f1' : '#666666' + }, + linkWidth: (link) => { + const linkIsHighlighted = + (link.source as NodeObject).id! === centralHighlightedNode?.id! || + (link.target as NodeObject).id! === centralHighlightedNode?.id! + + return linkIsHighlighted ? 2 * physics.linkWidth : physics.linkWidth + }, + linkDirectionalParticleWidth: physics.particleWidth, + + d3AlphaDecay: physics.alphaDecay, + d3AlphaMin: physics.alphaMin, + d3VelocityDecay: physics.velocityDecay, + + onNodeClick: onNodeClick, + onBackgroundClick: () => { + setScope((currentScope) => ({ + ...currentScope, + nodeIds: [], + })) + }, + onNodeHover: (node) => { + if (!physics.hover) { + return + } + setHoverNode(node) + }, + } - const nodeTitle = (node as OrgRoamNode).title - const label = nodeTitle.substring(0, Math.min(nodeTitle.length, 30)) - // const label = 'label' - const fontSize = 12 / globalScale - const textWidth = ctx.measureText(label).width - const bckgDimensions = [textWidth * 1.1, fontSize].map((n) => n + fontSize * 0.5) as [ - number, - number, - ] // some padding - - const fadeFactor = Math.min( - (3 * (globalScale - physics.labelScale)) / physics.labelScale, - 1, - ) - - // draw label background - ctx.fillStyle = - 'rgba(20, 20, 20, ' + - (highlightedNodes.length === 0 - ? 0.5 * fadeFactor - : highlightedNodes[node.id!] - ? 0.5 - : 0.15 * fadeFactor) + - ')' - ctx.fillRect( - node.x! - bckgDimensions[0] / 2, - node.y! - bckgDimensions[1] / 2, - ...bckgDimensions, - ) - - // draw label text - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - ctx.fillStyle = - 'rgb(255, 255, 255, ' + - (highlightedNodes.length === 0 - ? fadeFactor - : highlightedNodes[node.id!] - ? 1 - : 0.3 * fadeFactor) + - ')' - ctx.font = `${fontSize}px Sans-Serif` - ctx.fillText(label, node.x!, node.y!) - }} - nodeCanvasObjectMode={() => 'after'} - d3AlphaDecay={physics.alphaDecay} - d3AlphaMin={physics.alphaMin} - d3VelocityDecay={physics.velocityDecay} - backgroundColor={'#242730'} - onNodeClick={selectClick} - onBackgroundClick={() => { - setSelectedNode(null) - }} - onNodeHover={(node) => { - if (!physics.hover) { - return - } - setHoverNode(node) - }} - /> + return ( + <div> + {threeDim ? ( + <ForceGraph3D ref={graph3dRef} {...graphCommonProps} nodeThreeObjectExtend={true} /> + ) : ( + <ForceGraph2D ref={graph2dRef} {...graphCommonProps} /> + )} </div> ) } + +function numbereWithinRange(num: number, min: number, max: number) { + return Math.min(Math.max(num, min), max) +} |