8000 Component | Graph: More Link Particle Flow and Fit View controls by rokotyan · Pull Request #25 · ExaForce/unovis · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Component | Graph: More Link Particle Flow and Fit View controls #25

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

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'
import type { GraphLink, GraphNode } from '@unovis/ts'
import { GraphFitViewAlignment, GraphLink, GraphNode } from '@unovis/ts'
import type { VisGraphRef } from '@unovis/react'

import { CustomGraph } from './component'
Expand All @@ -13,6 +13,7 @@ export const subTitle = 'User provided rendering functions'

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

const nodes: CustomGraphNode[] = useMemo(() => ([
Expand All @@ -35,11 +36,11 @@ export const component = (): JSX.Element => {
]), [])

const links: CustomGraphLink[] = useMemo(() => ([
{ source: '0', target: '1', showFlow: true },
{ source: '0', target: '2', showFlow: true },
{ source: '0', target: '3', showFlow: true },
{ source: '0', target: '4', showFlow: true },
{ source: '1', target: '5', showFlow: true },
{ source: '0', target: '1', showFlow: true, linkFlowParticleSize: 1.5, linkFlowParticleSpeed: 15 },
{ source: '0', target: '2', showFlow: true, linkFlowParticleSize: 2, linkFlowParticleSpeed: 25 },
{ source: '0', target: '3', showFlow: true, linkFlowParticleSize: 3, linkFlowParticleSpeed: 10 },
{ source: '0', target: '4', showFlow: true, linkFlowParticleSize: 3, linkFlowParticleSpeed: 30 },
{ source: '1', target: '5', showFlow: true, linkFlowParticleSize: 2.5, linkFlowParticleSpeed: 20 },
]), [])

// Modifying layout after the calculation
Expand All @@ -61,7 +62,13 @@ export const component = (): JSX.Element => {
height={'100vh'}
linkFlow={useCallback((l: CustomGraphLink) => showLinkFlow && l.showFlow, [showLinkFlow])}
>

linkFlowAnimDuration={useCallback((l: CustomGraphLink) => l.linkFlowAnimDuration, [])}
linkFlowParticleSpeed={useCallback((l: CustomGraphLink) => l.linkFlowParticleSpeed, [])}
linkFlowParticleSize={useCallback((l: CustomGraphLink) => l.linkFlowParticleSize, [])}
linkWidth={0}
linkBandWidth={useCallback((l: CustomGraphLink) => 2 * (l.linkFlowParticleSize ?? 1), [])}
fitViewAlign={fitViewAlignment}
fitViewPadding={useMemo(() => ({ top: 50, right: 50, bottom: 100, left: 50 }), [])}
/>
<div className={s.checkboxContainer}>
<label>
Expand All @@ -72,6 +79,17 @@ export const component = (): JSX.Element => {
/>
Show Link Flow
</label>
<select
value={fitViewAlignment}
=> setFitViewAlignment(e.target.value as GraphFitViewAlignment)}
className={s.graphButton}
>
{Object.values(GraphFitViewAlignment).map((alignment) => (
<option key={alignment} value={alignment}>
{alignment.charAt(0).toUpperCase() + alignment.slice(1)}
</option>
))}
</select>
<button className={s.graphButton} => fitView(['0', '1', '2', '3'])}>Zoom To Identity and Network Nodes</button>
<button className={s.graphButton} => fitView()}>Fit Graph</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export type CustomGraphLink<Datum = unknown> = {
showArrow?: boolean;
showFlow?: boolean;
width?: number;
linkFlowAnimDuration?: number;
linkFlowParticleSize?: number;
linkFlowParticleSpeed?: number;
};

export type CustomGraphSwimlane = {
Expand Down
19 changes: 15 additions & 4 deletions packages/ts/src/components/graph/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ComponentConfigInterface, ComponentDefaultConfig } from 'core/component

// Types
import { TrimMode } from 'types/text'
import { Spacing } from 'types/spacing'
import { GraphInputLink, GraphInputNode, GraphInputData } from 'types/graph'
import { BooleanAccessor, ColorAccessor, NumericAccessor, StringAccessor, GenericAccessor } from 'types/accessor'

Expand All @@ -32,9 +33,9 @@ import {
GraphNode,
GraphLink,
GraphNodeSelectionHighlightMode,
GraphFitViewAlignment,
} from './types'


