Skip to content
Draft
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
84 changes: 47 additions & 37 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 38 additions & 3 deletions packages/angular/src/components/sankey/sankey.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SankeyInputLink,
VisEventType,
VisEventCallback,
SankeyZoomMode,
SankeyExitTransitionType,
SankeyEnterTransitionType,
SankeyNode,
Expand All @@ -23,6 +24,7 @@ import {
TrimMode,
SankeySubLabelPlacement,
} from '@unovis/ts'
import { D3ZoomEvent } from 'd3-zoom'
import { VisCoreComponent } from '../../core'

@Component({
Expand Down Expand Up @@ -88,6 +90,21 @@ export class VisSankeyComponent<N extends SankeyInputNode, L extends SankeyInput
/** Coefficient to scale the height of the diagram when the amount of links is low: `C * links.length`, clamped to `[height / 2, height]`. Default: `1/16` */
@Input() heightNormalizationCoeff?: number

/** Horizontal and vertical scale factor applied to the computed layout (column spacing). Keeps node width intact. Default: `undefined` */
@Input() zoomScale?: [number, number]

/** Pan offset in pixels. Default: `undefined` */
@Input() zoomPan?: [number, number]

/** Enable interactive zoom/pan behavior. Default: `true` */
@Input() enableZoom?: boolean

/** Allowed interactive zoom scale extent. Default: `[1, 5]` */
@Input() zoomExtent?: [number, number]

/** Zoom interaction mode. Default: `SankeyZoomMode.XY` */
@Input() zoomMode?: SankeyZoomMode | string

/** Type of animation on removing nodes. Default: `ExitTransitionType.Default` */
@Input() exitTransitionType?: SankeyExitTransitionType

Expand Down Expand Up @@ -182,12 +199,18 @@ export class VisSankeyComponent<N extends SankeyInputNode, L extends SankeyInput
/** Label background */
@Input() labelBackground?: boolean

/** Label fit mode (wrap or trim). Default: `FitMode.TRIM` * */
/** Label fit mode (wrap or trim). Default: `FitMode.Trim` * */
@Input() labelFit?: FitMode

/** Maximum label with in pixels. Default: `70` */
@Input() labelMaxWidth?: number

/** Whether to take the available space for the label. This property is used only if `labelMaxWidth` is not provided. Default: `false` */
@Input() labelMaxWidthTakeAvailableSpace?: boolean

/** Tolerance for the available space for the label. This property is used only if `labelMaxWidthTakeAvailableSpace` is `true`. Default: `undefined` (use label and sub-label font sizes) */
@Input() labelMaxWidthTakeAvailableSpaceTolerance?: number

/** Expand trimmed label on hover. Default: `true` */
@Input() labelExpandTrimmedOnHover?: boolean

Expand All @@ -200,6 +223,9 @@ export class VisSankeyComponent<N extends SankeyInputNode, L extends SankeyInput
/** Label text separators for wrapping. Default: `[' ', '-']` */
@Input() labelTextSeparator?: string[]

/** Label text decoration. Default: `undefined` */
@Input() labelTextDecoration?: StringAccessor<SankeyNode<N, L>>

/** Force break words to fit long labels. Default: `true` */
@Input() labelForceWordBreak?: boolean

Expand All @@ -226,9 +252,18 @@ export class VisSankeyComponent<N extends SankeyInputNode, L extends SankeyInput
/** Sub-label position. Default: `SankeySubLabelPlacement.Below` */
@Input() subLabelPlacement?: SankeySubLabelPlacement | string

/** Sub-label text decoration. Default: `undefined` */
@Input() subLabelTextDecoration?: StringAccessor<SankeyNode<N, L>>

/** Sub-label to label width ratio when `subLabelPlacement` is set to `SankeySubLabelPlacement.Inline`
* Default: `0.4`, which means that 40% of `labelMaxWidth` will be given to sub-label, and 60% to the main label. */
@Input() subLabelToLabelInlineWidthRatio?: number

/** Zoom event callback. Default: `undefined` */
@Input() onZoom?: (horizontalScale: number, verticalScale: number, panX: number, panY: number, zoomExtent: [number, number], event: D3ZoomEvent<SVGGElement, unknown> | undefined) => void

/** Set selected nodes by unique id. Default: `undefined` */
@Input() selectedNodeIds?: string[]
@Input() data: { nodes: N[]; links?: L[] }

component: Sankey<N, L> | undefined
Expand All @@ -250,8 +285,8 @@ export class VisSankeyComponent<N extends SankeyInputNode, L extends SankeyInput
}

private getConfig (): SankeyConfigInterface<N, L> {
const { duration, events, attributes, id, heightNormalizationCoeff, exitTransitionType, enterTransitionType, highlightSubtreeOnHover, highlightDuration, highlightDelay, iterations, nodeSort, linkSort, nodeWidth, nodeAlign, nodeHorizontalSpacing, nodeMinHeight, nodeMaxHeight, nodePadding, showSingleNode, nodeCursor, nodeIcon, nodeColor, nodeFixedValue, nodeIconColor, linkColor, linkValue, linkCursor, label, subLabel, labelPosition, labelVerticalAlign, labelBackground, labelFit, labelMaxWidth, labelExpandTrimmedOnHover, labelTrimMode, labelFontSize, labelTextSeparator, labelForceWordBreak, labelColor, labelCursor, labelVisibility, subLabelFontSize, subLabelColor, subLabelPlacement, subLabelToLabelInlineWidthRatio } = this
const config = { duration, events, attributes, id, heightNormalizationCoeff, exitTransitionType, enterTransitionType, highlightSubtreeOnHover, highlightDuration, highlightDelay, iterations, nodeSort, linkSort, nodeWidth, nodeAlign, nodeHorizontalSpacing, nodeMinHeight, nodeMaxHeight, nodePadding, showSingleNode, nodeCursor, nodeIcon, nodeColor, nodeFixedValue, nodeIconColor, linkColor, linkValue, linkCursor, label, subLabel, labelPosition, labelVerticalAlign, labelBackground, labelFit, labelMaxWidth, labelExpandTrimmedOnHover, labelTrimMode, labelFontSize, labelTextSeparator, labelForceWordBreak, labelColor, labelCursor, labelVisibility, subLabelFontSize, subLabelColor, subLabelPlacement, subLabelToLabelInlineWidthRatio }
const { duration, events, attributes, id, heightNormalizationCoeff, zoomScale, zoomPan, enableZoom, zoomExtent, zoomMode, exitTransitionType, enterTransitionType, highlightSubtreeOnHover, highlightDuration, highlightDelay, iterations, nodeSort, linkSort, nodeWidth, nodeAlign, nodeHorizontalSpacing, nodeMinHeight, nodeMaxHeight, nodePadding, showSingleNode, nodeCursor, nodeIcon, nodeColor, nodeFixedValue, nodeIconColor, linkColor, linkValue, linkCursor, label, subLabel, labelPosition, labelVerticalAlign, labelBackground, labelFit, labelMaxWidth, labelMaxWidthTakeAvailableSpace, labelMaxWidthTakeAvailableSpaceTolerance, labelExpandTrimmedOnHover, labelTrimMode, labelFontSize, labelTextSeparator, labelTextDecoration, labelForceWordBreak, labelColor, labelCursor, labelVisibility, subLabelFontSize, subLabelColor, subLabelPlacement, subLabelTextDecoration, subLabelToLabelInlineWidthRatio, onZoom, selectedNodeIds } = this
const config = { duration, events, attributes, id, heightNormalizationCoeff, zoomScale, zoomPan, enableZoom, zoomExtent, zoomMode, exitTransitionType, enterTransitionType, highlightSubtreeOnHover, highlightDuration, highlightDelay, iterations, nodeSort, linkSort, nodeWidth, nodeAlign, nodeHorizontalSpacing, nodeMinHeight, nodeMaxHeight, nodePadding, showSingleNode, nodeCursor, nodeIcon, nodeColor, nodeFixedValue, nodeIconColor, linkColor, linkValue, linkCursor, label, subLabel, labelPosition, labelVerticalAlign, labelBackground, labelFit, labelMaxWidth, labelMaxWidthTakeAvailableSpace, labelMaxWidthTakeAvailableSpaceTolerance, labelExpandTrimmedOnHover, labelTrimMode, labelFontSize, labelTextSeparator, labelTextDecoration, labelForceWordBreak, labelColor, labelCursor, labelVisibility, subLabelFontSize, subLabelColor, subLabelPlacement, subLabelTextDecoration, subLabelToLabelInlineWidthRatio, onZoom, selectedNodeIds }
const keys = Object.keys(config) as (keyof SankeyConfigInterface<N, L>)[]
keys.forEach(key => { if (config[key] === undefined) delete config[key] })

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { sum } from 'd3-array'
import { groupBy } from '@src/utils/array'
import { SankeyLink, SankeyNode } from '@unovis/ts'

export type ApiEndpointRecord = {
collapsedUrl: string;
Expand Down Expand Up @@ -121,3 +122,29 @@ export function getSankeyData (apiData: ApiEndpointRecord[], collapsedItems: { [
})),
}
}

export const compareStrings = (a = '', b = ''): number => {
const strA = a.toUpperCase()
const strB = b.toUpperCase()

if (strA < strB) return -1
if (strA > strB) return 1
return 0
}

export const nodeSort = (a: SankeyNode<ApiEndpointNode, ApiEndpointLink>, b: SankeyNode<ApiEndpointNode, ApiEndpointLink>): number => {
const aParent = a.targetLinks[0]?.source
const bParent = b.targetLinks[0]?.source
const aGrandparent = a.targetLinks[0]?.source?.targetLinks[0]?.source
const bGrandparent = b.targetLinks[0]?.source?.targetLinks[0]?.source

if ((aParent === bParent)) { // Same parent nodes are sorted by: value + alphabetically
return (b.value - a.value) || compareStrings(a?.path, b?.path)
} else { // Different parent nodes are sorted by: 1st grandparent value + 1st parent value + alphabetically
return (bGrandparent?.value - aGrandparent?.value) || (bParent?.value - aParent?.value) || -compareStrings(aParent?.path, bParent?.path)
}
}

export const linkSort = (a: SankeyLink<ApiEndpointNode, ApiEndpointLink>, b: SankeyLink<ApiEndpointNode, ApiEndpointLink>): number => {
return b.value - a.value || compareStrings(a.target?.path, b.target?.path) // Links sorted by: value + alphabetically
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
Sankey,
SankeyEnterTransitionType,
SankeyExitTransitionType,
SankeyLink,
SankeyNode,
SankeyNodeAlign,
SankeySubLabelPlacement,
Expand All @@ -15,7 +14,7 @@ import {
import { ExampleViewerDurationProps } from '@src/components/ExampleViewer/index'

import apiRawData from './apieplist.json'
import { getSankeyData, ApiEndpointNode, ApiEndpointLink } from './data'
import { getSankeyData, ApiEndpointNode, ApiEndpointLink, nodeSort, linkSort } from './data'

export const title = 'API Endpoints Tree'
export const subTitle = 'Collapsible nodes'
Expand All @@ -28,15 +27,6 @@ export const component = (props: ExampleViewerDurationProps): React.ReactNode =>
const nodeWidth = 30
const nodeHorizontalSpacing = 260

const compareStrings = (a = '', b = ''): number => {
const strA = a.toUpperCase()
const strB = b.toUpperCase()

if (strA < strB) return -1
if (strA > strB) return 1
return 0
}

return (
<>
<VisSingleContainer data={data} sizing={Sizing.Extend}>
Expand Down Expand Up @@ -66,21 +56,8 @@ export const component = (props: ExampleViewerDurationProps): React.ReactNode =>
enterTransitionType={SankeyEnterTransitionType.FromAncestor}
highlightSubtreeOnHover={false}
duration={props.duration}
nodeSort={(a: SankeyNode<ApiEndpointNode, ApiEndpointLink>, b: SankeyNode<ApiEndpointNode, ApiEndpointLink>) => {
const aParent = a.targetLinks[0]?.source
const bParent = b.targetLinks[0]?.source
const aGrandparent = a.targetLinks[0]?.source?.targetLinks[0]?.source
const bGrandparent = b.targetLinks[0]?.source?.targetLinks[0]?.source

if ((aParent === bParent)) { // Same parent nodes are sorted by: value + alphabetically
return (b.value - a.value) || compareStrings(a?.path, b?.path)
} else { // Different parent nodes are sorted by: 1st grandparent value + 1st parent value + alphabetically
return (bGrandparent?.value - aGrandparent?.value) || (bParent?.value - aParent?.value) || -compareStrings(aParent?.path, bParent?.path)
}
}}
linkSort={(a: SankeyLink<ApiEndpointNode, ApiEndpointLink>, b: SankeyLink<ApiEndpointNode, ApiEndpointLink>) => {
return b.value - a.value || compareStrings(a.target?.path, b.target?.path) // Links sorted by: value + alphabetically
}}
nodeSort={nodeSort}
linkSort={linkSort}
events={{
[Sankey.selectors.background]: {
// eslint-disable-next-line no-console
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'
import { VisSingleContainer, VisSankey } from '@unovis/react'
import { Position } from '@unovis/ts'

export const title = 'Basic Sankey'
export const subTitle = 'Label position'

const data = {
nodes: [
{ id: 'A', label: 'Alpha Source', subLabel: 'Total Input' },
{ id: 'B', label: 'Beta Stage', subLabel: 'Direct Output' },
{ id: 'C', label: 'Gamma Processing', subLabel: 'Split to Destinations' },
{ id: 'D', label: 'Delta Sink', subLabel: 'Primary Destination' },
{ id: 'E', label: 'Epsilon Endpoint', subLabel: 'External Export' },
{ id: 'F', label: 'Fallback Flow', subLabel: 'Return/Recycle' },
],
links: [
{ source: 'A', target: 'B', value: 28 },
{ source: 'A', target: 'C', value: 72 },
{ source: 'C', target: 'D', value: 37 },
{ source: 'C', target: 'E', value: 10.5 },
{ source: 'C', target: 'F', value: 24.5 },
],
}

export const component = (): React.ReactNode => {
const [labelPosition, setLabelPosition] = React.useState<Position.Auto | Position.Left | Position.Right | string>(Position.Auto)

return (
<>
<div style={{ marginBottom: 8 }}>
<label>
Label position{' '}
<select
value={labelPosition}
onChange={e => setLabelPosition(e.target.value as Position.Auto | Position.Left | Position.Right | string)}
>
<option value={Position.Auto}>Auto</option>
<option value={Position.Left}>Left</option>
<option value={Position.Right}>Right</option>
</select>
</label>
</div>
<VisSingleContainer data={data}>
<VisSankey
label={(d: typeof data.nodes[0]) => d.label}
labelForceWordBreak={false}
labelPosition={labelPosition}
subLabel={(d: typeof data.nodes[0]) => d.subLabel}
selectedNodeIds={['A']}
/>
</VisSingleContainer>
</>
)
}
Loading