Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Component | Graph: SVGs in Link labels, Zoom start/end callbacks, Fit view to specific nodes, Docs update #465

Merged
merged 10 commits into from
Oct 30, 2024
25 changes: 21 additions & 4 deletions packages/angular/src/components/graph/graph.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from '@unovis/ts'
import { Selection } from 'd3-selection'
import { D3DragEvent } from 'd3-drag'
import { D3ZoomEvent } from 'd3-zoom'
import { D3ZoomEvent, ZoomTransform } from 'd3-zoom'
import { D3BrushEvent } from 'd3-brush'
import { VisCoreComponent } from '../../core'

Expand Down Expand Up @@ -310,9 +310,20 @@ export class VisGraphComponent<N extends GraphInputNode, L extends GraphInputLin
/** Custom "update" function for node rendering. Default: `undefined` */
@Input() nodeUpdateCustomRenderFunction?: (datum: GraphNode<N, L>, nodeGroupElementSelection: Selection<SVGGElement, GraphNode<N, L>, null, unknown>, config: GraphConfigInterface<N, L>, duration: number, zoomLevel: number) => void

/** Custom partial "update" function for node rendering which will be triggered after the following events:
* - Full node update (`nodeUpdateCustomRenderFunction`);
* - Background click;
* - Node and Link mouseover and mouseout;
* - Node brushing,
* Default: `undefined` */
@Input() nodePartialUpdateCustomRenderFunction?: (datum: GraphNode<N, L>, nodeGroupElementSelection: Selection<SVGGElement, GraphNode<N, L>, null, unknown>, config: GraphConfigInterface<N, L>, duration: number, zoomLevel: number) => void

/** Custom "exit" function for node rendering. Default: `undefined` */
@Input() nodeExitCustomRenderFunction?: (datum: GraphNode<N, L>, nodeGroupElementSelection: Selection<SVGGElement, GraphNode<N, L>, null, unknown>, config: GraphConfigInterface<N, L>, duration: number, zoomLevel: number) => void

/** Custom render function that will be called while zooming / panning the graph. Default: `undefined` */
@Input() nodeOnZoomCustomRenderFunction?: (datum: GraphNode<N, L>, nodeGroupElementSelection: Selection<SVGGElement, GraphNode<N, L>, null, unknown>, config: GraphConfigInterface<N, L>, zoomLevel: number) => void

/** Define the mode for highlighting selected nodes in the graph. Default: `GraphNodeSelectionHighlightMode.GreyoutNonConnected` */
@Input() nodeSelectionHighlightMode?: GraphNodeSelectionHighlightMode

Expand All @@ -335,7 +346,13 @@ export class VisGraphComponent<N extends GraphInputNode, L extends GraphInputLin
@Input() onNodeDragEnd?: (n: GraphNode<N, L>, event: D3DragEvent<SVGGElement, GraphNode<N, L>, unknown>) => void | undefined

/** Zoom event callback. Default: `undefined` */
@Input() onZoom?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent<SVGGElement, unknown> | undefined) => void
@Input() onZoom?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent<SVGGElement, unknown> | undefined, transform: ZoomTransform) => void

/** Zoom start event callback. Default: `undefined` */
@Input() onZoomStart?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent<SVGGElement, unknown> | undefined, transform: ZoomTransform) => void

/** Zoom end event callback. Default: `undefined` */
@Input() onZoomEnd?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent<SVGGElement, unknown> | undefined, transform: ZoomTransform) => void

/** Callback function to be called when the graph layout is calculated. Default: `undefined` */
@Input() onLayoutCalculated?: (n: GraphNode<N, L>[], links: GraphLink<N, L>[]) => void
Expand Down Expand Up @@ -369,8 +386,8 @@ export class VisGraphComponent<N extends GraphInputNode, L extends GraphInputLin
}

