From 4e4aa9f7b33b4c6caa457584b695a2ec16d7bdbd Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Mon, 26 Aug 2024 16:11:52 -0700 Subject: [PATCH 01/10] Component | Graph: Ability to render link labels as SVG `use` elements for internal links Render link labels as SVG `use` elements when the label text is an id pointing to the element provided via `svgDefs` to the container. This allows the link labels to use SVG icons instead of simple text --- .../components/graph/modules/link/index.ts | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/ts/src/components/graph/modules/link/index.ts b/packages/ts/src/components/graph/modules/link/index.ts index 08a1575a8..1fbf8dc33 100644 --- a/packages/ts/src/components/graph/modules/link/index.ts +++ b/packages/ts/src/components/graph/modules/link/index.ts @@ -19,7 +19,7 @@ import { GraphCircleLabel, GraphLink, GraphLinkArrowStyle, GraphLinkStyle } from import { GraphConfigInterface } from '../../config' // Helpers -import { getX, getY } from '../node/helper' +import { getX, getY, isInternalHref } from '../node/helper' import { getLinkShiftTransform, getLinkStrokeWidth, @@ -66,9 +66,6 @@ export function createLinks linkLabelGroup.append('rect') .attr('class', linkSelectors.linkLabelBackground) - - linkLabelGroup.append('text') - .attr('class', linkSelectors.linkLabelContent) } /** Updates the links partially according to their `_state` */ @@ -206,13 +203,38 @@ export function updateLinks const linkLabelPos = linkPathElement.getPointAtLength(pathLength / 2 + linkLabelShift) const linkLabelTranslate = `translate(${linkLabelPos.x}, ${linkLabelPos.y})` const linkLabelBackground = linkLabelGroup.select(`.${linkSelectors.linkLabelBackground}`) - const linkLabelContent = linkLabelGroup.select(`.${linkSelectors.linkLabelContent}`) + let linkLabelContent = linkLabelGroup.select(`.${linkSelectors.linkLabelContent}`) // If the label was hidden or didn't have text before, we need to set the initial position - if (!linkLabelContent.text() || linkLabelContent.attr('hidden')) { + if (!linkLabelContent.size() || !linkLabelContent.text() || linkLabelContent.attr('hidden')) { linkLabelGroup.attr('transform', linkLabelTranslate) } + // Update the label content DOM element (text vs use) + const shouldRenderUseElement = isInternalHref(linkLabelText) + linkLabelGroup.select(`.${linkSelectors.linkLabelContent}`).remove() + linkLabelContent = linkLabelGroup + .append(shouldRenderUseElement ? 'use' : 'text') + .attr('class', linkSelectors.linkLabelContent) + + const linkLabelFontSize = toPx(linkLabelDatum.fontSize) ?? getCSSVariableValueInPixels('var(--vis-graph-link-label-font-size)', linkLabelContent.node()) + const linkLabelColor = linkLabelDatum.textColor ?? getLinkLabelTextColor(linkLabelDatum) + if (shouldRenderUseElement) { + linkLabelContent + .attr('href', linkLabelText) + .attr('x', -linkLabelFontSize / 2) + .attr('y', -linkLabelFontSize / 2) + .attr('width', linkLabelFontSize) + .attr('height', linkLabelFontSize) + .style('fill', linkLabelColor) + } else { + linkLabelContent + .text(linkLabelText) + .attr('dy', '0.1em') + .style('font-size', linkLabelFontSize) + .style('fill', linkLabelColor) + } + linkLabelGroup.attr('hidden', null) .style('cursor', linkLabelDatum.cursor) @@ -220,16 +242,9 @@ export function updateLinks .attr('transform', linkLabelTranslate) .style('opacity', 1) - linkLabelContent - .text(linkLabelText) - .attr('dy', '0.1em') - .style('font-size', linkLabelDatum.fontSize) - .style('fill', linkLabelDatum.textColor ?? getLinkLabelTextColor(linkLabelDatum)) - - const shouldBeRenderedAsCircle = linkLabelText.length <= 2 + const shouldBeRenderedAsCircle = linkLabelText.length <= 2 || shouldRenderUseElement const linkLabelPaddingVertical = 4 const linkLabelPaddingHorizontal = shouldBeRenderedAsCircle ? linkLabelPaddingVertical : 8 - const linkLabelFontSize = toPx(linkLabelDatum.fontSize) ?? getCSSVariableValueInPixels('var(--vis-graph-link-label-font-size)', linkLabelContent.node()) const linkLabelWidthPx = estimateStringPixelLength(linkLabelText, linkLabelFontSize) const linkLabelBackgroundBorderRadius = linkLabelDatum.radius ?? (shouldBeRenderedAsCircle ? linkLabelFontSize : 4) const linkLabelBackgroundWidth = (shouldBeRenderedAsCircle ? linkLabelFontSize : linkLabelWidthPx) From 8e6f175194dbaec92e3a303ee745f6c4ee1d4b51 Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Tue, 27 Aug 2024 11:18:38 -0700 Subject: [PATCH 02/10] Dev | Examples | Graph: Link Label Icons --- .../graph/graph-link-label-icons/bucket.svg | 3 + .../graph/graph-link-label-icons/index.tsx | 69 +++++++++++++++++++ .../graph/graph-link-label-icons/instance.svg | 5 ++ .../graph/graph-link-label-icons/person.svg | 10 +++ .../graph/graph-link-label-icons/role.svg | 3 + 5 files changed, 90 insertions(+) create mode 100644 packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/bucket.svg create mode 100644 packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/index.tsx create mode 100644 packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/instance.svg create mode 100644 packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/person.svg create mode 100644 packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/role.svg diff --git a/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/bucket.svg b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/bucket.svg new file mode 100644 index 000000000..d2d024def --- /dev/null +++ b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/bucket.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/index.tsx b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/index.tsx new file mode 100644 index 000000000..c3d7fb573 --- /dev/null +++ b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/index.tsx @@ -0,0 +1,69 @@ +import React, { useState, useEffect } from 'react' +import { VisSingleContainer, VisGraph } from '@unovis/react' +import { ExampleViewerDurationProps } from '@src/components/ExampleViewer/index' +import { sample } from '@src/utils/array' + +import personIcon from './person.svg?raw' +import roleIcon from './role.svg?raw' +import instanceIcon from './instance.svg?raw' +import bucketIcon from './bucket.svg?raw' + +export const title = 'Graph: Link Label Icons' +export const subTitle = 'SVG icons in link labels' + +export const component = (props: ExampleViewerDurationProps): JSX.Element => { + const svgDefs = ` + ${personIcon} + ${roleIcon} + ${instanceIcon} + ${bucketIcon} + ` + + const nodes = [ + { id: 'jdoe@acme.com', icon: '#personIcon', fillColor: '#DFFAFD', label: 'External User', sublabel: 'jdoe@acme.com' }, + { id: 'AWSReservedSSO_Something', icon: '#roleIcon', fillColor: '#E3DEFC', label: 'Role', sublabel: 'AWSReservedSSO_Something' }, + ] + + const links = [ + { source: 0, target: 1, label: { text: '~' } }, + ] + + const [data, setData] = useState({ nodes, links }) + + // Re-render the component here to test how the link label updates + useEffect(() => { + const interval = setInterval(() => { + const links = [ + { source: 0, target: 1, label: { text: sample(['#personIcon', '#roleIcon', '#instanceIcon', '#bucketIcon', '2', 'long label']) } }, + ] + + setData({ nodes, links }) + }, 1000) + + return () => clearInterval(interval) + }, []) + + return ( + + + data={data} + nodeIcon={(n) => n.icon} + nodeIconSize={18} + nodeStroke={'none'} + nodeFill={n => n.fillColor} + nodeLabel={n => n.label} + nodeSubLabel={n => n.sublabel} + layoutType='dagre' + dagreLayoutSettings={{ + rankdir: 'LR', + ranksep: 120, + nodesep: 20, + }} + linkArrow={'single'} + linkLabel={(l: typeof links[0]) => l.label} + duration={props.duration} + /> + + ) +} + diff --git a/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/instance.svg b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/instance.svg new file mode 100644 index 000000000..7dfa6895d --- /dev/null +++ b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/instance.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/person.svg b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/person.svg new file mode 100644 index 000000000..0b2c3d93d --- /dev/null +++ b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/person.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/role.svg b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/role.svg new file mode 100644 index 000000000..90dbf6051 --- /dev/null +++ b/packages/dev/src/examples/networks-and-flows/graph/graph-link-label-icons/role.svg @@ -0,0 +1,3 @@ + + + From b10aace718ee27db0f976d7bad94cf31920218aa Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Tue, 27 Aug 2024 11:31:50 -0700 Subject: [PATCH 03/10] Website | Docs | Graph: Custom SVG link labels --- packages/website/docs/networks-and-flows/Graph.mdx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/website/docs/networks-and-flows/Graph.mdx b/packages/website/docs/networks-and-flows/Graph.mdx index 3b0f1cb2e..79ccbcc90 100644 --- a/packages/website/docs/networks-and-flows/Graph.mdx +++ b/packages/website/docs/networks-and-flows/Graph.mdx @@ -309,9 +309,14 @@ Providing an accessor function to `linkArrow` will turn on arrows display on lin sample([undefined, 'single', 'double'])} /> ### Labels -Links can have textual label. When the label is short (two characters or less) it'll be rendered with a circular +Links can have textual or custom SVG labels. When the label is short (two characters or less) or an SVG href, it'll be rendered with a circular background similarly to node's [side labels](Graph#on-the-side). Longer labels will have a rectangular background. -To enable links labels you'll need to provide a function to `linkLabel` returning a `GraphLinkLabel` object to display. +To enable link labels you'll need to provide a function for `linkLabel` returning a `GraphLinkLabel` object to display. + +To use custom SVG as labels, you'll first need to define it in your container [SVG defs](http://localhost:9300/docs/containers/Single_Container#svg-defs) +and provide the `href` to your custom SVG definition using the `text` property of your label. In this case the `fontSize` +property will control the size of your custom SVG label. + ({ text: i*i*i })} /> The default appearance of the labels is controlled with the following CSS variables: @@ -524,7 +529,7 @@ in the gallery to learn more. ### Precalculated If you want to specify node locations, set `layoutType` to `GraphLayoutType.Precalculated`(or `"precalculated"`). -Then pass in node positions (`x` and `y`) as part of graph data. +Then pass in node positions (`x` and `y`) as part of graph data. Note: if you selected `GraphLayoutType.Precalculated` but fail to pass in `x` and `y`, all your nodes will render at the default positions. From fd2f69d09a6d53ff993585a57e1bfa765b7e6066 Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Tue, 27 Aug 2024 12:02:03 -0700 Subject: [PATCH 04/10] Misc | Utils: Consolidate utility exports Consolidate utility exports in `packages/ts/src/index.ts` by creating a new `utils/index.ts` file that re-exports all utility modules. This simplifies the main entry point and makes it easier to discover and use the available utility functions. --- packages/ts/src/index.ts | 5 +---- packages/ts/src/utils/index.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 packages/ts/src/utils/index.ts diff --git a/packages/ts/src/index.ts b/packages/ts/src/index.ts index 761ab8fda..a36464e9d 100644 --- a/packages/ts/src/index.ts +++ b/packages/ts/src/index.ts @@ -3,7 +3,4 @@ export * from './components' export * from './types' export * from './styles/colors' export * from './styles/sizes' -export * from './utils/data' -export * from './utils/text' -export * from './utils/svg' -export * from './utils/color' +export * from './utils' diff --git a/packages/ts/src/utils/index.ts b/packages/ts/src/utils/index.ts new file mode 100644 index 000000000..e4adc31e4 --- /dev/null +++ b/packages/ts/src/utils/index.ts @@ -0,0 +1,12 @@ +export * from './data' +export * from './text' +export * from './svg' +export * from './color' +export * from './path' +export * from './misc' +export * from './type' +export * from './scale' +export * from './d3' +export * from './map' +export * from './style' +export * from './html' From 4630729edcafd16d2ea512e4d88aa1d5ecbaa231 Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Mon, 14 Oct 2024 14:39:36 -0700 Subject: [PATCH 05/10] Component | Graph: Ensure `fitView` waits for layout calculation - Ensures the `fitView` method waits for the `_layoutCalculationPromise` to resolve before calling `_fit` to prevent potential errors. --- packages/ts/src/components/graph/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ts/src/components/graph/index.ts b/packages/ts/src/components/graph/index.ts index 8317a753d..3a6137493 100644 --- a/packages/ts/src/components/graph/index.ts +++ b/packages/ts/src/components/graph/index.ts @@ -970,7 +970,7 @@ export class Graph< } public fitView (duration = this.config.duration): void { - this._layoutCalculationPromise.then(() => { + this._layoutCalculationPromise?.then(() => { this._fit(duration) }) } From ebb8646d18028e847c678ee3f9e46caf98c180b1 Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Mon, 14 Oct 2024 14:45:04 -0700 Subject: [PATCH 06/10] Component | Graph: Add zoom start and end callbacks - Adds `onZoomStart` and `onZoomEnd` callbacks to the `GraphConfigInterface` to allow users to hook into the start and end of the zoom behavior. - Implements the `_onZoomStart` and `_onZoomEnd` methods to call the respective callbacks when the zoom behavior starts and ends. --- packages/ts/src/components/graph/config.ts | 6 ++++++ packages/ts/src/components/graph/index.ts | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/packages/ts/src/components/graph/config.ts b/packages/ts/src/components/graph/config.ts index 85566c31b..9194a0218 100644 --- a/packages/ts/src/components/graph/config.ts +++ b/packages/ts/src/components/graph/config.ts @@ -240,6 +240,10 @@ export interface GraphConfigInterface, event: D3DragEvent, unknown>) => void | undefined; /** Zoom event callback. Default: `undefined` */ onZoom?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined, transform: ZoomTransform) => void; + /** Zoom start event callback. Default: `undefined` */ + onZoomStart?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined, transform: ZoomTransform) => void; + /** Zoom end event callback. Default: `undefined` */ + onZoomEnd?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined, transform: ZoomTransform) => void; /** Callback function to be called when the graph layout is calculated. Default: `undefined` */ onLayoutCalculated?: (n: GraphNode[], links: GraphLink[]) => void; /** Graph node selection brush callback function. Default: `undefined` */ @@ -351,6 +355,8 @@ export const GraphDefaultConfig: GraphConfigInterface() .scaleExtent(this.config.zoomScaleExtent) .on('zoom', (e: D3ZoomEvent) => this._onZoom(e.transform, e)) + .on('start', (e: D3ZoomEvent) => this._onZoomStart(e.transform, e)) + .on('end', (e: D3ZoomEvent) => this._onZoomEnd(e.transform, e)) this._brushBehavior = brush() .on('start brush end', this._onBrush.bind(this)) @@ -707,6 +709,20 @@ export class Graph< ) } + private _onZoomStart (t: ZoomTransform, event?: D3ZoomEvent): void { + const { config } = this + const transform = t || event.transform + this._scale = transform.k + if (isFunction(config.onZoomStart)) config.onZoomStart(this._scale, config.zoomScaleExtent, event, transform) + } + + private _onZoomEnd (t: ZoomTransform, event?: D3ZoomEvent): void { + const { config } = this + const transform = t || event.transform + this._scale = transform.k + if (isFunction(config.onZoomEnd)) config.onZoomEnd(this._scale, config.zoomScaleExtent, event, transform) + } + private _updateNodePosition (d: GraphNode, x: number, y: number): void { const transform = zoomTransform(this.g.node()) const scale = transform.k From 9ce973a569b2ad2c7b1a95524761e171186b2b73 Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Mon, 14 Oct 2024 14:46:12 -0700 Subject: [PATCH 07/10] Angular | Component | Graph: Wrapper update --- .../src/components/graph/graph.component.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/angular/src/components/graph/graph.component.ts b/packages/angular/src/components/graph/graph.component.ts index eff3b1298..d5d3714fc 100644 --- a/packages/angular/src/components/graph/graph.component.ts +++ b/packages/angular/src/components/graph/graph.component.ts @@ -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' @@ -310,9 +310,20 @@ export class VisGraphComponent, nodeGroupElementSelection: Selection, null, unknown>, config: GraphConfigInterface, 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, nodeGroupElementSelection: Selection, null, unknown>, config: GraphConfigInterface, duration: number, zoomLevel: number) => void + /** Custom "exit" function for node rendering. Default: `undefined` */ @Input() nodeExitCustomRenderFunction?: (datum: GraphNode, nodeGroupElementSelection: Selection, null, unknown>, config: GraphConfigInterface, duration: number, zoomLevel: number) => void + /** Custom render function that will be called while zooming / panning the graph. Default: `undefined` */ + @Input() nodeOnZoomCustomRenderFunction?: (datum: GraphNode, nodeGroupElementSelection: Selection, null, unknown>, config: GraphConfigInterface, zoomLevel: number) => void + /** Define the mode for highlighting selected nodes in the graph. Default: `GraphNodeSelectionHighlightMode.GreyoutNonConnected` */ @Input() nodeSelectionHighlightMode?: GraphNodeSelectionHighlightMode @@ -335,7 +346,13 @@ export class VisGraphComponent, event: D3DragEvent, unknown>) => void | undefined /** Zoom event callback. Default: `undefined` */ - @Input() onZoom?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined) => void + @Input() onZoom?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined, transform: ZoomTransform) => void + + /** Zoom start event callback. Default: `undefined` */ + @Input() onZoomStart?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined, transform: ZoomTransform) => void + + /** Zoom end event callback. Default: `undefined` */ + @Input() onZoomEnd?: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined, transform: ZoomTransform) => void /** Callback function to be called when the graph layout is calculated. Default: `undefined` */ @Input() onLayoutCalculated?: (n: GraphNode[], links: GraphLink[]) => void @@ -369,8 +386,8 @@ export class VisGraphComponent { - 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)[] keys.forEach(key => { if (config[key] === undefined) delete config[key] }) From 2a1381fe4e5db2e99256395408944a788448304a Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Tue, 15 Oct 2024 14:38:56 -0700 Subject: [PATCH 08/10] Component | Graph: Add ability to fit view to specific nodes - Adds an optional `nodeIds` parameter to the `fitView` method that allows the caller to specify which nodes to fit the view to. - Updates the `_fit` method to filter the nodes to the specified `nodeIds` if provided, or use all nodes if not. - This enables users to focus the view on a subset of nodes, rather than always fitting the entire graph. --- packages/ts/src/components/graph/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ts/src/components/graph/index.ts b/packages/ts/src/components/graph/index.ts index 8806c2417..6014cc863 100644 --- a/packages/ts/src/components/graph/index.ts +++ b/packages/ts/src/components/graph/index.ts @@ -463,9 +463,10 @@ export class Graph< } } - private _fit (duration = 0): void { + private _fit (duration = 0, nodeIds?: (string | number)[]): void { const { datamodel: { nodes } } = this - const transform = this._getTransform(nodes) + const fitViewNodes = nodeIds?.length ? nodes.filter(n => nodeIds.includes(n.id)) : nodes + const transform = this._getTransform(fitViewNodes) smartTransition(this.g, duration) .call(this._zoomBehavior.transform, transform) this._onZoom(transform) @@ -985,9 +986,9 @@ export class Graph< return zoomTransform(this.g.node()).k } - public fitView (duration = this.config.duration): void { + public fitView (duration = this.config.duration, nodeIds?: (string | number)[]): void { this._layoutCalculationPromise?.then(() => { - this._fit(duration) + this._fit(duration, nodeIds) }) } From 563d42d9ed3011619795b00b65fc416c994b9723 Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Tue, 15 Oct 2024 14:40:15 -0700 Subject: [PATCH 09/10] Dev | Examples | Graph | Custom Rendering: Zoom to specific nodes, modify the layout --- .../graph-custom-node-rendering/component.tsx | 163 ++++++++++-------- .../graph-custom-node-rendering/index.tsx | 43 ++++- .../graph-custom-node-rendering/styles.ts | 6 + 3 files changed, 134 insertions(+), 78 deletions(-) diff --git a/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/component.tsx b/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/component.tsx index eba21d0a2..1b88d86b6 100644 --- a/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/component.tsx +++ b/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/component.tsx @@ -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' @@ -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 & { +> = VisSingleContainerProps<{ links: L; nodes: N }> & +VisGraphProps & { 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 = ( - props: CustomGraphProps -): ReactElement => { - const [showLinkFlow, setShowLinkFlow] = useState(true) +// eslint-disable-next-line @typescript-eslint/naming-convention +function CustomGraphComponent ( + props: CustomGraphProps, + ref: React.Ref | null> +): ReactElement { + const graphRef = useRef>(null) const [selectedNodeId, setSelectedNodeId] = useState(undefined) const graphD3SelectionRef = useRef | null>(null) @@ -38,22 +41,28 @@ export const CustomGraph = ({ - 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, - nodes: GraphNode[], - links: GraphLink[], - config: GraphConfigInterface, - duration: number, - zoomLevel: number - ): void => { - graphD3SelectionRef.current = g - renderSwimlanes(g, nodes) - }, []) + const onRenderComplete = useCallback( + ( + g: Selection, + nodes: GraphNode[], + links: GraphLink[], + config: GraphConfigInterface, + duration: number, + zoomLevel: number + ): void => { + graphD3SelectionRef.current = g + renderSwimlanes(g, nodes) + }, + [] + ) const onZoom = useCallback((zoomLevel: number) => { if (graphD3SelectionRef.current) { @@ -61,61 +70,63 @@ export const CustomGraph = ({ - [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 ( - <> - - - 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} - /> - -
- -
- + + + 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} + /> + ) } + +export const CustomGraph = React.forwardRef(CustomGraphComponent) as ( + props: CustomGraphProps & { ref?: React.Ref> } +) => ReactElement diff --git a/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/index.tsx b/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/index.tsx index 7dd48934f..f9a284780 100644 --- a/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/index.tsx +++ b/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/index.tsx @@ -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 | null>(null) + const nodes: CustomGraphNode[] = useMemo(() => ([ { id: '0', @@ -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[], links: GraphLink[]) => { + nodes[0].x = 100 + }, []) + + + const fitView = useCallback((nodeIds?: string[]) => { + graphRef.current?.component?.fitView(undefined, nodeIds) + }, []) + return ( - + <> + showLinkFlow && l.showFlow, [showLinkFlow])} + onLayoutCalculated={onLayoutCalculated} + + /> +
+ + + +
+ ) } diff --git a/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/styles.ts b/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/styles.ts index 4feaa4206..62e7adfe6 100644 --- a/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/styles.ts +++ b/packages/dev/src/examples/networks-and-flows/graph/graph-custom-node-rendering/styles.ts @@ -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; +` From 2f56e4497439a95eb9157f374f6dfbbe6fb3e6e6 Mon Sep 17 00:00:00 2001 From: Nikita Rokotyan Date: Thu, 17 Oct 2024 15:11:02 -0700 Subject: [PATCH 10/10] Website | Docs | Graph: Updating the docs --- packages/ts/src/components/graph/config.ts | 2 +- .../website/docs/networks-and-flows/Graph.mdx | 156 +++++++++++++++++- 2 files changed, 149 insertions(+), 9 deletions(-) diff --git a/packages/ts/src/components/graph/config.ts b/packages/ts/src/components/graph/config.ts index 9194a0218..2e04c9728 100644 --- a/packages/ts/src/components/graph/config.ts +++ b/packages/ts/src/components/graph/config.ts @@ -245,7 +245,7 @@ export interface GraphConfigInterface | undefined, transform: ZoomTransform) => void; /** Callback function to be called when the graph layout is calculated. Default: `undefined` */ - onLayoutCalculated?: (n: GraphNode[], links: GraphLink[]) => void; + onLayoutCalculated?: (nodes: GraphNode[], links: GraphLink[]) => void; /** Graph node selection brush callback function. Default: `undefined` */ onNodeSelectionBrush?: (selectedNodes: GraphNode[], event: D3BrushEvent | undefined) => void; /** Graph multiple node drag callback function. Default: `undefined` */ diff --git a/packages/website/docs/networks-and-flows/Graph.mdx b/packages/website/docs/networks-and-flows/Graph.mdx index 79ccbcc90..68a0d86e7 100644 --- a/packages/website/docs/networks-and-flows/Graph.mdx +++ b/packages/website/docs/networks-and-flows/Graph.mdx @@ -103,7 +103,7 @@ zoom level at initialization to fit to the size of the container. nodeSize={50} /> -### Custom SVG Nodes +### Custom SVG Shapes Alternatively, you can provide `nodeShape` property with custom SVGs to get the exact shape you want. You can either provide it directly as a string in your _StringAccessor_ or for better control over the element, put the shape(s) definition in the container's `svgDefs` property and reference it with @@ -287,6 +287,59 @@ The disabled state appearance can be redefined with these CSS variables: --vis-dark-graph-node-side-label-background-greyout-color: #f1f4f7; ``` +### Custom Rendering `1.5.0` +The _Graph_ component offers extensive customization options for node rendering, allowing you to define how +nodes are displayed at various stages of their lifecycle — such as on entering, updating, zooming, and exiting. +You can inject custom rendering functions using the following configuration properties: + +- `nodeEnterCustomRenderFunction`: Customize the node rendering when a node enters the DOM. +- `nodeUpdateCustomRenderFunction`: Define the rendering when a node updates its position or properties. +- `nodePartialUpdateCustomRenderFunction`: Partially update nodes on specific interactions like mouseover, background click, and brushing. +- `nodeExitCustomRenderFunction`: Customize how nodes are rendered when they exit the DOM. +- `nodeOnZoomCustomRenderFunction`: Adjust node rendering dynamically during zooming or panning. + +These functions provide access to each node’s data (`datum`), the node’s DOM element selection +(`g`), the component configuration (`config`), and the current zoom level (`zoomLevel`). +This gives you full control to modify elements such as SVG shapes, colors, labels, icons, and more. + +```ts +import { select, Selection } from 'd3-selection' +import { GraphNode, GraphConfigInterface } from '@unovis/ts' + +export const nodeEnterCustomRenderFunction = ( + datum: GraphNode, + g: Selection, + config: GraphConfigInterface +) => { + // Initial rendering logic for the node + g.append('circle') + + // Add custom icons, labels, or any additional elements + g.append('text') + .attr('dy', -10) + .attr('text-anchor', 'middle') +} + +export const nodeUpdateCustomRenderFunction = ( + datum: GraphNode, + g: Selection, + config: GraphConfigInterface +) => { + // Update the node's size, color + g.select('circle') + .attr('r', config.nodeSize ?? 20) + .style('fill', config.nodeFill ?? 'steelblue') + + // Update labels or any custom elements based on node data + g.select('text') + .text(datum.id) +} +``` + +Using these functions, you can create highly customized and dynamic node appearances, adapting the visual +representation based on data or user interaction. These functions are invoked for each node in the graph, +providing flexibility for various use cases, from static iconography to interactive, animated elements. + ## Links ### Color, Width and Type Set link color and width with the `linkStroke` and `linkWidth` properties. The default link color can be set with the @@ -308,12 +361,12 @@ Providing an accessor function to `linkArrow` will turn on arrows display on lin `GraphLinkArrowStyle.Single` (`"single"` or simply `true`) or `GraphLinkArrowStyle.Double` (`"double"`) or `null`. sample([undefined, 'single', 'double'])} /> -### Labels +### Labels `Updated in 1.5.0` Links can have textual or custom SVG labels. When the label is short (two characters or less) or an SVG href, it'll be rendered with a circular background similarly to node's [side labels](Graph#on-the-side). Longer labels will have a rectangular background. To enable link labels you'll need to provide a function for `linkLabel` returning a `GraphLinkLabel` object to display. -To use custom SVG as labels, you'll first need to define it in your container [SVG defs](http://localhost:9300/docs/containers/Single_Container#svg-defs) +To use custom SVG as labels (available in _Unovis_ 1.5.0), you'll first need to define it in your container [SVG defs](http://localhost:9300/docs/containers/Single_Container#svg-defs) and provide the `href` to your custom SVG definition using the `text` property of your label. In this case the `fontSize` property will control the size of your custom SVG label. @@ -452,7 +505,7 @@ object to `dagreLayoutSettings`. }} /> -### ELK `New in 1.1.0` +### ELK Starting from Unovis version 1.1.0 _Graph_ supports [The Eclipse Layout Kernel](https://www.eclipse.org/elk/) which has [several layout algorithms](https://www.eclipse.org/elk/reference/algorithms.html) available. You can enable ELK by setting `layoutType` to `GraphLayoutType.Elk` (or `"elk"`) and providing the layout configuration via @@ -536,6 +589,30 @@ Note: if you selected `GraphLayoutType.Precalculated` but fail to pass in `x` an ### Non-connected nodes aside If you want non-connected graph nodes to be placed below the layout, set `layoutNonConnectedAside` to `true`. +### Post-Layout Customization `1.5.0` +The `Graph` component includes a `onLayoutCalculated` callback, which provides an opportunity to adjust +the positions or properties of nodes and links after the layout has been calculated. This can be +useful if you need to make final tweaks or apply additional logic once the layout is determined. + +This callback function is triggered with the calculated node and link arrays, allowing you to +inspect and modify their properties directly. For example, you can use this callback to enforce +specific positioning constraints or adjust node/link styles. + +Here’s a basic example that demonstrates how to use the `onLayoutCalculated` callback to adjust node positions: + +```ts +const onLayoutCalculated = (nodes: GraphNode[], links: GraphLink[]) => { + // Modify nodes based on custom criteria + nodes.forEach(node => { + if (node.group === 'special') { + // Set specific positions or styles for nodes in the 'special' group + node.x += 50; + node.y -= 30; + } + }); +}; +``` + ## Fitting the graph into container _Graph_ automatically fits the layout to the container size on every config or data update. However, when the user has moved or zoomed the graph there's some level of tolerance after which automatic fitting will be disabled. The tolerance @@ -709,9 +786,45 @@ public fitView () If you use React or Angular, you can access the component instance for calling these methods by using [`useRef`](https://react.dev/reference/react/useRef) or [`ViewChild`](https://angular.io/api/core/ViewChild) respectively. +### Callbacks +The _Graph_ component supports a comprehensive set of interaction callbacks, giving you control +over node dragging, zooming/panning. These callbacks allow you to add +custom behavior and responses during user interactions, enhancing the interactivity and +responsiveness of the graph. + +#### Node Dragging Callbacks +You can define custom actions for node dragging with the following callbacks: + +- `onNodeDragStart`: Triggered when a node drag starts. +- `onNodeDrag`: Called continuously as a node is being dragged. +- `onNodeDragEnd`: Invoked when a node drag operation ends. + +Each of these callbacks receives the node data and the drag event, allowing for actions such +as updating other elements based on the dragged node’s position or applying visual effects +during the drag. + +```ts +onNodeDragStart: (n: GraphNode, event: D3DragEvent, unknown>) => void | undefined; +onNodeDrag: (n: GraphNode, event: D3DragEvent, unknown>) => void | undefined; +onNodeDragEnd: (n: GraphNode, event: D3DragEvent, unknown>) => void | undefined; +``` + +#### Zoom and Pan Callbacks + +For handling zoom and pan interactions, you can use these callbacks: + +- `onZoomStart`: Fires when a zoom or pan operation begins. +- `onZoom`: Triggered continuously during zooming or panning, providing the current +zoom scale and transform details. +- `onZoomEnd`: Called when zooming or panning ends. + +These callbacks allow you to dynamically adjust graph elements, update UI components, or log +zoom and pan activities. ```ts -onZoom: (zoomScale: number, zoomScaleExtent: number) => void; +onZoom: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined, transform: ZoomTransform) => void; +onZoomStart: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined, transform: ZoomTransform) => void; +onZoomEnd: (zoomScale: number, zoomScaleExtent: [number, number], event: D3ZoomEvent | undefined, transform: ZoomTransform) => void; ``` ## Multiple Node Drag @@ -732,11 +845,38 @@ The appearance of the "brushed" nodes can be customized with the following CSS v --vis-graph-brushed-node-icon-fill-color; ``` -Additionally, there are two callback functions in Graph's configuration that can be used to see which nodes are being brushed and dragged. +### Callbacks +Additionally, there are two callback functions in _Graph_'s configuration that can be used to see which nodes are being brushed and dragged: + +- `onNodeSelectionBrush`: Fires when nodes are selected using the brushing tool. It provides an array of selected nodes and the brush event, making it ideal for grouping or highlighting nodes. +- `onNodeSelectionDrag`: Called during a multi-node drag operation, enabling you to manage grouped node movements or apply custom interactions. + + +```ts +onNodeSelectionBrush: (selectedNodes: GraphNode[], event: D3BrushEvent | undefined) => void; +onNodeSelectionDrag: (selectedNodes: GraphNode[], event: D3DragEvent, unknown>) => void; +``` + +## Post-Render Customization `1.5.0` +The _Graph_ component provides an `onRenderComplete` callback function that allows you to add custom +elements to the graph’s canvas after the rendering is fully complete. This function is especially +useful for layering additional elements or annotations on top of the existing nodes and links. + +This callback receives several parameters, including the canvas selection (`g`),arrays of nodes and +links, and configuration details. With this access, you can append, update, or transform elements +as needed to enhance the visual output. ```ts -onNodeSelectionBrush (selectedNodes: GraphNode[], event: D3BrushEvent) void; -onNodeSelectionDrag (selectedNodes: GraphNode[], event: D3BrushEvent) void; +onRenderComplete ( + g: Selection, + nodes: GraphNode[], + links: GraphLink[], + config: GraphConfigInterface, + duration: number, + zoomLevel: number, + width: number, + height: number +) => void; ``` ## Events