export interface GraphConfigInterface<N extends GraphInputNode, L extends GraphInputLink> extends ComponentConfigInterface {
// Zoom and drag
/** Zoom level constraints. Default: [0.35, 1.25] */
Expand All @@ -51,6 +52,10 @@ export interface GraphConfigInterface<N extends GraphInputNode, L extends GraphI
disableBrush?: boolean;
/** Interval to re-render the graph when zooming. Default: `100` */
zoomThrottledUpdateNodeThreshold?: number;
/** Padding for the graph when fitting to container. Default: `50` */
fitViewPadding?: Spacing | number;
/** Default alignment when fitting the graph view. Default: `GraphFitViewAlignment.Center` */
fitViewAlign?: GraphFitViewAlignment;

// Layout general settings
/** Type of the graph layout. Default: `GraphLayoutType.Force` */
Expand Down Expand Up @@ -142,10 +147,13 @@ export interface GraphConfigInterface<N extends GraphInputNode, L extends GraphI
linkDisabled?: BooleanAccessor<L>;
/** Link flow animation accessor function or constant value. Default: `false` */
linkFlow?: BooleanAccessor<L>;
/** Animation duration of the flow (traffic) circles. Default: `20000` */
linkFlowAnimDuration?: number;
/** Animation duration of the flow (traffic) circles in milliseconds. If `linkFlowParticleSpeed` is provided,
* this duration will be calculated based on the link length and particle speed. Default: `20000` */
linkFlowAnimDuration?: NumericAccessor<L>;
/** Size of the moving particles that represent traffic flow. Default: `2` */
linkFlowParticleSize?: number;
linkFlowParticleSize?: NumericAccessor<L>;
/** Speed of the moving particles in pixels per second. This property takes precedence over `linkFlowAnimDuration`. Default: `undefined` */
linkFlowParticleSpeed?: NumericAccessor<L>;
/** Link label accessor function or constant value. Default: `undefined` */
linkLabel?: GenericAccessor<GraphCircleLabel | GraphCircleLabel[], L> | undefined;
/** Shift label along the link center a little bit to avoid overlap with the link arrow. Default: `true` */
Expand Down Expand Up @@ -307,6 +315,8 @@ export const GraphDefaultConfig: GraphConfigInterface<GraphInputNode, GraphInput
layoutAutofit: true,
layoutAutofitTolerance: 8.0,
layoutNonConnectedAside: false,
fitViewPadding: 50,
fitViewAlign: GraphFitViewAlignment.Center,

layoutGroupOrder: [],
layoutParallelSubGroupsPerRow: 1,
Expand Down Expand Up @@ -337,6 +347,7 @@ export const GraphDefaultConfig: GraphConfigInterface<GraphInputNode, GraphInput

linkFlowAnimDuration: 20000,
linkFlowParticleSize: 2,
linkFlowParticleSpeed: undefined,
linkWidth: 1,
linkStyle: GraphLinkStyle.Solid,
linkBandWidth: 0,
Expand Down
83 changes: 62 additions & 21 deletions packages/ts/src/components/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@ import { GraphInputLink, GraphInputNode, GraphInputData } from 'types/graph'
import { Spacing } from 'types/spacing'

6DB6 // Utils
import { isNumber, clamp, shallowDiff, isFunction, getBoolean, isPlainObject, isEqual } from 'utils/data'
import { isNumber, clamp, shallowDiff, isFunction, getBoolean, isPlainObject, isEqual, getNumber } from 'utils/data'
import { smartTransition } from 'utils/d3'

// Local Types
import { GraphNode, GraphLink, GraphLayoutType, GraphLinkArrowStyle, GraphPanel, GraphNodeSelectionHighlightMode } from './types'
import {
GraphNode,
GraphLink,
GraphLayoutType,
GraphLinkArrowStyle,
GraphPanel,
GraphNodeSelectionHighlightMode,
GraphFitViewAlignment,
} from './types'

// Config
import { GraphDefaultConfig, GraphConfigInterface } from './config'
Expand Down Expand Up @@ -203,8 +211,10 @@ export class Graph<
}

get bleed (): Spacing {
const extraPadding = 50 // Extra padding to take into account labels and selection outlines
return { top: extraPadding, bottom: extraPadding, left: extraPadding, right: extraPadding }
const padding = this.config.fitViewPadding // Extra padding to take into account labels and selection outlines
return isNumber(padding)
? { top: padding, bottom: padding, left: padding, right: padding }
: padding
}

_render (customDuration?: number): void {
Expand Down Expand Up @@ -469,16 +479,16 @@ export class Graph<
}
}

