Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions ai/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ wrapper is generated from it.
8. **Use the repo's type-check helpers.** For runtime type checks reach for the helpers in
`@/utils/data` (`isString`, `isNumber`, `isFunction`, `isArray`, `isObject`, `isPlainObject`)
instead of hand-writing `typeof x === 'string'`, `typeof x === 'function'`, or `Array.isArray(…)`.
9. **Optimize screenshots before committing.** Run new or changed PNG screenshots and gallery
previews through a lossless PNG optimizer (for example, `oxipng`). Preserve their dimensions,
transparency, and appearance, and visually verify the optimized images before staging them.
9. **Optimize repository image assets before committing.** Run new or changed PNG assets, such as
gallery previews and test snapshots, through a lossless PNG optimizer (for example, `oxipng`).
Preserve their dimensions, transparency, and appearance, and visually verify them before staging.

10. **Prefer direct imports.** Import individual components, enums, utilities, and types from their
defining modules instead of package root barrels. This reduces bundler/test dependency traversal,
Expand Down Expand Up @@ -108,6 +108,9 @@ Full rules, the parser regex, the type-picking cheatsheet, and ~25 real examples
- Branch from `main` (don't commit to `main` directly). The **F5 CLA bot** comments on first-time
contributions; there is no DCO/sign-off requirement.
- PR body uses a checklist (Dev examples / Wrappers / Gallery example / Docs) with screenshots.
- **Never commit screenshots or recordings intended for PR review.** Keep them outside the repository
and upload media with `gh pr create --attach <file>` or `gh pr edit <number> --attach <file>`.
Repeat `--attach` for multiple files; use the uploaded attachments in the PR body.
- Immediately before PR submission, run `pnpm cmds-report` from the repo root after final changes
and review the report. Fix failures introduced by the PR; document remaining failures and skipped
checks in the PR description. The runner has exclusions, so also complete the required CI checks.
Expand Down
10 changes: 8 additions & 2 deletions packages/angular/src/components/graph/graph.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from '@unovis/ts/components/graph/types'
import { StringAccessor, GenericAccessor, NumericAccessor, ColorAccessor, BooleanAccessor } from '@unovis/ts/types/accessor'
import { ElkShape } from 'elkjs'
import type { CurveFunction } from '@unovis/ts/types/curve'
import { TrimMode, FitMode } from '@unovis/ts/types/text'
import { Selection } from 'd3-selection'
import { D3DragEvent } from 'd3-drag'
Expand Down Expand Up @@ -248,6 +249,11 @@ export class VisGraphComponent<N extends GraphInputNode, L extends GraphInputLin
* Default: `0` */
@Input() linkCurvature?: NumericAccessor<L>

/** D3 curve factory used to draw links (e.g. `curveBasis` or `curveSmoothStep`). Receives the source,
* two control points shaped by `linkCurvature`, and the target. `curveSmoothStep` uses only the endpoints.
* Default: `undefined` (the existing cubic Bezier path shaped by `linkCurvature`) */
@Input() linkCurve?: CurveFunction

/** Highlight links on hover. Default: `true` */
@Input() linkHighlightOnHover?: boolean

Expand Down Expand Up @@ -451,8 +457,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, fitViewPadding, fitViewAlign, layoutType, layoutAutofit, layoutAutofitTolerance, layoutNonConnectedAside, layoutNodeGroup, layoutGroupOrder, layoutParallelNodesPerColumn, layoutParallelNodeSubGroup, layoutParallelSubGroupsPerRow, layoutParallelNodeSpacing, layoutParallelSubGroupSpacing, layoutParallelGroupSpacing, layoutParallelSortConnectionsByGroup, forceLayoutSettings, dagreLayoutSettings, layoutElkSettings, layoutElkNodeGroups, layoutElkGetNodeShape, linkWidth, linkStyle, linkBandWidth, linkArrow, linkStroke, linkDisabled, linkFlow, linkFlowAnimDuration, linkFlowParticleSize, linkFlowParticleSpeed, linkLabel, linkLabelShiftFromCenter, linkNeighborSpacing, linkCurvature, linkHighlightOnHover, linkSourcePointOffset, linkTargetPointOffset, selectedLinkId, nodeSize, nodeStrokeWidth, nodeShape, nodeGaugeValue, nodeGaugeFill, nodeGaugeAnimDuration, nodeIcon, nodeIconSize, nodeLabel, nodeLabelTrim, nodeLabelTrimMode, nodeLabelTrimLength, nodeLabelFitMode, nodeLabelWidth, nodeLabelSeparator, nodeLabelForceWordBreak, nodeSubLabel, nodeSubLabelTrim, nodeSubLabelTrimMode, nodeSubLabelTrimLength, nodeSubLabelFitMode, nodeSubLabelWidth, nodeSubLabelSeparator, nodeSubLabelForceWordBreak, 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, shouldDataUpdate } = this
const config = { duration, events, attributes, zoomScaleExtent, disableZoom, zoomEventFilter, disableDrag, disableBrush, zoomThrottledUpdateNodeThreshold, fitViewPadding, fitViewAlign, layoutType, layoutAutofit, layoutAutofitTolerance, layoutNonConnectedAside, layoutNodeGroup, layoutGroupOrder, layoutParallelNodesPerColumn, layoutParallelNodeSubGroup, layoutParallelSubGroupsPerRow, layoutParallelNodeSpacing, layoutParallelSubGroupSpacing, layoutParallelGroupSpacing, layoutParallelSortConnectionsByGroup, forceLayoutSettings, dagreLayoutSettings, layoutElkSettings, layoutElkNodeGroups, layoutElkGetNodeShape, linkWidth, linkStyle, linkBandWidth, linkArrow, linkStroke, linkDisabled, linkFlow, linkFlowAnimDuration, linkFlowParticleSize, linkFlowParticleSpeed, linkLabel, linkLabelShiftFromCenter, linkNeighborSpacing, linkCurvature, linkHighlightOnHover, linkSourcePointOffset, linkTargetPointOffset, selectedLinkId, nodeSize, nodeStrokeWidth, nodeShape, nodeGaugeValue, nodeGaugeFill, nodeGaugeAnimDuration, nodeIcon, nodeIconSize, nodeLabel, nodeLabelTrim, nodeLabelTrimMode, nodeLabelTrimLength, nodeLabelFitMode, nodeLabelWidth, nodeLabelSeparator, nodeLabelForceWordBreak, nodeSubLabel, nodeSubLabelTrim, nodeSubLabelTrimMode, nodeSubLabelTrimLength, nodeSubLabelFitMode, nodeSubLabelWidth, nodeSubLabelSeparator, nodeSubLabelForceWordBreak, 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, shouldDataUpdate }
const { duration, events, attributes, zoomScaleExtent, disableZoom, zoomEventFilter, disableDrag, disableBrush, zoomThrottledUpdateNodeThreshold, fitViewPadding, fitViewAlign, layoutType, layoutAutofit, layoutAutofitTolerance, layoutNonConnectedAside, layoutNodeGroup, layoutGroupOrder, layoutParallelNodesPerColumn, layoutParallelNodeSubGroup, layoutParallelSubGroupsPerRow, layoutParallelNodeSpacing, layoutParallelSubGroupSpacing, layoutParallelGroupSpacing, layoutParallelSortConnectionsByGroup, forceLayoutSettings, dagreLayoutSettings, layoutElkSettings, layoutElkNodeGroups, layoutElkGetNodeShape, linkWidth, linkStyle, linkBandWidth, linkArrow, linkStroke, linkDisabled, linkFlow, linkFlowAnimDuration, linkFlowParticleSize, linkFlowParticleSpeed, linkLabel, linkLabelShiftFromCenter, linkNeighborSpacing, linkCurvature, linkCurve, linkHighlightOnHover, linkSourcePointOffset, linkTargetPointOffset, selectedLinkId, nodeSize, nodeStrokeWidth, nodeShape, nodeGaugeValue, nodeGaugeFill, nodeGaugeAnimDuration, nodeIcon, nodeIconSize, nodeLabel, nodeLabelTrim, nodeLabelTrimMode, nodeLabelTrimLength, nodeLabelFitMode, nodeLabelWidth, nodeLabelSeparator, nodeLabelForceWordBreak, nodeSubLabel, nodeSubLabelTrim, nodeSubLabelTrimMode, nodeSubLabelTrimLength, nodeSubLabelFitMode, nodeSubLabelWidth, nodeSubLabelSeparator, nodeSubLabelForceWordBreak, 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, shouldDataUpdate } = this
const config = { duration, events, attributes, zoomScaleExtent, disableZoom, zoomEventFilter, disableDrag, disableBrush, zoomThrottledUpdateNodeThreshold, fitViewPadding, fitViewAlign, layoutType, layoutAutofit, layoutAutofitTolerance, layoutNonConnectedAside, layoutNodeGroup, layoutGroupOrder, layoutParallelNodesPerColumn, layoutParallelNodeSubGroup, layoutParallelSubGroupsPerRow, layoutParallelNodeSpacing, layoutParallelSubGroupSpacing, layoutParallelGroupSpacing, layoutParallelSortConnectionsByGroup, forceLayoutSettings, dagreLayoutSettings, layoutElkSettings, layoutElkNodeGroups, layoutElkGetNodeShape, linkWidth, linkStyle, linkBandWidth, linkArrow, linkStroke, linkDisabled, linkFlow, linkFlowAnimDuration, linkFlowParticleSize, linkFlowParticleSpeed, linkLabel, linkLabelShiftFromCenter, linkNeighborSpacing, linkCurvature, linkCurve, linkHighlightOnHover, linkSourcePointOffset, linkTargetPointOffset, selectedLinkId, nodeSize, nodeStrokeWidth, nodeShape, nodeGaugeValue, nodeGaugeFill, nodeGaugeAnimDuration, nodeIcon, nodeIconSize, nodeLabel, nodeLabelTrim, nodeLabelTrimMode, nodeLabelTrimLength, nodeLabelFitMode, nodeLabelWidth, nodeLabelSeparator, nodeLabelForceWordBreak, nodeSubLabel, nodeSubLabelTrim, nodeSubLabelTrimMode, nodeSubLabelTrimLength, nodeSubLabelFitMode, nodeSubLabelWidth, nodeSubLabelSeparator, nodeSubLabelForceWordBreak, 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, shouldDataUpdate }
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
@@ -0,0 +1,16 @@
import { Curve } from '@unovis/ts/types/curve'
import { SmoothStepDirection } from '@unovis/ts/utils/curves/smooth-step'

