diff --git a/frontend/app/src/components/graph/GraphEditor.tsx b/frontend/app/src/components/graph/GraphEditor.tsx
index 2649528cb..fb3dada40 100644
--- a/frontend/app/src/components/graph/GraphEditor.tsx
+++ b/frontend/app/src/components/graph/GraphEditor.tsx
@@ -21,7 +21,6 @@ import useSWR from 'swr';
export function GraphEditor ({ knowledgeBaseId }: { knowledgeBaseId: number }) {
const [query, setQuery] = useSearchParam('query', 'sample-question:What is TiDB?');
- const [graphStyle, setGraphStyle] = useState<'new' | 'legacy'>('new');
const [key, fetcher] = getFetchInfo(knowledgeBaseId, query);
@@ -34,7 +33,6 @@ export function GraphEditor ({ knowledgeBaseId }: { knowledgeBaseId: number }) {
return (
-
{(error != null) &&
Failed to fetch subgraph
{getErrorMessage(error)}
@@ -42,12 +40,11 @@ export function GraphEditor ({ knowledgeBaseId }: { knowledgeBaseId: number }) {
(
ref.current && createPortal(
void }) {
- return (
-
-
-
- )
-}
-
function Editor ({ knowledgeBaseId, network, target, onTargetChange, onEnterSubgraph }: NetworkViewerDetailsProps & { knowledgeBaseId: number, onEnterSubgraph: (type: string, entityId: IdType) => void }) {
if (target) {
if (target.type === 'link') {
diff --git a/frontend/app/src/components/graph/components/NetworkCanvas.tsx b/frontend/app/src/components/graph/components/NetworkCanvas.tsx
index 01b0db11c..0fa0b29b3 100644
--- a/frontend/app/src/components/graph/components/NetworkCanvas.tsx
+++ b/frontend/app/src/components/graph/components/NetworkCanvas.tsx
@@ -1,28 +1,26 @@
-import { useEffect, useRef, useState } from 'react';
import type { IdType, NetworkLink, NetworkNode, ReadonlyNetwork } from '../network/Network';
-import { NetworkRenderer, type NetworkRendererOptions } from '../network/NetworkRenderer';
+import { useEffect, useRef, useState } from 'react';
+
import { CanvasNetworkRenderer } from '../network/CanvasNetworkRenderer';
+import type { NetworkRendererOptions } from '../network/NetworkRendererOptions';
export interface NetworkCanvasProps extends NetworkRendererOptions {
network: ReadonlyNetwork;
target: { type: string, id: IdType } | undefined;
className?: string;
- useCanvasRenderer?: boolean;
}
-export function NetworkCanvas ({ className, network, target, useCanvasRenderer = false, ...options }: NetworkCanvasProps) {
+export function NetworkCanvas ({ className, network, target, ...options }: NetworkCanvasProps) {
const ref = useRef(null);
- const [renderer, setRenderer] = useState | CanvasNetworkRenderer>();
+ const [renderer, setRenderer] = useState>();
useEffect(() => {
- // Cleanup previous renderer if it exists (needed for renderer switching)
+ // Cleanup previous renderer if it exists
if (renderer) {
renderer.unmount();
}
- const newRenderer = useCanvasRenderer
- ? new CanvasNetworkRenderer(network, options)
- : new NetworkRenderer(network, options);
+ const newRenderer = new CanvasNetworkRenderer(network, options);
if (ref.current) {
newRenderer.mount(ref.current);
@@ -33,7 +31,7 @@ export function NetworkCanvas {
if (!renderer) {
diff --git a/frontend/app/src/components/graph/components/NetworkViewer.tsx b/frontend/app/src/components/graph/components/NetworkViewer.tsx
index d6814fa1e..cf6a0f7e5 100644
--- a/frontend/app/src/components/graph/components/NetworkViewer.tsx
+++ b/frontend/app/src/components/graph/components/NetworkViewer.tsx
@@ -4,7 +4,7 @@ import { type FC, type ReactNode, useMemo, useState } from 'react';
import { NetworkCanvas } from '../components/NetworkCanvas';
import { NetworkContext } from '../components/NetworkContext';
import { BaseNetwork, type IdType } from '../network/Network';
-import type { NetworkRendererOptions } from '../network/NetworkRenderer';
+import type { NetworkRendererOptions } from '../network/NetworkRendererOptions';
import { type Entity, type Relationship } from '../utils';
export interface NetworkViewerProps {
@@ -13,7 +13,6 @@ export interface NetworkViewerProps {
loading: boolean;
loadingTitle: ReactNode;
Details: FC;
- useCanvasRenderer?: boolean;
}
export interface NetworkViewerDetailsProps {
@@ -32,7 +31,7 @@ function randomPosition (radius: number, kbSpacing: number, kbIndex: number, kbC
};
}
-export function NetworkViewer ({ network, loading, loadingTitle, className, Details, useCanvasRenderer = false }: NetworkViewerProps) {
+export function NetworkViewer ({ network, loading, loadingTitle, className, Details }: NetworkViewerProps) {
const [target, setTarget] = useState<{ type: string, id: IdType }>();
const knowledgeGraphIndexMap = useMemo(() => {
@@ -146,7 +145,6 @@ export function NetworkViewer ({ network, loading, loadingTitle, className, Deta
className={cn('w-full h-full overflow-hidden')}
network={network}
target={target}
- useCanvasRenderer={useCanvasRenderer}
{...networkOptions}
/>
{
+ id: IdType;
+ index: number;
+ source: NetworkNodeView;
+ target: NetworkNodeView;
+ label?: string;
+ details?: string;
+ meta?: any;
+}
export class CanvasNetworkRenderer {
private _el: HTMLElement | undefined;
diff --git a/frontend/app/src/components/graph/network/NetworkRenderer.ts b/frontend/app/src/components/graph/network/NetworkRenderer.ts
deleted file mode 100644
index d8e1f0ec4..000000000
--- a/frontend/app/src/components/graph/network/NetworkRenderer.ts
+++ /dev/null
@@ -1,647 +0,0 @@
-import type { SimulationLinkDatum, SimulationNodeDatum } from 'd3';
-import * as d3 from 'd3';
-import type { IdType, NetworkLink, NetworkNode, ReadonlyNetwork } from './Network';
-
-export interface NetworkRendererOptions {
- showId?: boolean;
- showLinkLabel?: boolean;
-
- getNodeInitialAttrs?: (node: Node, index: number) => Pick;
-
- getNodeLabel?: (node: Node) => string | undefined;
- getNodeDetails?: (node: Node) => string | undefined;
- getNodeMeta?: (node: Node) => any;
- getNodeRadius?: (node: Node) => number;
- getNodeColor?: (node: Node) => string;
- getNodeStrokeColor?: (node: Node) => string;
- getNodeLabelColor?: (node: Node) => string;
- getNodeLabelStrokeColor?: (node: Node) => string;
-
- getLinkLabel?: (node: Link) => string | undefined;
- getLinkDetails?: (node: Link) => string | undefined;
- getLinkMeta?: (node: Link) => any;
-
- getLinkColor?: (link: Link) => string;
- getLinkLabelColor?: (link: Link) => string;
- getLinkLabelStrokeColor?: (link: Link) => string;
-
- onClickNode?: (node: Node, event: MouseEvent) => void;
- onClickLink?: (node: Link, event: MouseEvent) => void;
- onClickCanvas?: () => void;
-}
-
-export interface NetworkNodeView extends SimulationNodeDatum {
- id: IdType;
- index: number;
- radius: number;
- label?: string;
- details?: string;
- meta?: any;
-}
-
-export interface NetworkLinkView extends SimulationLinkDatum {
- id: IdType;
- index: number;
- source: NetworkNodeView;
- target: NetworkNodeView;
- label?: string;
- details?: string;
- meta?: any;
-}
-
-export class NetworkRenderer {
- private _el: HTMLElement | undefined;
- private _graph: d3.Selection;
- private _grid: d3.Selection;
- private _ro: ResizeObserver | undefined;
- private _gridZoomed: ((event: d3.D3ZoomEvent) => void) | undefined;
- private _destroyGrid: (() => void) | undefined;
-
- private _onFocusNode: ((id: IdType) => void) | undefined;
- private _onFocusLink: ((id: IdType) => void) | undefined;
- private _onBlurNode: (() => void) | undefined;
- private _onBlurLink: (() => void) | undefined;
-
- private _onUpdateLink: ((id: IdType) => void) | undefined;
- private _onUpdateNode: ((id: IdType) => void) | undefined;
-
- private nodes: NetworkNodeView[] = [];
- private links: NetworkLinkView[] = [];
-
- private canvas: HTMLCanvasElement | undefined;
- scale = 1;
-
- constructor (
- private network: ReadonlyNetwork,
- private options: NetworkRendererOptions,
- ) {
- ;(window as any).network = network;
-
- this.compile(options);
-
- this._graph = d3.create('svg')
- .attr('style', 'max-width: 100%; height: auto; height: intrinsic;');
-
- this._grid = this._graph
- .append('g');
- }
-
- private compile (options: NetworkRendererOptions) {
- const nodeMap = new Map();
- this.nodes = this.network.nodes().map((node, index) => {
- nodeMap.set(node.id, index);
- return {
- id: node.id,
- index,
- radius: options.getNodeRadius?.(node) ?? 5,
- label: options.getNodeLabel?.(node),
- details: options.getNodeDetails?.(node),
- meta: options.getNodeMeta?.(node),
- // x, y
- ...options.getNodeInitialAttrs?.(node, index),
- };
- });
- this.links = this.network.links().map((link, index) => ({
- id: link.id,
- index,
- source: this.nodes[nodeMap.get(link.source)!],
- target: this.nodes[nodeMap.get(link.target)!],
- label: options.getLinkLabel?.(link),
- details: options.getLinkDetails?.(link),
- meta: options.getLinkMeta?.(link),
- }));
- }
-
- get massive () {
- return this.nodes.length > 50 || this.links.length > 50;
- }
-
- mount (container: HTMLElement) {
- if (!this.canvas) {
- this.canvas = document.createElement('canvas');
- }
-
- if (this._el) {
- return;
- }
- this._el = container;
- const ro = this._ro = new ResizeObserver(([entry]) => {
- const { width, height } = entry.contentRect;
- this._graph
- .attr('width', width + 'px')
- .attr('height', height + 'px')
- .attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`);
-
- this.renderGrid(width, height);
- });
- ro.observe(container);
- container.style.overflow = 'hidden';
- container.append(this._graph.node()!);
- const { width, height } = container.getBoundingClientRect();
- this.render();
- this.renderGrid(width, height);
- }
-
- unmount () {
- if (this._onUpdateLink) {
- this.network.on('update:link', this._onUpdateLink);
- this._onUpdateLink = undefined;
- }
- if (this._onUpdateNode) {
- this.network.on('update:node', this._onUpdateNode);
- this._onUpdateNode = undefined;
- }
- if (!this._el) {
- return;
- }
- // Safely remove the graph node if it exists and is still a child
- const graphNode = this._graph.node();
- if (graphNode && this._el.contains(graphNode)) {
- this._el.removeChild(graphNode);
- }
- this._ro?.disconnect();
- this._ro = undefined;
- this._el = undefined;
- }
-
- private renderGrid (width: number, height: number) {
- this._destroyGrid?.();
-
- const svg = this._grid;
- const k = height / width;
-
- const x = d3.scaleLinear()
- .domain([-4.5, 4.5])
- .range([0, width]);
-
- const y = d3.scaleLinear()
- .domain([-4.5 * k, 4.5 * k])
- .range([height, 0]);
-
- const xAxis = (g: d3.Selection, x: d3.ScaleLinear) => g
- .attr('transform', `translate(${-width / 2},${height / 2})`)
- .call(d3.axisTop(x).ticks(12))
- .call(g => g.select('.domain').attr('display', 'none'));
-
- const yAxis = (g: d3.Selection, y: d3.ScaleLinear) => g
- .attr('transform', `translate(-${width / 2},-${height / 2})`)
- .call(d3.axisRight(y).ticks(12 * k))
- .call(g => g.select('.domain').attr('display', 'none'));
-
- const grid = (g: d3.Selection, x: d3.ScaleLinear, y: d3.ScaleLinear) => g
- .attr('transform', `translate(-${width / 2},-${height / 2})`)
- .attr('stroke', 'currentColor')
- .attr('stroke-opacity', 0.1)
- .call(g => g
- .selectAll('.x')
- .data(x.ticks(12))
- .join(
- enter => enter.append('line').attr('class', 'x').attr('y2', height),
- update => update,
- exit => exit.remove(),
- )
- .attr('x1', d => 0.5 + x(d))
- .attr('x2', d => 0.5 + x(d)))
- .call(g => g
- .selectAll('.y')
- .data(y.ticks(12 * k))
- .join(
- enter => enter.append('line').attr('class', 'y').attr('x2', width),
- update => update,
- exit => exit.remove(),
- )
- .attr('y1', d => 0.5 + y(d))
- .attr('y2', d => 0.5 + y(d)));
-
- const gGrid = svg.append('g');
-
- const gx = svg.append('g');
-
- const gy = svg.append('g');
-
- gGrid.call(grid, d3.zoomIdentity.rescaleX(x), d3.zoomIdentity.rescaleY(y));
-
- this._gridZoomed = ({ transform }) => {
- const zx = transform.rescaleX(x).interpolate(d3.interpolateRound);
- const zy = transform.rescaleY(y).interpolate(d3.interpolateRound);
-
- gx.call(xAxis, zx);
- gy.call(yAxis, zy);
- gGrid.call(grid, zx, zy);
- };
-
- this._destroyGrid = () => {
- gx.remove();
- gy.remove();
- gGrid.remove();
- };
- }
-
- focusNode (id: IdType): void {
- this._onFocusNode?.(id);
- }
-
- blurNode (): void {
- this._onBlurNode?.();
- }
-
- focusLink (id: IdType): void {
- this._onFocusLink?.(id);
-
- }
-
- blurLink (): void {
- this._onBlurLink?.();
- }
-
- render () {
- // @ts-ignore
- let currentLinkId: IdType | undefined = undefined;
- // @ts-ignore
- let currentNodeId: IdType | undefined = undefined;
-
- const svg = this._graph
- .on('click', () => {
- this.options.onClickCanvas?.();
- });
-
- const link = svg.append('g')
- .attr('opacity', 1)
- .attr('stroke-linejoin', 'round')
- .selectAll()
- .data(this.links)
- .join('g')
- .style('cursor', 'pointer')
- .on('click', (event, link) => {
- event.stopPropagation();
- this.options.onClickLink?.(this.network.link(link.id)!, event);
- });
-
- const linkPath = link
- .append('path')
- .attr('fill', d => this.options.getLinkColor?.(this.network.link(d.id) as any) ?? 'hsl(var(--primary) / 50%)');
-
- let linkText = (this.options.showLinkLabel !== false && !this.massive)
- ? link
- .append('text')
- .attr('font-size', 12)
- .attr('fill', d => this.options.getLinkLabelColor?.(this.network.link(d.id) as any) ?? 'hsl(var(--primary))')
- .attr('stroke', d => this.options.getLinkLabelStrokeColor?.(this.network.link(d.id) as any) ?? 'hsl(var(--primary-foreground))')
- .attr('stroke-width', 2)
- .attr('paint-order', 'stroke')
- .attr('text-anchor', 'middle')
- .text(d => this.options.getLinkLabel?.(this.network.link(d.id)!) ?? '')
- : undefined;
-
- const node = svg.append('g')
- .selectAll('g')
- .data(this.nodes)
- .enter()
- .append('g')
- .attr('title', d => d.details ?? null)
- .style('cursor', 'pointer')
- .attr('opacity', 1)
- .on('click', (event, node) => {
- event.stopPropagation();
- this.options.onClickNode?.(this.network.node(node.id)!, event);
- });
-
- const circle = node
- .append('circle')
- .attr('stroke', d => this.options.getNodeStrokeColor?.(this.network.node(d.id) as any) ?? 'hsl(var(--primary))')
- .attr('fill', d => this.options.getNodeColor?.(this.network.node(d.id) as any) ?? 'hsl(var(--primary))')
- .attr('r', node => node.radius / this.scale);
-
- const text = node
- .append('text')
- .text((d) => this.createText(d.id))
- .attr('fill', d => this.options.getNodeLabelColor?.(this.network.node(d.id) as any) ?? 'hsl(var(--primary))')
- .attr('stroke', d => this.options.getNodeLabelStrokeColor?.(this.network.node(d.id) as any) ?? 'hsl(var(--primary-foreground))')
- .attr('stroke-width', 2)
- .attr('font-size', 12)
- .attr('paint-order', 'stroke')
- .attr('alignment-baseline', 'center')
- .attr('opacity', 1);
-
- const ticked = () => {
- linkPath.attr('d', (node) => this.createLinkPath(node.source, node.target, 1));
-
- linkText?.call((selection) => {
- this.assignLinkText(selection, this.scale);
- });
-
- circle
- .attr('cx', d => d.x!)
- .attr('cy', d => d.y!)
- .call(drag(simulation));
-
- text
- .attr('x', node => this.createTextX(node, this.scale))
- .attr('y', node => this.createTextY(node, this.scale));
-
- };
-
- const zoomed = (event: d3.D3ZoomEvent) => {
- const { transform } = event;
- if (isFinite(transform.k)) {
- this.scale = transform.k;
- }
-
- circle.attr('transform', transform as any);
- // .attr('r', node => node.radius / transform.k)
- // .attr('stroke-width', 1 / transform.k);
-
- link.attr('transform', transform as any);
-
- // linkPath.attr('transform', transform as any);
- // .attr('stroke-width', 3 / transform.k)
- // .attr('d', (node) => this.createLinkPath(node.source, node.target, transform.k));
-
- linkText
- ?.attr('font-size', 12 / transform.k)
- .attr('stroke-width', 2 / transform.k)
- .call((selection) => {
- this.assignLinkText(selection, transform.k);
- });
-
- link.append('title')
- .text(d => this.options.getLinkLabel?.(this.network.link(d.id)!) ?? '');
-
- text.attr('transform', transform as any)
- .attr('font-size', 12 / transform.k)
- .attr('stroke-width', 2 / transform.k)
- .attr('x', node => this.createTextX(node, transform.k))
- .attr('y', node => this.createTextY(node, transform.k));
-
- this._gridZoomed?.(event);
- };
-
- const forceNode = d3.forceCollide(node => node.radius * 1.5);
- const forceManyBody = d3.forceManyBody();
- const forceLink = d3.forceLink(this.links).strength(0.01).id(node => node.id);
- const forceCenter = d3.forceCenter();
-
- const simulation = d3.forceSimulation(this.nodes)
- .force('link', forceLink)
- .force('charge', forceNode)
- .force('manyBody', forceManyBody)
- .force('center', forceCenter)
- .on('tick', ticked);
-
- const drag = (simulation: d3.Simulation) => {
- const dragstarted = (event: d3.D3DragEvent) => {
- if (!event.active) simulation.alphaTarget(0.3).restart();
- event.subject.fx = event.subject.x;
- event.subject.fy = event.subject.y;
- };
-
- const dragged = (event: d3.D3DragEvent) => {
- event.subject.fx += event.dx / this.scale;
- event.subject.fy += event.dy / this.scale;
- };
-
- const dragended = (event: d3.D3DragEvent) => {
- if (!event.active) simulation.alphaTarget(0);
- event.subject.fx = null;
- event.subject.fy = null;
- };
-
- return d3.drag()
- .on('start', dragstarted)
- .on('drag', dragged)
- .on('end', dragended);
- };
-
- const zoom = d3.zoom()
- .scaleExtent([0.1, 32])
- .on('zoom', zoomed);
-
- svg
- .call(zoom)
- .call(zoom.transform, d3.zoomIdentity);
-
- const onFocusLink = (id: IdType) => {
- currentLinkId = id;
- const currentLink = this.network.link(id);
- link.transition().attr('opacity', link => link.id === id ? 1 : 0.2);
- node.transition().attr('opacity', node => {
- return (currentLink?.target === node.id || currentLink?.source === node.id) ? 1 : 0.2;
- });
- if (this.massive) {
- text.transition()
- .attr('opacity', (node) => {
- return (currentLink?.target === node.id || currentLink?.source === node.id) ? 1 : 0;
- });
- }
- };
-
- const onFocusNode = (id: IdType) => {
- currentNodeId = id;
- const currentNode = this.network.node(id);
-
- node
- .transition()
- .attr('opacity', node => {
- if (node.id === currentNode?.id) {
- return 1;
- }
- if (this.network.nodeNeighborhoods(id)?.has(node.id)) {
- return 0.7;
- }
- return 0.2;
- });
-
- if (this.massive) {
- text.transition()
- .attr('opacity', node => {
- if (node.id === currentNode?.id) {
- return 1;
- }
- if (this.network.nodeNeighborhoods(id)?.has(node.id)) {
- return 0.7;
- }
- return 0;
- });
- }
-
- link
- .transition()
- .attr('opacity', link => {
- return (link.source.id === currentNode?.id || link.target.id === currentNode?.id) ? 1 : 0.2;
- });
- };
-
- const onBlur = () => {
- currentLinkId = undefined;
- currentNodeId = undefined;
- node.transition().attr('opacity', 1);
- link.transition().attr('opacity', 1);
- if (this.massive) {
- text.transition().attr('opacity', 0);
- }
- };
-
- this._onFocusNode = onFocusNode;
- this._onFocusLink = onFocusLink;
- this._onBlurNode = onBlur;
- this._onBlurLink = onBlur;
-
- this._onUpdateLink = () => {
- // noop currently
- };
-
- this._onUpdateNode = id => {
- text.filter(node => node.id === id).text(this.createText(id));
- };
-
- this.network.on('update:link', this._onUpdateLink);
- this.network.on('update:node', this._onUpdateNode);
- }
-
- private createText (id: IdType) {
- const node = this.network.node(id) as any;
- return (node?.name ?? node?.description) + (this.options.showId ? ` #${id}` : '');
- }
-
- private assignLinkText (text: d3.Selection, scale: number) {
- const options = this.options;
- const network = this.network;
- const that = this;
- text.each(function (link) {
- let source = link.source;
- let target = link.target;
-
- if (source.x! > target.x!) {
- let t = target;
- target = source;
- source = t;
- }
-
- const theta = Math.atan2(target.x! - source.x!, target.y! - source.y!);
- const x1 = source.x! + Math.sin(theta) * (source.radius + 3) / scale;
- const y1 = source.y! + Math.cos(theta) * (source.radius + 3) / scale;
- const x2 = target.x! - Math.sin(theta) * (target.radius + 3) / scale;
- const y2 = target.y! - Math.cos(theta) * (target.radius + 3) / scale;
-
- const off = 2 + 6 / scale;
-
- const dx = x2 - x1;
- const dy = y2 - y1;
-
- const d = Math.sqrt(dx * dx + dy * dy);
-
- const nx = dx / d;
- const ny = dy / d;
-
- const x = x1 + dx / 2 + ny * off;
- const y = y1 + dy / 2 - nx * off;
-
- const text = options.getLinkLabel?.(network.link(link.id)!) ?? '';
-
- d3.select(this)
- .attr('x', x)
- .attr('y', y)
- .attr('width', d)
- .attr('transform', `rotate(${-theta / Math.PI * 180 - 270})`)
- .attr('transform-origin', `${x} ${y}`)
- .text(that.clipText(text, d, 12 / scale, 2 / scale));
- });
- }
-
- private createLinkPath (source: NetworkNodeView, target: NetworkNodeView, scale: number): string {
- const sourceR = source.radius;
- const targetR = target.radius;
-
- const theta = Math.atan2(target.x! - source.x!, target.y! - source.y!);
- const x1 = source.x! + Math.sin(theta) * (sourceR + 3) / scale;
- const y1 = source.y! + Math.cos(theta) * (sourceR + 3) / scale;
- const x2 = target.x! - Math.sin(theta) * (targetR + 3) / scale;
- const y2 = target.y! - Math.cos(theta) * (targetR + 3) / scale;
- return arrow(x1, y1, x2, y2, 3 / scale);
- }
-
- private createTextX (node: NetworkNodeView, _scale: number): number {
- return node.x! + (node.radius + 1);
- }
-
- private createTextY (node: NetworkNodeView, _scale: number): number {
- return node.y! + node.radius / 2;
- }
-
- private clipText (text: string, width: number, fontSize: number, lineWidth: number): string {
- const ctx = this.canvas!.getContext('2d')!;
- ctx.font = `${fontSize}px sans-serif`;
- ctx.lineWidth = lineWidth;
- const rect = ctx.measureText(text);
-
- if (compare(rect, width)) {
- return text;
- }
-
- let min = 0;
- let max = text.length;
-
- while (max - min > 1) {
- const t = Math.floor((min + max) / 2);
- const rect = ctx.measureText(text.slice(0, t) + '...');
- if (compare(rect, width)) {
- min = t;
- } else {
- max = t;
- }
- }
-
- if (min < 3) {
- return '';
- }
- return text.slice(0, min) + '...';
- }
-}
-
-function compare (metrics: TextMetrics, width: number) {
- return metrics.actualBoundingBoxRight - metrics.actualBoundingBoxLeft < width;
-}
-
-function arrow (x1: number, y1: number, x2: number, y2: number, width: number) {
- const dx = x2 - x1;
- const dy = y2 - y1;
-
- const d = Math.sqrt(dx * dx + dy * dy);
-
- const nx = dx / d;
- const ny = dy / d;
-
- const s1x = x1 + ny * width / 2;
- const s1y = y1 - nx * width / 2;
-
- const s2x = x1 - ny * width / 2;
- const s2y = y1 + nx * width / 2;
-
- const tbx = x2 - nx * width * 3.5;
- const tby = y2 - ny * width * 3.5;
-
- const tb1x = tbx + ny * width / 2;
- const tb1y = tby - nx * width / 2;
-
- const t1x = tbx + ny * width * 2;
- const t1y = tby - nx * width * 2;
-
- const t2x = tbx - ny * width * 2;
- const t2y = tby + nx * width * 2;
-
- const tb2x = tbx - ny * width / 2;
- const tb2y = tby + nx * width / 2;
-
- return [
- 'M', x1, y1,
- 'L', s1x, s1y,
- 'L', tb1x, tb1y,
- 'L', t1x, t1y,
- 'L', x2, y2,
- 'L', t2x, t2y,
- 'L', tb2x, tb2y,
- 'L', s2x, s2y,
- 'z',
- ].join(' ');
-
-}
diff --git a/frontend/app/src/components/graph/network/NetworkRendererOptions.ts b/frontend/app/src/components/graph/network/NetworkRendererOptions.ts
new file mode 100644
index 000000000..12b79153d
--- /dev/null
+++ b/frontend/app/src/components/graph/network/NetworkRendererOptions.ts
@@ -0,0 +1,30 @@
+import type { SimulationNodeDatum } from 'd3';
+
+export interface NetworkRendererOptions {
+ showId?: boolean;
+ showLinkLabel?: boolean;
+
+ getNodeInitialAttrs?: (node: Node, index: number) => Pick;
+
+ getNodeLabel?: (node: Node) => string | undefined;
+ getNodeDetails?: (node: Node) => string | undefined;
+ getNodeMeta?: (node: Node) => any;
+ getNodeRadius?: (node: Node) => number;
+ getNodeColor?: (node: Node) => string;
+ getNodeStrokeColor?: (node: Node) => string;
+ getNodeLabelColor?: (node: Node) => string;
+ getNodeLabelStrokeColor?: (node: Node) => string;
+
+ getLinkLabel?: (node: Link) => string | undefined;
+ getLinkDetails?: (node: Link) => string | undefined;
+ getLinkMeta?: (node: Link) => any;
+
+ getLinkColor?: (link: Link) => string;
+ getLinkLabelColor?: (link: Link) => string;
+ getLinkLabelStrokeColor?: (link: Link) => string;
+
+ onClickNode?: (node: Node, event: MouseEvent) => void;
+ onClickLink?: (node: Link, event: MouseEvent) => void;
+ onClickCanvas?: () => void;
+}
+