private getConfig (): GraphConfigInterface<N, L> {
const { duration, events, attributes, zoomScaleExtent, disableZoom, zoomEventFilter, disableDrag, disableBrush, zoomThrottledUpdateNodeThreshold, layoutType, layoutAutofit, layoutAutofitTolerance, layoutNonConnectedAside, layoutNodeGroup, layoutGroupOrder, layoutParallelNodesPerColumn, layoutParallelNodeSubGroup, layoutParallelSubGroupsPerRow, layoutParallelGroupSpacing, layoutParallelSortConnectionsByGroup, forceLayoutSettings, dagreLayoutSettings, layoutElkSettings, layoutElkNodeGroups, linkWidth, linkStyle, linkBandWidth, linkArrow, linkStroke, linkDisabled, linkFlow, linkFlowAnimDuration, linkFlowParticleSize, linkLabel, linkLabelShiftFromCenter, linkNeighborSpacing, linkCurvature, selectedLinkId, nodeSize, nodeStrokeWidth, nodeShape, nodeGaugeValue, nodeGaugeFill, nodeGaugeAnimDuration, nodeIcon, nodeIconSize, nodeLabel, nodeLabelTrim, nodeLabelTrimMode, nodeLabelTrimLength, nodeSubLabel, nodeSubLabelTrim, nodeSubLabelTrimMode, nodeSubLabelTrimLength, nodeSideLabels, nodeBottomIcon, nodeDisabled, nodeFill, nodeStroke, nodeSort, nodeEnterPosition, nodeEnterScale, nodeExitPosition, nodeExitScale, nodeEnterCustomRenderFunction, nodeUpdateCustomRenderFunction, nodeExitCustomRenderFunction, nodeSelectionHighlightMode, selectedNodeId, selectedNodeIds, panels, onNodeDragStart, onNodeDrag, onNodeDragEnd, onZoom, onLayoutCalculated, onNodeSelectionBrush, onNodeSelectionDrag, onRenderComplete } = this
const config = { duration, events, attributes, zoomScaleExtent, disableZoom, zoomEventFilter, disableDrag, disableBrush, zoomThrottledUpdateNodeThreshold, layoutType, layoutAutofit, layoutAutofitTolerance, layoutNonConnectedAside, layoutNodeGroup, layoutGroupOrder, layoutParallelNodesPerColumn, layoutParallelNodeSubGroup, layoutParallelSubGroupsPerRow, layoutParallelGroupSpacing, layoutParallelSortConnectionsByGroup, forceLayoutSettings, dagreLayoutSettings, layoutElkSettings, layoutElkNodeGroups, linkWidth, linkStyle, linkBandWidth, linkArrow, linkStroke, linkDisabled, linkFlow, linkFlowAnimDuration, linkFlowParticleSize, linkLabel, linkLabelShiftFromCenter, linkNeighborSpacing, linkCurvature, selectedLinkId, nodeSize, nodeStrokeWidth, nodeShape, nodeGaugeValue, nodeGaugeFill, nodeGaugeAnimDuration, nodeIcon, nodeIconSize, nodeLabel, nodeLabelTrim, nodeLabelTrimMode, nodeLabelTrimLength, nodeSubLabel, nodeSubLabelTrim, nodeSubLabelTrimMode, nodeSubLabelTrimLength, nodeSideLabels, nodeBottomIcon, nodeDisabled, nodeFill, nodeStroke, nodeSort, nodeEnterPosition, nodeEnterScale, nodeExitPosition, nodeExitScale, nodeEnterCustomRenderFunction, nodeUpdateCustomRenderFunction, nodeExitCustomRenderFunction, nodeSelectionHighlightMode, selectedNodeId, selectedNodeIds, panels, onNodeDragStart, onNodeDrag, onNodeDragEnd, onZoom, onLayoutCalculated, onNodeSelectionBrush, onNodeSelectionDrag, onRenderComplete }
const { duration, events, attributes, zoomScaleExtent, disableZoom, zoomEventFilter, disableDrag, disableBrush, zoomThrottledUpdateNodeThreshold, layoutType, layoutAutofit, layoutAutofitTolerance, layoutNonConnectedAside, layoutNodeGroup, layoutGroupOrder, layoutParallelNodesPerColumn, layoutParallelNodeSubGroup, layoutParallelSubGroupsPerRow, layoutParallelGroupSpacing, layoutParallelSortConnectionsByGroup, forceLayoutSettings, dagreLayoutSettings, layoutElkSettings, layoutElkNodeGroups, linkWidth, linkStyle, linkBandWidth, linkArrow, linkStroke, linkDisabled, linkFlow, linkFlowAnimDuration, linkFlowParticleSize, linkLabel, linkLabelShiftFromCenter, linkNeighborSpacing, linkCurvature, selectedLinkId, nodeSize, nodeStrokeWidth, nodeShape, nodeGaugeValue, nodeGaugeFill, nodeGaugeAnimDuration, nodeIcon, nodeIconSize, nodeLabel, nodeLabelTrim, nodeLabelTrimMode, nodeLabelTrimLength, nodeSubLabel, nodeSubLabelTrim, nodeSubLabelTrimMode, nodeSubLabelTrimLength, nodeSideLabels, nodeBottomIcon, nodeDisabled, nodeFill, nodeStroke, nodeSort, nodeEnterPosition, nodeEnterScale, nodeExitPosition, nodeExitScale, nodeEnterCustomRenderFunction, nodeUpdateCustomRenderFunction, nodePartialUpdateCustomRenderFunction, nodeExitCustomRenderFunction, nodeOnZoomCustomRenderFunction, nodeSelectionHighlightMode, selectedNodeId, selectedNodeIds, panels, onNodeDragStart, onNodeDrag, onNodeDragEnd, onZoom, onZoomStart, onZoomEnd, onLayoutCalculated, onNodeSelectionBrush, onNodeSelectionDrag, onRenderComplete } = this
const config = { duration, events, attributes, zoomScaleExtent, disableZoom, zoomEventFilter, disableDrag, disableBrush, zoomThrottledUpdateNodeThreshold, layoutType, layoutAutofit, layoutAutofitTolerance, layoutNonConnectedAside, layoutNodeGroup, layoutGroupOrder, layoutParallelNodesPerColumn, layoutParallelNodeSubGroup, layoutParallelSubGroupsPerRow, layoutParallelGroupSpacing, layoutParallelSortConnectionsByGroup, forceLayoutSettings, dagreLayoutSettings, layoutElkSettings, layoutElkNodeGroups, linkWidth, linkStyle, linkBandWidth, linkArrow, linkStroke, linkDisabled, linkFlow, linkFlowAnimDuration, linkFlowParticleSize, linkLabel, linkLabelShiftFromCenter, linkNeighborSpacing, linkCurvature, selectedLinkId, nodeSize, nodeStrokeWidth, nodeShape, nodeGaugeValue, nodeGaugeFill, nodeGaugeAnimDuration, nodeIcon, nodeIconSize, nodeLabel, nodeLabelTrim, nodeLabelTrimMode, nodeLabelTrimLength, nodeSubLabel, nodeSubLabelTrim, nodeSubLabelTrimMode, nodeSubLabelTrimLength, nodeSideLabels, nodeBottomIcon, nodeDisabled, nodeFill, nodeStroke, nodeSort, nodeEnterPosition, nodeEnterScale, nodeExitPosition, nodeExitScale, nodeEnterCustomRenderFunction, nodeUpdateCustomRenderFunction, nodePartialUpdateCustomRenderFunction, nodeExitCustomRenderFunction, nodeOnZoomCustomRenderFunction, nodeSelectionHighlightMode, selectedNodeId, selectedNodeIds, panels, onNodeDragStart, onNodeDrag, onNodeDragEnd, onZoom, onZoomStart, onZoomEnd, onLayoutCalculated, onNodeSelectionBrush, onNodeSelectionDrag, onRenderComplete }
const keys = Object.keys(config) as (keyof GraphConfigInterface<N, L>)[]
keys.forEach(key => { if (config[key] === undefined) delete config[key] })

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useRef, ReactElement, useState } from 'rea
import { Selection } from 'd3-selection'
import { cx } from '@emotion/css'
import { GraphNode, GraphLink, GraphConfigInterface, Graph, GraphNodeSelectionHighlightMode } from '@unovis/ts'
import { VisSingleContainer, VisGraph, VisSingleContainerProps, VisGraphProps } from '@unovis/react'
import { VisSingleContainer, VisGraph, VisSingleContainerProps, VisGraphProps, VisGraphRef } from '@unovis/react'
import { nodeEnterCustomRenderFunction, nodeSvgDefs, nodeUpdateCustomRenderFunction } from './node-rendering'
import { DEFAULT_NODE_SIZE, nodeTypeColorMap, nodeTypeIconMap } from './constants'
import type { CustomGraphNodeType } from './enums'
Expand All @@ -15,18 +15,21 @@ import { renderSwimlanes, updateSwimlanes } from './swimlane-rendering'
export type CustomGraphProps<
N extends CustomGraphNode,
L extends CustomGraphLink,
> = VisSingleContainerProps<{ links: L; nodes: N}> & VisGraphProps<N, L> & {
> = VisSingleContainerProps<{ links: L; nodes: N }> &
VisGraphProps<N, L> & {
links: L[];
nodes: N[];
onBackgroundClick?: (event: MouseEvent) => void;
onLinkClick?: (link: L, event: MouseEvent, i: number) => void;
onNodeClick?: (node: N, event: MouseEvent, i: number) => void;
};
}