export const CURVE_OPTIONS = [
{ label: 'curveSmoothStep', value: undefined },
{ label: 'curveBasis', value: Curve.basis },
{ label: 'curveCardinal', value: Curve.cardinal },
{ label: 'curveCatmullRom', value: Curve.catmullRom },
{ label: 'curveLinear', value: Curve.linear },
{ label: 'curveNatural', value: Curve.natural },
{ label: 'curveStep', value: Curve.step },
{ label: 'curveStepAfter', value: Curve.stepAfter },
{ label: 'curveStepBefore', value: Curve.stepBefore },
]

export const DIRECTIONS = Object.values(SmoothStepDirection)
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React, { useMemo, useState } from 'react'
import { VisSingleContainer } from '@unovis/react/containers/single-container'
import { VisGraph } from '@unovis/react/components/graph'
import { GraphLayoutType, GraphLinkArrowStyle } from '@unovis/ts/components/graph/types'
import { curveSmoothStep, SmoothStepDirection } from '@unovis/ts/utils/curves/smooth-step'
import { generateNodeLinkData } from '@src/utils/data'
import type { ExampleViewerDurationProps } from '@src/components/ExampleViewer/index'

import { CURVE_OPTIONS, DIRECTIONS } from './constants'

export const title = 'ELK Layout: Link Curve Type'
export const subTitle = 'D3 curve factories with configurable smooth-step routing'