private _fit (duration = 0, nodeIds?: (string | number)[]): void {
private _fit (duration = 0, nodeIds?: (string | number)[], alignment = this.config.fitViewAlign): void {
const { datamodel: { nodes } } = this
const fitViewNodes = nodeIds?.length ? nodes.filter(n => nodeIds.includes(n.id)) : nodes
const transform = this._getTransform(fitViewNodes)
const transform = this._getTransform(fitViewNodes, alignment)
smartTransition(this.g, duration)
.call(this._zoomBehavior.transform, transform)
this._onZoom(transform)
}

private _getTransform (nodes: GraphNode<N, L>[]): ZoomTransform {
private _getTransform (nodes: GraphNode<N, L>[], alignment: GraphFitViewAlignment): ZoomTransform {
const { nodeSize, zoomScaleExtent } = this.config
const { left, top, right, bottom } = this.bleed

Expand All @@ -500,23 +510,45 @@ export class Graph<
return zoomIdentity
}

const xScale = w / (xExtent[1] - xExtent[0] + left + right)
const yScale = h / (yExtent[1] - yExtent[0] + top + bottom)
const xScale = w / (xExtent[1] - xExtent[0] + (left || 0) + (right || 0))
const yScale = h / (yExtent[1] - yExtent[0] + (top || 0) + (bottom || 0))

const clampedScale = clamp(min([xScale, yScale]), zoomScaleExtent[0], zoomScaleExtent[1])

const xCenter = (xExtent[1] + xExtent[0]) / 2
const yCenter = (yExtent[1] + yExtent[0]) / 2
const translateX = this._width / 2 - xCenter * clampedScale
const translateY = this._height / 2 - yCenter * clampedScale
// Calculate translation based on alignment
let translateX: number
let translateY: number

switch (alignment) {
case GraphFitViewAlignment.Left:
translateX = left - xExtent[0] * clampedScale
translateY = this._height / 2 - (yExtent[0] + (yExtent[1] - yExtent[0]) / 2) * clampedScale
break
case GraphFitViewAlignment.Right:
translateX = this._width - (xExtent[1] - xExtent[0]) * clampedScale - right
translateY = this._height / 2 - (yExtent[0] + (yExtent[1] - yExtent[0]) / 2) * clampedScale
break
case GraphFitViewAlignment.Top:
translateX = this._width / 2 - (xExtent[0] + (xExtent[1] - xExtent[0]) / 2) * clampedScale
translateY = top - yExtent[0] * clampedScale
break
case GraphFitViewAlignment.Bottom:
translateX = this._width / 2 - (xExtent[0] + (xExtent[1] - xExtent[0]) / 2) * clampedScale
translateY = this._height - (yExtent[1] - yExtent[0]) * clampedScale - bottom
break
case GraphFitViewAlignment.Center:
default:
translateX = this._width / 2 - (xExtent[0] + (xExtent[1] - xExtent[0]) / 2) * clampedScale
translateY = this._height / 2 - (yExtent[0] + (yExtent[1] - yExtent[0]) / 2) * clampedScale
}

const transform = zoomIdentity
.translate(translateX, translateY)
.scale(clampedScale)

return transform
}


private _setNodeSelectionState (nodesToSelect: (GraphNode<N, L> | undefined)[]): void {
const { config, datamodel } = this

Expand Down Expand Up @@ -661,16 +693,25 @@ export class Graph<
}

private _onLinkFlowTimerFrame (elapsed = 0): void {
const { config: { linkFlow, linkFlowAnimDuration }, datamodel: { links } } = this
const { config, datamodel: { links } } = this

const hasLinksWithFlow = links.some((d, i) => getBoolean(d, linkFlow, i))
const hasLinksWithFlow = links.some((d, i) => getBoolean(d, config.linkFlow, i))
if (!hasLinksWithFlow) return

const t = (elapsed % linkFlowAnimDuration) / linkFlowAnimDuration
const linkElements = this._linksGroup.selectAll<SVGGElement, GraphLink<N, L>>(`.${linkSelectors.gLink}`)

const linksToAnimate = linkElements.filter(d => !d._state.greyout)
linksToAnimate.each(d => { d._state.flowAnimTime = t })
linksToAnimate.each((l, i, els) => {
let linkFlowAnimDuration = getNumber(l, config.linkFlowAnimDuration, l._indexGlobal)
const linkFlowParticleSpeed = getNumber(l, config.linkFlowParticleSpeed, l._indexGlobal)

// If particle speed is provided, calculate duration based on link length and speed
if (linkFlowParticleSpeed) {
const linkPathElement = els[i].querySelector<SVGPathElement>(`.${linkSelectors.linkSupport}`)
const pathLength = linkPathElement ? (this._linkPathLengthMap.get(linkPathElement.getAttribute('d')) ?? linkPathElement.getTotalLength()) : 0
if (pathLength > 0) linkFlowAnimDuration = (pathLength / linkFlowParticleSpeed) * 1000 // Convert to milliseconds
}
l._state.flowAnimTime = (elapsed % linkFlowAnimDuration) / linkFlowAnimDuration
})
animateLinkFlow(linksToAnimate, this.config, this._scale, this._linkPathLengthMap)
}

Expand Down Expand Up @@ -992,9 +1033,9 @@ export class Graph<
return zoomTransform(this.g.node()).k
}