export const CustomGraph = <N extends CustomGraphNode, L extends CustomGraphLink>(
props: CustomGraphProps<N, L>
): ReactElement => {
const [showLinkFlow, setShowLinkFlow] = useState(true)
// eslint-disable-next-line @typescript-eslint/naming-convention
function CustomGraphComponent<N extends CustomGraphNode, L extends CustomGraphLink> (
props: CustomGraphProps<N, L>,
ref: React.Ref<VisGraphRef<N, L> | null>
): ReactElement {
const graphRef = useRef<VisGraphRef<N, L>>(null)
const [selectedNodeId, setSelectedNodeId] = useState<string | undefined>(undefined)

const graphD3SelectionRef = useRef<Selection<SVGGElement, unknown, null, undefined> | null>(null)
Expand All @@ -38,84 +41,92 @@ export const CustomGraph = <N extends CustomGraphNode, L extends CustomGraphLink
return n.fillColor ?? nodeTypeColorMap.get(n.type as CustomGraphNodeType)
}, [])

const data = useMemo(() => ({
nodes: props.nodes,
links: props.links,
}), [props.nodes, props.links])
const data = useMemo(
() => ({
nodes: props.nodes,
links: props.links,
}),
[props.nodes, props.links]
)

const onRenderComplete = useCallback((
g: Selection<SVGGElement, unknown, null, undefined>,
nodes: GraphNode<N, L>[],
links: GraphLink<N, L>[],
config: GraphConfigInterface<N, L>,
duration: number,
zoomLevel: number
): void => {
graphD3SelectionRef.current = g
renderSwimlanes(g, nodes)
}, [])
const onRenderComplete = useCallback(
(
g: Selection<SVGGElement, unknown, null, undefined>,
nodes: GraphNode<N, L>[],
links: GraphLink<N, L>[],
config: GraphConfigInterface<N, L>,
duration: number,
zoomLevel: number
): void => {
graphD3SelectionRef.current = g
renderSwimlanes(g, nodes)
},
[]
)