export const component = (props: ExampleViewerDurationProps): React.ReactNode => {
const [data] = useState(generateNodeLinkData(20))
const [curveIndex, setCurveIndex] = useState(0)
const [direction, setDirection] = useState<SmoothStepDirection>(SmoothStepDirection.Up)
const [layoutDirection, setLayoutDirection] = useState(SmoothStepDirection.Up)
const [radius, setRadius] = useState(5)
const [offset, setOffset] = useState(20)
const smoothStep = useMemo(() => curveSmoothStep.radius(radius).offset(offset).direction(direction), [radius, offset, direction])
const layoutElkSettings = useMemo(() => ({ 'elk.direction': layoutDirection.toUpperCase() }), [layoutDirection])

return (
<>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 16 }}>
<label>
Curve: {' '}
<select aria-label="Curve" value={curveIndex} onChange={e => setCurveIndex(Number(e.target.value))}>
{CURVE_OPTIONS.map((option, i) => <option key={option.label} value={i}>{option.label}</option>)}
</select>
</label>
<label>
Direction: {' '}
<select aria-label="Direction" value={direction} onChange={e => {
const value = e.target.value as SmoothStepDirection
setDirection(value)
if (value !== SmoothStepDirection.Auto) setLayoutDirection(value)
}}>
{DIRECTIONS.map(value => <option key={value} value={value}>{value}</option>)}
</select>
</label>
<label>
Radius: {' '}
<input aria-label="Radius" type="number" min={0} max={50} value={radius} disabled={curveIndex !== 0}
style={{ width: 64 }} onChange={e => setRadius(Math.max(0, Number(e.target.value)))} />
</label>
<label>
Offset: {' '}
<input aria-label="Offset" type="number" min={0} max={100} value={offset} disabled={curveIndex !== 0}
style={{ width: 64 }} onChange={e => setOffset(Math.max(0, Number(e.target.value)))} />
</label>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 16, width: '100%' }}>
{[
{ label: 'Default Bezier', curve: undefined },
{ label: CURVE_OPTIONS[curveIndex].label, curve: curveIndex === 0 ? smoothStep : CURVE_OPTIONS[curveIndex].value },
].map(({ label, curve }, i) => (
<section key={i} style={{ flex: '1 1 400px', minWidth: 0 }}>
<h3>{label}</h3>
<VisSingleContainer data={data} height={700}>
<VisGraph
layoutType={GraphLayoutType.Elk}
layoutElkSettings={layoutElkSettings}
linkCurve={curve}
linkCurvature={1}
linkWidth={2}
linkStroke="var(--vis-color-main)"
linkArrow={GraphLinkArrowStyle.Single}
duration={props.duration}
/>
</VisSingleContainer>
</section>
))}
</div>
</>
)
}
7 changes: 7 additions & 0 deletions packages/ts/src/components/graph/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { Spacing } from '@/types/spacing'
import { GraphInputLink, GraphInputNode, GraphInputData } from '@/types/graph'
import { BooleanAccessor, ColorAccessor, NumericAccessor, StringAccessor, GenericAccessor } from '@/types/accessor'