public fitView (duration = this.config.duration, nodeIds?: (string | number)[]): void {
public fitView (duration = this.config.duration, nodeIds?: (string | number)[], alignment?: GraphFitViewAlignment): void {
this._layoutCalculationPromise?.then(() => {
this._fit(duration, nodeIds)
this._fit(duration, nodeIds, alignment)
})
}

Expand Down
15 changes: 8 additions & 7 deletions packages/ts/src/components/graph/modules/link/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export function updateLinks<N extends GraphInputNode, L extends GraphInputLink>
getLinkArrowDefId: (arrow: GraphLinkArrowStyle | undefined) => string,
linkPathLengthMap: Map<string, number>
): void {
const { linkFlowParticleSize, linkStyle, linkFlow, linkLabel, linkLabelShiftFromCenter } = config
const { linkStyle, linkFlow, linkLabel, linkLabelShiftFromCenter } = config
if (!selection.size()) return

selection
Expand All @@ -204,6 +204,7 @@ export function updateLinks<N extends GraphInputNode, L extends GraphInputLink>
const linkLabelData = ensureArray(
getValue&l 10000 t;GraphLink<N, L>, GraphCircleLabel | GraphCircleLabel[]>(d, linkLabel, d._indexGlobal)
)
const linkFlowParticleSize = getNumber(d, config.linkFlowParticleSize, d._indexGlobal)

// Particle Flow
flowGroup
Expand All @@ -212,7 +213,7 @@ export function updateLinks<N extends GraphInputNode, L extends GraphInputLink>

flowGroup
.selectAll(`.${linkSelectors.flowCircle}`)
.attr('r', linkFlowParticleSize / scale)
.attr('r', linkFlowParticleSize / Math.sqrt(scale))
.style('fill', linkColor)

smartTransition(flowGroup, duration)
Expand Down Expand Up @@ -365,7 +366,7 @@ export function animateLinkFlow<N extends GraphInputNode, L extends GraphInputLi
const linkGroup = select(element)
const flowGroup = linkGroup.select(`.${linkSelectors.flowGroup}`)

const linkPathElement = linkGroup.select<SVGPathElement>(`.${linkSelectors.linkSupport}`).node()
const linkPathElement = linkGroup.select<SVGPathElement>(`.${linkSelectors.link}`).node()
const cachedLinkPathLength = linkPathLengthMap.get(linkPathElement.getAttribute('d'))
const pathLength = cachedLinkPathLength ?? linkPathElement.getTotalLength()

Expand All @@ -387,15 +388,15 @@ export function zoomLinks<N extends GraphInputNode, L extends GraphInputLink> (
config: GraphConfigInterface<N, L>,
scale: number
): void {
const { linkFlowParticleSize } = config

selection.classed(generalSelectors.zoomOutLevel2, scale < ZoomLevel.Level2)

selection.select(`.${linkSelectors.flowGroup}`)
.style('opacity', scale < ZoomLevel.Level2 ? 0 : 1)

selection.selectAll(`.${linkSelectors.flowCircle}`)
.attr('r', linkFlowParticleSize / scale)
selection.each((l, i, els) => {
const r = getNumber(l, config.linkFlowParticleSize, l._indexGlobal) / Math.sqrt(scale)
select(els[i]).selectAll(`.${linkSelectors.flowCircle}`).attr('r', r)
})

const linkElements = selection.selectAll<SVGGElement, GraphLink<N, L>>(`.${linkSelectors.link}`)
linkElements
Expand Down
5 changes: 4 additions & 1 deletion packages/ts/src/components/graph/modules/link/style.ts
B680
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ export const variables = injectGlobal`

--vis-graph-link-band-opacity: 0.35;
--vis-graph-link-support-stroke-width: 10px;
--vis-graph-link-flow-opacity: 1;

--vis-dark-graph-link-stroke-color: #494b56;
--vis-dark-graph-link-label-background: #3f3f45;
--vis-dark-graph-link-label-text-color: var(--vis-graph-link-label-text-color-bright);


--vis-graph-link-dominant-baseline: middle;
}

Expand Down Expand Up @@ -95,14 +97,15 @@ export const linkBand = css`

export const flowGroup = css`
label: flow-group;

pointer-events: none;
`

export const flowCircle = css`
label: flow-circle;

fill: var(--vis-graph-link-stroke-color);
opacity: var(--vis-graph-link-flow-opacity);
`

export const linkLabelGroup = css`
Expand Down
8 changes: 8 additions & 0 deletions packages/ts/src/components/graph/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,11 @@ export enum GraphNodeSelectionHighlightMode {
Greyout ='greyout',
GreyoutNonConnected ='greyout-non-connected',
}

export enum GraphFitViewAlignment {
Center = 'center',
Top = 'top',
Bottom = 'bottom',
Left = 'left',
Right = 'right',
}
0