const onZoom = useCallback((zoomLevel: number) => {
if (graphD3SelectionRef.current) {
updateSwimlanes(graphD3SelectionRef.current)
}
}, [])

const events = useMemo(() => ({
[Graph.selectors.node]: {
click: (n: N) => { setSelectedNodeId(n.id) },
},
[Graph.selectors.background]: {
click: () => { setSelectedNodeId(undefined) },
},
}), [setSelectedNodeId])
const events = useMemo(
() => ({
[Graph.selectors.node]: {
click: (n: N) => {
setSelectedNodeId(n.id)
},
},
[Graph.selectors.background]: {
click: () => {
setSelectedNodeId(undefined)
},
},
}),
[setSelectedNodeId]
)

React.useImperativeHandle(ref, () => graphRef.current)

return (
<>
<VisSingleContainer
className={cx(s.exaforceGraph, props.className)}
svgDefs={nodeSvgDefs}
data={data}
height={props.height}
duration={1000}
>
<VisGraph<N, L>
layoutType={'parallel'}
layoutNodeGroup={useCallback((n: N) => n.type, [])}
linkArrow={useCallback((l: L) => l.showArrow, [])}
linkBandWidth={useCallback((l: L) => l.bandWidth, [])}
linkCurvature={1}
linkFlow={useCallback((l: L) => showLinkFlow && l.showFlow, [showLinkFlow])}
linkWidth={useCallback((l: L) => l.width, [])}
nodeFill={getNodeFillColor}
nodeIcon={getNodeIcon}
nodeSize={DEFAULT_NODE_SIZE}
nodeIconSize={DEFAULT_NODE_SIZE}
nodeLabel={useCallback((n: N) => n.label, [])}
nodeLabelTrimLength={30}
nodeStroke={'none'}
nodeSubLabel={useCallback((n: N) => n.subLabel, [])}
nodeSubLabelTrimLength={30}
nodeEnterCustomRenderFunction={nodeEnterCustomRenderFunction}
nodeUpdateCustomRenderFunction={nodeUpdateCustomRenderFunction}
onRenderComplete={onRenderComplete}
nodeSelectionHighlightMode={GraphNodeSelectionHighlightMode.None}
onZoom={onZoom}
selectedNodeId={selectedNodeId}
events={events}
{...props}
/>
</VisSingleContainer>
<div className={s.checkboxContainer}>
<label>
<input
type="checkbox"
checked={showLinkFlow}
onChange={(e) => setShowLinkFlow(e.target.checked)}
/>
Show Link Flow
</label>
</div>
</>
<VisSingleContainer
className={cx(s.exaforceGraph, props.className)}
svgDefs={nodeSvgDefs}
data={data}
height={props.height}
duration={1000}
>
<VisGraph<N, L>
ref={graphRef}
layoutType={'parallel'}
layoutNodeGroup={useCallback((n: N) => n.type, [])}
linkArrow={useCallback((l: L) => l.showArrow, [])}
linkBandWidth={useCallback((l: L) => l.bandWidth, [])}
linkCurvature={1}
linkWidth={useCallback((l: L) => l.width, [])}
nodeFill={getNodeFillColor}
nodeIcon={getNodeIcon}
nodeSize={DEFAULT_NODE_SIZE}
nodeIconSize={DEFAULT_NODE_SIZE}
nodeLabel={useCallback((n: N) => n.label, [])}
nodeLabelTrimLength={30}
nodeStroke={'none'}
nodeSubLabel={useCallback((n: N) => n.subLabel, [])}
nodeSubLabelTrimLength={30}
nodeEnterCustomRenderFunction={nodeEnterCustomRenderFunction}
nodeUpdateCustomRenderFunction={nodeUpdateCustomRenderFunction}
onRenderComplete={onRenderComplete}
nodeSelectionHighlightMode={GraphNodeSelectionHighlightMode.None}
onZoom={onZoom}
selectedNodeId={selectedNodeId}
events={events}
zoomScaleExtent={useMemo(() => [0.5, 3], [])}
{...props}
/>
</VisSingleContainer>
)
}

