@nesso-how/graph 0.1.0-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/GlyphSVG.d.ts +9 -0
- package/dist/GlyphSVG.js +7 -0
- package/dist/NessoGraph.d.ts +41 -0
- package/dist/NessoGraph.js +34 -0
- package/dist/ReadOnlyConceptNode.d.ts +5 -0
- package/dist/ReadOnlyConceptNode.js +65 -0
- package/dist/ReadOnlyNessoEdge.d.ts +5 -0
- package/dist/ReadOnlyNessoEdge.js +100 -0
- package/dist/context.d.ts +11 -0
- package/dist/context.js +14 -0
- package/dist/geometry.d.ts +48 -0
- package/dist/geometry.js +65 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/ratingColor.d.ts +1 -0
- package/dist/ratingColor.js +12 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Omar Desogus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @nesso-how/graph
|
|
2
|
+
|
|
3
|
+
Embeddable read-only Nesso knowledge graph React component, built on [React Flow](https://reactflow.dev).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nesso-how/graph
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { NessoGraph } from '@nesso-how/graph'
|
|
15
|
+
import '@xyflow/react/dist/style.css'
|
|
16
|
+
|
|
17
|
+
<NessoGraph
|
|
18
|
+
nodes={nodes}
|
|
19
|
+
edges={edges}
|
|
20
|
+
display={{ edgeEncoding: 'full', curveStyle: 'straight' }}
|
|
21
|
+
style={{ width: '100%', height: 400 }}
|
|
22
|
+
/>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`nodes`/`edges` (or a full `graph: NessoGraphFile`) render read-only by default —
|
|
26
|
+
no drag, connect, or selection. Pass `display`/`palette` to control how categories,
|
|
27
|
+
glyphs, and curves are drawn; pass any other [`ReactFlow`](https://reactflow.dev/api-reference/react-flow)
|
|
28
|
+
prop through `reactFlowProps`.
|
|
29
|
+
|
|
30
|
+
### Interactivity
|
|
31
|
+
|
|
32
|
+
Turn on `nodesDraggable`/`nodesConnectable`/`elementsSelectable` as needed. How you
|
|
33
|
+
provide nodes determines who owns their state:
|
|
34
|
+
|
|
35
|
+
- `nodes`/`edges` — *controlled*: you own the state and must also pass
|
|
36
|
+
`onNodesChange`/`onEdgesChange`/`onConnect` to apply updates (e.g. the main app,
|
|
37
|
+
where positions live in its own store).
|
|
38
|
+
- `defaultNodes`/`defaultEdges` — *uncontrolled*: React Flow seeds its internal
|
|
39
|
+
state once and manages drag/connect/selection itself — no wiring needed, the
|
|
40
|
+
right choice for decorative or one-off embeds.
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
<NessoGraph
|
|
44
|
+
defaultNodes={nodes}
|
|
45
|
+
defaultEdges={edges}
|
|
46
|
+
nodesDraggable
|
|
47
|
+
style={{ width: '100%', height: 400 }}
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GlyphKind } from '@nesso-how/relation-types';
|
|
2
|
+
interface Props {
|
|
3
|
+
kind: GlyphKind;
|
|
4
|
+
color?: string;
|
|
5
|
+
size?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Renders a relation glyph from `@nesso-how/relation-types`' framework-agnostic SVG data. */
|
|
8
|
+
export declare function GlyphSVG({ kind, color, size }: Props): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
package/dist/GlyphSVG.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { GLYPH_PATHS } from '@nesso-how/relation-types';
|
|
4
|
+
/** Renders a relation glyph from `@nesso-how/relation-types`' framework-agnostic SVG data. */
|
|
5
|
+
export function GlyphSVG({ kind, color = 'currentColor', size = 14 }) {
|
|
6
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 14 14", style: { color }, strokeWidth: 1.4, strokeLinecap: "round", strokeLinejoin: "round", fill: "none", stroke: "currentColor", dangerouslySetInnerHTML: { __html: GLYPH_PATHS[kind] } }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Node, Edge, NodeTypes, EdgeTypes, ReactFlowProps, Viewport, OnNodesChange, OnEdgesChange, OnConnect, OnConnectStart, OnConnectEnd, OnMoveEnd } from '@xyflow/react';
|
|
2
|
+
import type { NessoGraphFile } from '@nesso-how/formats';
|
|
3
|
+
import type { ConceptNodeData, NessoEdgeData, GraphDisplaySettings, CategoryPalette } from '@nesso-how/types';
|
|
4
|
+
type PassthroughKeys = 'nodes' | 'defaultNodes' | 'edges' | 'defaultEdges' | 'nodeTypes' | 'edgeTypes' | 'nodesDraggable' | 'nodesConnectable' | 'elementsSelectable' | 'onNodesChange' | 'onEdgesChange' | 'onConnect' | 'onConnectStart' | 'onConnectEnd' | 'onSelectionChange' | 'onMoveEnd' | 'onNodeClick' | 'onEdgeClick' | 'fitView' | 'defaultViewport' | 'minZoom' | 'maxZoom' | 'panOnDrag' | 'zoomOnScroll';
|
|
5
|
+
export interface NessoGraphProps {
|
|
6
|
+
graph?: NessoGraphFile;
|
|
7
|
+
nodes?: Node[];
|
|
8
|
+
defaultNodes?: Node[];
|
|
9
|
+
edges?: Edge[];
|
|
10
|
+
defaultEdges?: Edge[];
|
|
11
|
+
display?: Partial<GraphDisplaySettings>;
|
|
12
|
+
palette?: CategoryPalette;
|
|
13
|
+
showConfidence?: boolean;
|
|
14
|
+
nodeTypes?: NodeTypes;
|
|
15
|
+
edgeTypes?: EdgeTypes;
|
|
16
|
+
nodesDraggable?: boolean;
|
|
17
|
+
nodesConnectable?: boolean;
|
|
18
|
+
elementsSelectable?: boolean;
|
|
19
|
+
panOnDrag?: boolean;
|
|
20
|
+
zoomOnScroll?: boolean;
|
|
21
|
+
onNodesChange?: OnNodesChange;
|
|
22
|
+
onEdgesChange?: OnEdgesChange;
|
|
23
|
+
onConnect?: OnConnect;
|
|
24
|
+
onConnectStart?: OnConnectStart;
|
|
25
|
+
onConnectEnd?: OnConnectEnd;
|
|
26
|
+
onSelectionChange?: ReactFlowProps['onSelectionChange'];
|
|
27
|
+
onMoveEnd?: OnMoveEnd;
|
|
28
|
+
onNodeClick?: (id: string, data: ConceptNodeData) => void;
|
|
29
|
+
onEdgeClick?: (id: string, data: NessoEdgeData) => void;
|
|
30
|
+
fitView?: boolean;
|
|
31
|
+
defaultViewport?: Viewport;
|
|
32
|
+
minZoom?: number;
|
|
33
|
+
maxZoom?: number;
|
|
34
|
+
reactFlowProps?: Omit<ReactFlowProps, PassthroughKeys>;
|
|
35
|
+
style?: React.CSSProperties;
|
|
36
|
+
className?: string;
|
|
37
|
+
onDoubleClick?: React.MouseEventHandler<HTMLDivElement>;
|
|
38
|
+
children?: React.ReactNode;
|
|
39
|
+
}
|
|
40
|
+
export declare function NessoGraph({ graph, nodes: nodesProp, defaultNodes, edges: edgesProp, defaultEdges, display, palette, showConfidence, nodeTypes, edgeTypes, nodesDraggable, nodesConnectable, elementsSelectable, panOnDrag, zoomOnScroll, onNodesChange, onEdgesChange, onConnect, onConnectStart, onConnectEnd, onSelectionChange, onMoveEnd, onNodeClick, onEdgeClick, fitView, defaultViewport, minZoom, maxZoom, reactFlowProps, style, className, onDoubleClick, children, }: NessoGraphProps): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { ReactFlow, Background, Controls } from '@xyflow/react';
|
|
5
|
+
import { GraphDisplayContext } from './context.js';
|
|
6
|
+
import { ReadOnlyConceptNode } from './ReadOnlyConceptNode.js';
|
|
7
|
+
import { ReadOnlyNessoEdge } from './ReadOnlyNessoEdge.js';
|
|
8
|
+
const DEFAULT_NODE_TYPES = { concept: ReadOnlyConceptNode };
|
|
9
|
+
const DEFAULT_EDGE_TYPES = { nesso: ReadOnlyNessoEdge };
|
|
10
|
+
export function NessoGraph({ graph, nodes: nodesProp, defaultNodes, edges: edgesProp, defaultEdges, display, palette = 'default', showConfidence = false, nodeTypes = DEFAULT_NODE_TYPES, edgeTypes = DEFAULT_EDGE_TYPES, nodesDraggable = false, nodesConnectable = false, elementsSelectable = true, panOnDrag = true, zoomOnScroll = true, onNodesChange, onEdgesChange, onConnect, onConnectStart, onConnectEnd, onSelectionChange, onMoveEnd, onNodeClick, onEdgeClick, fitView = true, defaultViewport, minZoom, maxZoom, reactFlowProps, style, className, onDoubleClick, children, }) {
|
|
11
|
+
const controlledNodes = nodesProp ?? graph?.nodes;
|
|
12
|
+
const controlledEdges = (edgesProp ?? graph?.edges);
|
|
13
|
+
// Controlled (`nodes`/`graph`) takes precedence; otherwise fall back to ReactFlow's
|
|
14
|
+
// own uncontrolled mode via `defaultNodes`/`defaultEdges` — see prop docs above.
|
|
15
|
+
const nodesProps = controlledNodes !== undefined
|
|
16
|
+
? { nodes: controlledNodes }
|
|
17
|
+
: { defaultNodes: defaultNodes ?? [] };
|
|
18
|
+
const edgesProps = controlledEdges !== undefined
|
|
19
|
+
? { edges: controlledEdges }
|
|
20
|
+
: { defaultEdges: defaultEdges ?? [] };
|
|
21
|
+
const ctx = useMemo(() => ({
|
|
22
|
+
edgeEncoding: display?.edgeEncoding ?? graph?.display?.edgeEncoding ?? 'full',
|
|
23
|
+
showHeatmap: display?.showHeatmap ?? graph?.display?.showHeatmap ?? true,
|
|
24
|
+
curveStyle: display?.curveStyle ?? graph?.display?.curveStyle ?? 'arc',
|
|
25
|
+
autoCurveFlip: display?.autoCurveFlip ?? graph?.display?.autoCurveFlip ?? true,
|
|
26
|
+
palette,
|
|
27
|
+
showConfidence,
|
|
28
|
+
}), [display, graph?.display, palette, showConfidence]);
|
|
29
|
+
return (_jsx("div", { style: { width: '100%', height: '100%', ...style }, className: className, onDoubleClick: onDoubleClick, children: _jsx(GraphDisplayContext.Provider, { value: ctx, children: _jsx(ReactFlow, { ...nodesProps, ...edgesProps, nodeTypes: nodeTypes, edgeTypes: edgeTypes, nodesDraggable: nodesDraggable, nodesConnectable: nodesConnectable, elementsSelectable: elementsSelectable, panOnDrag: panOnDrag, zoomOnScroll: zoomOnScroll, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onConnect: onConnect, onConnectStart: onConnectStart, onConnectEnd: onConnectEnd, onSelectionChange: onSelectionChange, onMoveEnd: onMoveEnd, fitView: fitView, defaultViewport: defaultViewport, minZoom: minZoom, maxZoom: maxZoom, onNodeClick: onNodeClick
|
|
30
|
+
? (_, node) => onNodeClick(node.id, node.data)
|
|
31
|
+
: undefined, onEdgeClick: onEdgeClick
|
|
32
|
+
? (_, edge) => onEdgeClick(edge.id, edge.data)
|
|
33
|
+
: undefined, ...reactFlowProps, children: children ?? (_jsxs(_Fragment, { children: [_jsx(Background, {}), _jsx(Controls, {})] })) }) }) }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Node, NodeProps } from '@xyflow/react';
|
|
2
|
+
import type { ConceptNodeData } from '@nesso-how/types';
|
|
3
|
+
type ConceptNodeType = Node<ConceptNodeData>;
|
|
4
|
+
export declare function ReadOnlyConceptNode({ data, selected }: NodeProps<ConceptNodeType>): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { Handle, Position } from '@xyflow/react';
|
|
4
|
+
import { useGraphDisplay } from './context.js';
|
|
5
|
+
import { ratingColor } from './ratingColor.js';
|
|
6
|
+
const HIDDEN_HANDLE = {
|
|
7
|
+
width: 1,
|
|
8
|
+
height: 1,
|
|
9
|
+
minWidth: 0,
|
|
10
|
+
background: 'transparent',
|
|
11
|
+
border: 'none',
|
|
12
|
+
borderRadius: 0,
|
|
13
|
+
opacity: 0,
|
|
14
|
+
pointerEvents: 'none',
|
|
15
|
+
};
|
|
16
|
+
export function ReadOnlyConceptNode({ data, selected }) {
|
|
17
|
+
const { showHeatmap, showConfidence } = useGraphDisplay();
|
|
18
|
+
const heatTint = ratingColor(data.lastRating ?? 0);
|
|
19
|
+
const confColor = showConfidence ? heatTint : 'var(--ink, #1a1a1a)';
|
|
20
|
+
const isStale = data.reps > 0 && data.due <= Date.now();
|
|
21
|
+
return (_jsxs("div", { style: {
|
|
22
|
+
position: 'relative',
|
|
23
|
+
padding: '6px 14px',
|
|
24
|
+
borderRadius: 999,
|
|
25
|
+
background: selected || showHeatmap
|
|
26
|
+
? 'var(--bg-card, #f5f5f5)'
|
|
27
|
+
: 'transparent',
|
|
28
|
+
border: selected || showHeatmap
|
|
29
|
+
? '0.5px solid var(--line, #d0d0d0)'
|
|
30
|
+
: '0.5px solid transparent',
|
|
31
|
+
cursor: 'default',
|
|
32
|
+
userSelect: 'none',
|
|
33
|
+
minWidth: 60,
|
|
34
|
+
}, children: [showHeatmap && (_jsx("div", { style: {
|
|
35
|
+
position: 'absolute',
|
|
36
|
+
inset: 0,
|
|
37
|
+
borderRadius: 999,
|
|
38
|
+
background: heatTint,
|
|
39
|
+
opacity: 0.14,
|
|
40
|
+
pointerEvents: 'none',
|
|
41
|
+
} })), selected && (_jsx("div", { style: {
|
|
42
|
+
position: 'absolute',
|
|
43
|
+
inset: -6,
|
|
44
|
+
borderRadius: 999,
|
|
45
|
+
border: '1px dashed var(--accent, #3b82f6)',
|
|
46
|
+
opacity: 0.7,
|
|
47
|
+
pointerEvents: 'none',
|
|
48
|
+
} })), _jsx("span", { style: {
|
|
49
|
+
font: '500 16px Fraunces, ui-serif, Georgia, serif',
|
|
50
|
+
letterSpacing: '-0.005em',
|
|
51
|
+
color: 'var(--ink, #1a1a1a)',
|
|
52
|
+
display: 'block',
|
|
53
|
+
whiteSpace: 'pre',
|
|
54
|
+
}, children: data.text }), _jsx("div", { style: {
|
|
55
|
+
position: 'absolute',
|
|
56
|
+
bottom: 5,
|
|
57
|
+
left: 16,
|
|
58
|
+
right: 16,
|
|
59
|
+
height: selected ? 1.4 : 0.8,
|
|
60
|
+
background: isStale && showConfidence
|
|
61
|
+
? `repeating-linear-gradient(90deg, ${confColor} 0, ${confColor} 4px, transparent 4px, transparent 8px)`
|
|
62
|
+
: confColor,
|
|
63
|
+
opacity: selected ? 0.9 : 0.55,
|
|
64
|
+
} }), _jsx(Handle, { id: "out", type: "source", position: Position.Right, style: HIDDEN_HANDLE }), _jsx(Handle, { id: "in", type: "target", position: Position.Left, style: HIDDEN_HANDLE })] }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Edge, EdgeProps } from '@xyflow/react';
|
|
2
|
+
import type { NessoEdgeData } from '@nesso-how/types';
|
|
3
|
+
type NessoFlowEdge = Edge<NessoEdgeData, 'nesso'>;
|
|
4
|
+
export declare function ReadOnlyNessoEdge({ source, target, data, selected, }: EdgeProps<NessoFlowEdge>): import("react/jsx-runtime").JSX.Element | null;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useStore } from '@xyflow/react';
|
|
5
|
+
import { PALETTES, RELATION_TYPES } from '@nesso-how/relation-types';
|
|
6
|
+
import { GlyphSVG } from './GlyphSVG.js';
|
|
7
|
+
import { useGraphDisplay } from './context.js';
|
|
8
|
+
import { effectiveCurveFlip, flowNodeCenterX, flowNodeCenterY, nessoArcPath, rectExit, } from './geometry.js';
|
|
9
|
+
function asEdgeTypeName(value, fallback = 'causes') {
|
|
10
|
+
return typeof value === 'string' && value in RELATION_TYPES ? value : fallback;
|
|
11
|
+
}
|
|
12
|
+
function EdgePathElement({ d, color, lineStyle, width = 1.5, opacity = 0.78, }) {
|
|
13
|
+
const base = {
|
|
14
|
+
fill: 'none',
|
|
15
|
+
stroke: color,
|
|
16
|
+
strokeWidth: width,
|
|
17
|
+
opacity,
|
|
18
|
+
strokeLinecap: 'round',
|
|
19
|
+
};
|
|
20
|
+
if (lineStyle === 'double') {
|
|
21
|
+
return (_jsxs("g", { children: [_jsx("path", { d: d, fill: "none", stroke: "var(--paper, #ffffff)", strokeWidth: width + 3, opacity: 1 }), _jsx("path", { d: d, ...base, strokeWidth: width * 2.6 }), _jsx("path", { d: d, fill: "none", stroke: "var(--paper, #ffffff)", strokeWidth: width * 0.7, opacity: 1 })] }));
|
|
22
|
+
}
|
|
23
|
+
if (lineStyle === 'wavy') {
|
|
24
|
+
return _jsx("path", { d: d, ...base, strokeDasharray: "1 4", strokeWidth: width * 1.2 });
|
|
25
|
+
}
|
|
26
|
+
if (lineStyle === 'dashed')
|
|
27
|
+
return _jsx("path", { d: d, ...base, strokeDasharray: "6 5" });
|
|
28
|
+
if (lineStyle === 'dotted')
|
|
29
|
+
return _jsx("path", { d: d, ...base, strokeDasharray: "0.1 5", strokeWidth: width * 1.4 });
|
|
30
|
+
return _jsx("path", { d: d, ...base });
|
|
31
|
+
}
|
|
32
|
+
export function ReadOnlyNessoEdge({ source, target, data, selected, }) {
|
|
33
|
+
const [hovered, setHovered] = useState(false);
|
|
34
|
+
const { edgeEncoding, curveStyle, autoCurveFlip, palette } = useGraphDisplay();
|
|
35
|
+
const sourceNode = useStore((s) => s.nodeLookup.get(source));
|
|
36
|
+
const targetNode = useStore((s) => s.nodeLookup.get(target));
|
|
37
|
+
const edgeType = asEdgeTypeName(data?.type);
|
|
38
|
+
const T = RELATION_TYPES[edgeType];
|
|
39
|
+
const color = edgeEncoding === 'minimal'
|
|
40
|
+
? 'var(--ink-3, #888888)'
|
|
41
|
+
: (PALETTES[palette]?.[T.cat] ?? '#666666');
|
|
42
|
+
const lineStyle = edgeEncoding === 'minimal' ? 'solid' : T.line;
|
|
43
|
+
const showLabel = edgeEncoding === 'full' || (edgeEncoding !== 'minimal' && (hovered || selected));
|
|
44
|
+
const straight = curveStyle === 'straight';
|
|
45
|
+
if (!sourceNode || !targetNode)
|
|
46
|
+
return null;
|
|
47
|
+
const sw = sourceNode.measured?.width ?? 80;
|
|
48
|
+
const sh = sourceNode.measured?.height ?? 32;
|
|
49
|
+
const tw = targetNode.measured?.width ?? 80;
|
|
50
|
+
const th = targetNode.measured?.height ?? 32;
|
|
51
|
+
const scx = sourceNode.internals.positionAbsolute.x + sw / 2;
|
|
52
|
+
const scy = flowNodeCenterY(sourceNode);
|
|
53
|
+
const tcx = targetNode.internals.positionAbsolute.x + tw / 2;
|
|
54
|
+
const tcy = flowNodeCenterY(targetNode);
|
|
55
|
+
const curveFlip = effectiveCurveFlip(autoCurveFlip, data?.curveFlipPinned, data?.curveFlip, flowNodeCenterX(sourceNode), scy, flowNodeCenterX(targetNode), tcy);
|
|
56
|
+
const pad = 6;
|
|
57
|
+
const flipSign = curveFlip ? -1 : 1;
|
|
58
|
+
const { a, b } = (() => {
|
|
59
|
+
if (straight) {
|
|
60
|
+
return {
|
|
61
|
+
a: rectExit(scx, scy, sw + pad * 2, sh + pad * 2, tcx, tcy),
|
|
62
|
+
b: rectExit(tcx, tcy, tw + pad * 2, th + pad * 2, scx, scy),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const dx = tcx - scx, dy = tcy - scy;
|
|
66
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
67
|
+
const nx = -dy / dist, ny = dx / dist;
|
|
68
|
+
const sibOff = (data?.siblingIdx ?? 0) * 14;
|
|
69
|
+
const bend = (Math.min(dist * 0.22, 90) + sibOff * 0.5) * flipSign;
|
|
70
|
+
const cpx = (scx + tcx) / 2 + nx * bend;
|
|
71
|
+
const cpy = (scy + tcy) / 2 + ny * bend;
|
|
72
|
+
return {
|
|
73
|
+
a: rectExit(scx, scy, sw + pad * 2, sh + pad * 2, cpx, cpy),
|
|
74
|
+
b: rectExit(tcx, tcy, tw + pad * 2, th + pad * 2, cpx, cpy),
|
|
75
|
+
};
|
|
76
|
+
})();
|
|
77
|
+
const { path, labelX, labelY, arrowAngle } = nessoArcPath(a.x, a.y, b.x, b.y, data?.siblingIdx ?? 0, straight, curveFlip);
|
|
78
|
+
const arrowSize = 7;
|
|
79
|
+
const a1 = arrowAngle + Math.PI - 0.45;
|
|
80
|
+
const a2 = arrowAngle + Math.PI + 0.45;
|
|
81
|
+
const ax1 = b.x + Math.cos(a1) * arrowSize;
|
|
82
|
+
const ay1 = b.y + Math.sin(a1) * arrowSize;
|
|
83
|
+
const ax2 = b.x + Math.cos(a2) * arrowSize;
|
|
84
|
+
const ay2 = b.y + Math.sin(a2) * arrowSize;
|
|
85
|
+
const w = selected ? 2 : 1.4;
|
|
86
|
+
const op = selected || hovered ? 1 : 0.78;
|
|
87
|
+
const r = 11;
|
|
88
|
+
return (_jsxs("g", { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), style: { cursor: 'default' }, children: [_jsx("path", { d: path, stroke: "transparent", strokeWidth: 14, fill: "none" }), _jsx(EdgePathElement, { d: path, color: color, lineStyle: lineStyle, width: w, opacity: op }), T.inverse !== 'self' && edgeEncoding !== 'minimal' && (_jsx("polygon", { points: `${b.x},${b.y} ${ax1},${ay1} ${ax2},${ay2}`, fill: color, opacity: 0.85 })), edgeEncoding !== 'minimal' && (_jsxs("g", { style: { pointerEvents: 'all' }, children: [_jsx("circle", { cx: labelX, cy: labelY, r: r, fill: "var(--paper, #ffffff)", stroke: color, strokeWidth: 1.2 }), _jsx("g", { transform: `translate(${labelX - 7}, ${labelY - 7})`, children: _jsx(GlyphSVG, { kind: T.glyph, color: color, size: 14 }) })] })), showLabel && (_jsx("foreignObject", { x: labelX - 60, y: labelY + r + 2, width: 120, height: 20, style: { overflow: 'visible', pointerEvents: 'none' }, children: _jsx("div", { style: {
|
|
89
|
+
display: 'inline-block',
|
|
90
|
+
background: 'var(--paper, #ffffff)',
|
|
91
|
+
border: '0.5px solid var(--line, #d0d0d0)',
|
|
92
|
+
borderRadius: 4,
|
|
93
|
+
padding: '1px 6px',
|
|
94
|
+
font: "500 10px 'JetBrains Mono', ui-monospace",
|
|
95
|
+
color,
|
|
96
|
+
letterSpacing: '0.02em',
|
|
97
|
+
whiteSpace: 'nowrap',
|
|
98
|
+
lineHeight: '16px',
|
|
99
|
+
}, children: T.label }) }))] }));
|
|
100
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EdgeEncoding, CurveStyle, CategoryPalette } from '@nesso-how/types';
|
|
2
|
+
export interface NessoGraphDisplayContext {
|
|
3
|
+
edgeEncoding: EdgeEncoding;
|
|
4
|
+
showHeatmap: boolean;
|
|
5
|
+
curveStyle: CurveStyle;
|
|
6
|
+
autoCurveFlip: boolean;
|
|
7
|
+
palette: CategoryPalette;
|
|
8
|
+
showConfidence: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const GraphDisplayContext: import("react").Context<NessoGraphDisplayContext>;
|
|
11
|
+
export declare function useGraphDisplay(): NessoGraphDisplayContext;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
const defaultContext = {
|
|
4
|
+
edgeEncoding: 'full',
|
|
5
|
+
showHeatmap: true,
|
|
6
|
+
curveStyle: 'arc',
|
|
7
|
+
autoCurveFlip: true,
|
|
8
|
+
palette: 'default',
|
|
9
|
+
showConfidence: false,
|
|
10
|
+
};
|
|
11
|
+
export const GraphDisplayContext = createContext(defaultContext);
|
|
12
|
+
export function useGraphDisplay() {
|
|
13
|
+
return useContext(GraphDisplayContext);
|
|
14
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export declare function defaultCurveFlip(sourceCenterX: number, sourceCenterY: number, targetCenterX: number, targetCenterY: number): boolean;
|
|
2
|
+
export declare function nodeCenterX(node: {
|
|
3
|
+
position: {
|
|
4
|
+
x: number;
|
|
5
|
+
};
|
|
6
|
+
measured?: {
|
|
7
|
+
width?: number;
|
|
8
|
+
};
|
|
9
|
+
}): number;
|
|
10
|
+
export declare function nodeCenterY(node: {
|
|
11
|
+
position: {
|
|
12
|
+
y: number;
|
|
13
|
+
};
|
|
14
|
+
measured?: {
|
|
15
|
+
height?: number;
|
|
16
|
+
};
|
|
17
|
+
}): number;
|
|
18
|
+
export declare function flowNodeCenterX(node: {
|
|
19
|
+
internals: {
|
|
20
|
+
positionAbsolute: {
|
|
21
|
+
x: number;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
measured?: {
|
|
25
|
+
width?: number;
|
|
26
|
+
};
|
|
27
|
+
}): number;
|
|
28
|
+
export declare function flowNodeCenterY(node: {
|
|
29
|
+
internals: {
|
|
30
|
+
positionAbsolute: {
|
|
31
|
+
y: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
measured?: {
|
|
35
|
+
height?: number;
|
|
36
|
+
};
|
|
37
|
+
}): number;
|
|
38
|
+
export declare function effectiveCurveFlip(auto: boolean, pinned: boolean | undefined, storedFlip: boolean | undefined, sourceCenterX: number, sourceCenterY: number, targetCenterX: number, targetCenterY: number): boolean;
|
|
39
|
+
export declare function rectExit(cx: number, cy: number, w: number, h: number, tx: number, ty: number): {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
};
|
|
43
|
+
export declare function nessoArcPath(sx: number, sy: number, tx: number, ty: number, siblingIdx?: number, straight?: boolean, curveFlip?: boolean): {
|
|
44
|
+
path: string;
|
|
45
|
+
labelX: number;
|
|
46
|
+
labelY: number;
|
|
47
|
+
arrowAngle: number;
|
|
48
|
+
};
|
package/dist/geometry.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Shared path math for Nesso edges — adapted from src/geometry/nessoEdgeGeometry.ts.
|
|
3
|
+
export function defaultCurveFlip(sourceCenterX, sourceCenterY, targetCenterX, targetCenterY) {
|
|
4
|
+
const targetAbove = targetCenterY < sourceCenterY;
|
|
5
|
+
const targetLeft = targetCenterX < sourceCenterX;
|
|
6
|
+
return targetAbove !== targetLeft;
|
|
7
|
+
}
|
|
8
|
+
export function nodeCenterX(node) {
|
|
9
|
+
const w = node.measured?.width ?? 80;
|
|
10
|
+
return node.position.x + w / 2;
|
|
11
|
+
}
|
|
12
|
+
export function nodeCenterY(node) {
|
|
13
|
+
const h = node.measured?.height ?? 32;
|
|
14
|
+
return node.position.y + h / 2;
|
|
15
|
+
}
|
|
16
|
+
export function flowNodeCenterX(node) {
|
|
17
|
+
const w = node.measured?.width ?? 80;
|
|
18
|
+
return node.internals.positionAbsolute.x + w / 2;
|
|
19
|
+
}
|
|
20
|
+
export function flowNodeCenterY(node) {
|
|
21
|
+
const h = node.measured?.height ?? 32;
|
|
22
|
+
return node.internals.positionAbsolute.y + h / 2;
|
|
23
|
+
}
|
|
24
|
+
export function effectiveCurveFlip(auto, pinned, storedFlip, sourceCenterX, sourceCenterY, targetCenterX, targetCenterY) {
|
|
25
|
+
if (auto && !pinned)
|
|
26
|
+
return defaultCurveFlip(sourceCenterX, sourceCenterY, targetCenterX, targetCenterY);
|
|
27
|
+
return Boolean(storedFlip);
|
|
28
|
+
}
|
|
29
|
+
export function rectExit(cx, cy, w, h, tx, ty) {
|
|
30
|
+
const dx = tx - cx, dy = ty - cy;
|
|
31
|
+
if (dx === 0 && dy === 0)
|
|
32
|
+
return { x: cx, y: cy };
|
|
33
|
+
const hx = w / 2, hy = h / 2;
|
|
34
|
+
const sx = dx === 0 ? Infinity : hx / Math.abs(dx);
|
|
35
|
+
const sy = dy === 0 ? Infinity : hy / Math.abs(dy);
|
|
36
|
+
const s = Math.min(sx, sy);
|
|
37
|
+
return { x: cx + dx * s, y: cy + dy * s };
|
|
38
|
+
}
|
|
39
|
+
export function nessoArcPath(sx, sy, tx, ty, siblingIdx = 0, straight = false, curveFlip = false) {
|
|
40
|
+
if (straight) {
|
|
41
|
+
const lx = (sx + tx) / 2;
|
|
42
|
+
const ly = (sy + ty) / 2;
|
|
43
|
+
return {
|
|
44
|
+
path: `M ${sx} ${sy} L ${tx} ${ty}`,
|
|
45
|
+
labelX: lx,
|
|
46
|
+
labelY: ly,
|
|
47
|
+
arrowAngle: Math.atan2(ty - sy, tx - sx),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const dx = tx - sx;
|
|
51
|
+
const dy = ty - sy;
|
|
52
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
53
|
+
const nx = -dy / dist;
|
|
54
|
+
const ny = dx / dist;
|
|
55
|
+
const off = siblingIdx * 14;
|
|
56
|
+
const sign = curveFlip ? -1 : 1;
|
|
57
|
+
const bend = (Math.min(dist * 0.22, 90) + off * 0.5) * sign;
|
|
58
|
+
const cpx = (sx + tx) / 2 + nx * bend;
|
|
59
|
+
const cpy = (sy + ty) / 2 + ny * bend;
|
|
60
|
+
const path = `M ${sx} ${sy} Q ${cpx} ${cpy} ${tx} ${ty}`;
|
|
61
|
+
const labelX = cpx * 0.5 + (sx + tx) * 0.25;
|
|
62
|
+
const labelY = cpy * 0.5 + (sy + ty) * 0.25;
|
|
63
|
+
const arrowAngle = Math.atan2(ty - cpy, tx - cpx);
|
|
64
|
+
return { path, labelX, labelY, arrowAngle };
|
|
65
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { NessoGraph } from './NessoGraph.js';
|
|
2
|
+
export type { NessoGraphProps } from './NessoGraph.js';
|
|
3
|
+
export { GlyphSVG } from './GlyphSVG.js';
|
|
4
|
+
export { ratingColor } from './ratingColor.js';
|
|
5
|
+
export { defaultCurveFlip, nodeCenterX, nodeCenterY, flowNodeCenterX, flowNodeCenterY, effectiveCurveFlip, rectExit, nessoArcPath, } from './geometry.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
export { NessoGraph } from './NessoGraph.js';
|
|
3
|
+
// Shared canvas utilities — import from here to avoid duplication with the main app.
|
|
4
|
+
export { GlyphSVG } from './GlyphSVG.js';
|
|
5
|
+
export { ratingColor } from './ratingColor.js';
|
|
6
|
+
export { defaultCurveFlip, nodeCenterX, nodeCenterY, flowNodeCenterX, flowNodeCenterY, effectiveCurveFlip, rectExit, nessoArcPath, } from './geometry.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ratingColor(rating: number, unratedFallback?: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
const RATING_COLORS = [
|
|
3
|
+
'var(--ink, #1a1a1a)',
|
|
4
|
+
'var(--conf-1, #ef4444)',
|
|
5
|
+
'var(--conf-2, #f97316)',
|
|
6
|
+
'var(--conf-4, #22c55e)',
|
|
7
|
+
'var(--conf-5, #3b82f6)',
|
|
8
|
+
];
|
|
9
|
+
export function ratingColor(rating, unratedFallback = 'var(--ink, #1a1a1a)') {
|
|
10
|
+
const idx = Math.max(0, Math.min(4, rating));
|
|
11
|
+
return idx === 0 ? unratedFallback : RATING_COLORS[idx];
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nesso-how/graph",
|
|
3
|
+
"version": "0.1.0-alpha.26",
|
|
4
|
+
"description": "Embeddable read-only Nesso knowledge graph React component",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/nesso-how/nesso",
|
|
9
|
+
"directory": "packages/graph"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "rm -rf dist && tsc",
|
|
26
|
+
"prepublishOnly": "pnpm build",
|
|
27
|
+
"dev": "tsc --watch"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@nesso-how/formats": "workspace:*",
|
|
31
|
+
"@nesso-how/relation-types": "workspace:*",
|
|
32
|
+
"@nesso-how/types": "workspace:*"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@xyflow/react": "^12.6.4",
|
|
36
|
+
"react": "^18.3.1",
|
|
37
|
+
"react-dom": "^18.3.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/react": "^18.3.23",
|
|
41
|
+
"@types/react-dom": "^18.3.7",
|
|
42
|
+
"typescript": "~5.8.3"
|
|
43
|
+
}
|
|
44
|
+
}
|