import type { CurveFunction } from '@/types/curve'

// Local Types
import {
GraphLayoutType,
Expand Down Expand Up @@ -174,6 +176,10 @@ export interface GraphConfigInterface<N extends GraphInputNode, L extends GraphI
* `1.5` - very curve.
* Default: `0` */
linkCurvature?: NumericAccessor<L>;
/** D3 curve factory used to draw links (e.g. `curveBasis` or `curveSmoothStep`). Receives the source,
* two control points shaped by `linkCurvature`, and the target. `curveSmoothStep` uses only the endpoints.
* Default: `undefined` (the existing cubic Bezier path shaped by `linkCurvature`) */
linkCurve?: CurveFunction;
/** Highlight links on hover. Default: `true` */
linkHighlightOnHover?: boolean;
/** Offset [x,y] in pixels from the source node's center point where the link should start. Default: `undefined` */
Expand Down Expand Up @@ -385,6 +391,7 @@ export const GraphDefaultConfig: GraphConfigInterface<GraphInputNode, GraphInput
linkNeighborSpacing: 8,
linkDisabled: false,
linkCurvature: 0,
linkCurve: undefined,
linkHighlightOnHover: true,
linkSourcePointOffset: undefined,
linkTargetPointOffset: undefined,
Expand Down
Loading
Loading