export const CustomGraph = React.forwardRef(CustomGraphComponent) as <N extends CustomGraphNode, L extends CustomGraphLink>(
props: CustomGraphProps<N, L> & { ref?: React.Ref<VisGraphRef<N, L>> }
) => ReactElement
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import React, { useMemo } from 'react'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import type { GraphLink, GraphNode } from '@unovis/ts'
import type { VisGraphRef } from '@unovis/react'

import { CustomGraph } from './component'
import { CustomGraphNodeType } from './enums'
import type { CustomGraphLink, CustomGraphNode } from './types'

import * as s from './styles'

export const title = 'Graph: Custom Nodes'
export const subTitle = 'User provided rendering functions'

export const component = (): JSX.Element => {
const [showLinkFlow, setShowLinkFlow] = useState(true)
const graphRef = useRef<VisGraphRef<CustomGraphNode, CustomGraphLink> | null>(null)

const nodes: CustomGraphNode[] = useMemo(() => ([
{
id: '0',
Expand Down Expand Up @@ -35,8 +42,40 @@ export const component = (): JSX.Element => {
{ source: '1', target: '5', showFlow: true },
]), [])

// Modifying layout after the calculation
const onLayoutCalculated = useCallback((nodes: GraphNode<CustomGraphNode, CustomGraphLink>[], links: GraphLink<CustomGraphNode, CustomGraphLink>[]) => {
nodes[0].x = 100
}, [])


const fitView = useCallback((nodeIds?: string[]) => {
graphRef.current?.component?.fitView(undefined, nodeIds)
}, [])

return (
<CustomGraph nodes={nodes} links={links} height={'100vh'} />
<>
<CustomGraph
ref={graphRef}
nodes={nodes}
links={links}
height={'100vh'}
linkFlow={useCallback((l: CustomGraphLink) => showLinkFlow && l.showFlow, [showLinkFlow])}
onLayoutCalculated={onLayoutCalculated}

/>
<div className={s.checkboxContainer}>
<label>
<input
type="checkbox"
checked={showLinkFlow}
onChange={(e) => setShowLinkFlow(e.target.checked)}
/>
Show Link Flow
</label>
<button className={s.graphButton} onClick={() => fitView(['0', '1', '2', '3'])}>Zoom To Identity and Network Nodes</button>
<button className={s.graphButton} onClick={() => fitView()}>Fit Graph</button>
</div>
</>
)
}

Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,9 @@ export const checkboxContainer = css`
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
`

export const graphButton = css`
label: graph-button;
display: block;
margin-top: 5px;
`
Loading
Loading