diff --git a/packages/g6/__tests__/dataset/flare.json b/packages/g6/__tests__/dataset/flare.json new file mode 100644 index 00000000000..67776f21f11 --- /dev/null +++ b/packages/g6/__tests__/dataset/flare.json @@ -0,0 +1,355 @@ +{ + "id": "flare", + "children": [ + { + "id": "analytics", + "children": [ + { + "id": "cluster", + "children": [ + { "id": "AgglomerativeCluster", "value": 3938 }, + { "id": "CommunityStructure", "value": 3812 }, + { "id": "HierarchicalCluster", "value": 6714 }, + { "id": "MergeEdge", "value": 743 } + ] + }, + { + "id": "graph", + "children": [ + { "id": "BetweennessCentrality", "value": 3534 }, + { "id": "LinkDistance", "value": 5731 }, + { "id": "MaxFlowMinCut", "value": 7840 }, + { "id": "ShortestPaths", "value": 5914 }, + { "id": "SpanningTree", "value": 3416 } + ] + }, + { + "id": "optimization", + "children": [{ "id": "AspectRatioBanker", "value": 7074 }] + } + ] + }, + { + "id": "animate", + "children": [ + { "id": "Easing", "value": 17010 }, + { "id": "FunctionSequence", "value": 5842 }, + { + "id": "interpolate", + "children": [ + { "id": "ArrayInterpolator", "value": 1983 }, + { "id": "ColorInterpolator", "value": 2047 }, + { "id": "DateInterpolator", "value": 1375 }, + { "id": "Interpolator", "value": 8746 }, + { "id": "MatrixInterpolator", "value": 2202 }, + { "id": "NumberInterpolator", "value": 1382 }, + { "id": "ObjectInterpolator", "value": 1629 }, + { "id": "PointInterpolator", "value": 1675 }, + { "id": "RectangleInterpolator", "value": 2042 } + ] + }, + { "id": "ISchedulable", "value": 1041 }, + { "id": "Parallel", "value": 5176 }, + { "id": "Pause", "value": 449 }, + { "id": "Scheduler", "value": 5593 }, + { "id": "Sequence", "value": 5534 }, + { "id": "Transition", "value": 9201 }, + { "id": "Transitioner", "value": 19975 }, + { "id": "TransitionEvent", "value": 1116 }, + { "id": "Tween", "value": 6006 } + ] + }, + { + "id": "display", + "children": [ + { "id": "DirtySprite", "value": 8833 }, + { "id": "LineSprite", "value": 1732 }, + { "id": "RectSprite", "value": 3623 }, + { "id": "TextSprite", "value": 10066 } + ] + }, + { + "id": "flex", + "children": [{ "id": "FlareVis", "value": 4116 }] + }, + { + "id": "physics", + "children": [ + { "id": "DragForce", "value": 1082 }, + { "id": "GravityForce", "value": 1336 }, + { "id": "IForce", "value": 319 }, + { "id": "NBodyForce", "value": 10498 }, + { "id": "Particle", "value": 2822 }, + { "id": "Simulation", "value": 9983 }, + { "id": "Spring", "value": 2213 }, + { "id": "SpringForce", "value": 1681 } + ] + }, + { + "id": "query", + "children": [ + { "id": "AggregateExpression", "value": 1616 }, + { "id": "And", "value": 1027 }, + { "id": "Arithmetic", "value": 3891 }, + { "id": "Average", "value": 891 }, + { "id": "BinaryExpression", "value": 2893 }, + { "id": "Comparison", "value": 5103 }, + { "id": "CompositeExpression", "value": 3677 }, + { "id": "Count", "value": 781 }, + { "id": "DateUtil", "value": 4141 }, + { "id": "Distinct", "value": 933 }, + { "id": "Expression", "value": 5130 }, + { "id": "ExpressionIterator", "value": 3617 }, + { "id": "Fn", "value": 3240 }, + { "id": "If", "value": 2732 }, + { "id": "IsA", "value": 2039 }, + { "id": "Literal", "value": 1214 }, + { "id": "Match", "value": 3748 }, + { "id": "Maximum", "value": 843 }, + { + "id": "methods", + "children": [ + { "id": "add", "value": 593 }, + { "id": "and", "value": 330 }, + { "id": "average", "value": 287 }, + { "id": "count", "value": 277 }, + { "id": "distinct", "value": 292 }, + { "id": "div", "value": 595 }, + { "id": "eq", "value": 594 }, + { "id": "fn", "value": 460 }, + { "id": "gt", "value": 603 }, + { "id": "gte", "value": 625 }, + { "id": "iff", "value": 748 }, + { "id": "isa", "value": 461 }, + { "id": "lt", "value": 597 }, + { "id": "lte", "value": 619 }, + { "id": "max", "value": 283 }, + { "id": "min", "value": 283 }, + { "id": "mod", "value": 591 }, + { "id": "mul", "value": 603 }, + { "id": "neq", "value": 599 }, + { "id": "not", "value": 386 }, + { "id": "or", "value": 323 }, + { "id": "orderby", "value": 307 }, + { "id": "range", "value": 772 }, + { "id": "select", "value": 296 }, + { "id": "stddev", "value": 363 }, + { "id": "sub", "value": 600 }, + { "id": "sum", "value": 280 }, + { "id": "update", "value": 307 }, + { "id": "variance", "value": 335 }, + { "id": "where", "value": 299 }, + { "id": "xor", "value": 354 }, + { "id": "-", "value": 264 } + ] + }, + { "id": "Minimum", "value": 843 }, + { "id": "Not", "value": 1554 }, + { "id": "Or", "value": 970 }, + { "id": "Query", "value": 13896 }, + { "id": "Range", "value": 1594 }, + { "id": "StringUtil", "value": 4130 }, + { "id": "Sum", "value": 791 }, + { "id": "Variable", "value": 1124 }, + { "id": "Variance", "value": 1876 }, + { "id": "Xor", "value": 1101 } + ] + }, + { + "id": "scale", + "children": [ + { "id": "IScaleMap", "value": 2105 }, + { "id": "LinearScale", "value": 1316 }, + { "id": "LogScale", "value": 3151 }, + { "id": "OrdinalScale", "value": 3770 }, + { "id": "QuantileScale", "value": 2435 }, + { "id": "QuantitativeScale", "value": 4839 }, + { "id": "RootScale", "value": 1756 }, + { "id": "Scale", "value": 4268 }, + { "id": "ScaleType", "value": 1821 }, + { "id": "TimeScale", "value": 5833 } + ] + }, + { + "id": "util", + "children": [ + { "id": "Arrays", "value": 8258 }, + { "id": "Colors", "value": 10001 }, + { "id": "Dates", "value": 8217 }, + { "id": "Displays", "value": 12555 }, + { "id": "Filter", "value": 2324 }, + { "id": "Geometry", "value": 10993 }, + { + "id": "heap", + "children": [ + { "id": "FibonacciHeap", "value": 9354 }, + { "id": "HeapNode", "value": 1233 } + ] + }, + { "id": "IEvaluable", "value": 335 }, + { "id": "IPredicate", "value": 383 }, + { "id": "IValueProxy", "value": 874 }, + { + "id": "math", + "children": [ + { "id": "DenseMatrix", "value": 3165 }, + { "id": "IMatrix", "value": 2815 }, + { "id": "SparseMatrix", "value": 3366 } + ] + }, + { "id": "Maths", "value": 17705 }, + { "id": "Orientation", "value": 1486 }, + { + "id": "palette", + "children": [ + { "id": "ColorPalette", "value": 6367 }, + { "id": "Palette", "value": 1229 }, + { "id": "ShapePalette", "value": 2059 }, + { "id": "SizePalette", "value": 2291 } + ] + }, + { "id": "Property", "value": 5559 }, + { "id": "Shapes", "value": 19118 }, + { "id": "Sort", "value": 6887 }, + { "id": "Stats", "value": 6557 }, + { "id": "Strings", "value": 22026 } + ] + }, + { + "id": "vis", + "children": [ + { + "id": "axis", + "children": [ + { "id": "Axes", "value": 1302 }, + { "id": "Axis", "value": 24593 }, + { "id": "AxisGridLine", "value": 652 }, + { "id": "AxisLabel", "value": 636 }, + { "id": "CartesianAxes", "value": 6703 } + ] + }, + { + "id": "controls", + "children": [ + { "id": "AnchorControl", "value": 2138 }, + { "id": "ClickControl", "value": 3824 }, + { "id": "Control", "value": 1353 }, + { "id": "ControlList", "value": 4665 }, + { "id": "DragControl", "value": 2649 }, + { "id": "ExpandControl", "value": 2832 }, + { "id": "HoverControl", "value": 4896 }, + { "id": "IControl", "value": 763 }, + { "id": "PanZoomControl", "value": 5222 }, + { "id": "SelectionControl", "value": 7862 }, + { "id": "TooltipControl", "value": 8435 } + ] + }, + { + "id": "data", + "children": [ + { "id": "Data", "value": 20544 }, + { "id": "DataList", "value": 19788 }, + { "id": "DataSprite", "value": 10349 }, + { "id": "EdgeSprite", "value": 3301 }, + { "id": "NodeSprite", "value": 19382 }, + { + "id": "render", + "children": [ + { "id": "ArrowType", "value": 698 }, + { "id": "EdgeRenderer", "value": 5569 }, + { "id": "IRenderer", "value": 353 }, + { "id": "ShapeRenderer", "value": 2247 } + ] + }, + { "id": "ScaleBinding", "value": 11275 }, + { "id": "Tree", "value": 7147 }, + { "id": "TreeBuilder", "value": 9930 } + ] + }, + { + "id": "events", + "children": [ + { "id": "DataEvent", "value": 2313 }, + { "id": "SelectionEvent", "value": 1880 }, + { "id": "TooltipEvent", "value": 1701 }, + { "id": "VisualizationEvent", "value": 1117 } + ] + }, + { + "id": "legend", + "children": [ + { "id": "Legend", "value": 20859 }, + { "id": "LegendItem", "value": 4614 }, + { "id": "LegendRange", "value": 10530 } + ] + }, + { + "id": "operator", + "children": [ + { + "id": "distortion", + "children": [ + { "id": "BifocalDistortion", "value": 4461 }, + { "id": "Distortion", "value": 6314 }, + { "id": "FisheyeDistortion", "value": 3444 } + ] + }, + { + "id": "encoder", + "children": [ + { "id": "ColorEncoder", "value": 3179 }, + { "id": "Encoder", "value": 4060 }, + { "id": "PropertyEncoder", "value": 4138 }, + { "id": "ShapeEncoder", "value": 1690 }, + { "id": "SizeEncoder", "value": 1830 } + ] + }, + { + "id": "filter", + "children": [ + { "id": "FisheyeTreeFilter", "value": 5219 }, + { "id": "GraphDistanceFilter", "value": 3165 }, + { "id": "VisibilityFilter", "value": 3509 } + ] + }, + { "id": "IOperator", "value": 1286 }, + { + "id": "label", + "children": [ + { "id": "Labeler", "value": 9956 }, + { "id": "RadialLabeler", "value": 3899 }, + { "id": "StackedAreaLabeler", "value": 3202 } + ] + }, + { + "id": "layout", + "children": [ + { "id": "AxisLayout", "value": 6725 }, + { "id": "BundledEdgeRouter", "value": 3727 }, + { "id": "CircleLayout", "value": 9317 }, + { "id": "CirclePackingLayout", "value": 12003 }, + { "id": "DendrogramLayout", "value": 4853 }, + { "id": "ForceDirectedLayout", "value": 8411 }, + { "id": "IcicleTreeLayout", "value": 4864 }, + { "id": "IndentedTreeLayout", "value": 3174 }, + { "id": "Layout", "value": 7881 }, + { "id": "NodeLinkTreeLayout", "value": 12870 }, + { "id": "PieLayout", "value": 2728 }, + { "id": "RadialTreeLayout", "value": 12348 }, + { "id": "RandomLayout", "value": 870 }, + { "id": "StackedAreaLayout", "value": 9121 }, + { "id": "TreeMapLayout", "value": 9191 } + ] + }, + { "id": "Operator", "value": 2490 }, + { "id": "OperatorList", "value": 5248 }, + { "id": "OperatorSequence", "value": 4190 }, + { "id": "OperatorSwitch", "value": 2581 }, + { "id": "SortOperator", "value": 2023 } + ] + }, + { "id": "Visualization", "value": 16540 } + ] + } + ] +} diff --git a/packages/g6/__tests__/demos/case-radial-dendrogram.ts b/packages/g6/__tests__/demos/case-radial-dendrogram.ts new file mode 100644 index 00000000000..24dbb6d33e6 --- /dev/null +++ b/packages/g6/__tests__/demos/case-radial-dendrogram.ts @@ -0,0 +1,58 @@ +import data from '@@/dataset/flare.json'; +import { Graph, treeToGraphData } from '@antv/g6'; + +export const caseRadialDendrogram: TestCase = async (context) => { + const graph = new Graph({ + ...context, + autoFit: 'view', + data: treeToGraphData(data), + node: { + style: { + size: 14, + labelText: (d) => d.id, + labelBackground: true, + }, + state: { + active: { + fill: '#00C9C9', + }, + }, + }, + edge: { + type: 'cubic-radial', + style: { + lineWidth: 2, + }, + state: { + active: { + stroke: '#009999', + }, + }, + }, + layout: [ + { + type: 'dendrogram', + radial: true, + nodeSep: 30, + rankSep: 200, + }, + ], + behaviors: [ + 'drag-canvas', + 'zoom-canvas', + 'drag-element', + { + key: 'hover-activate', + type: 'hover-activate', + degree: 5, + direction: 'in', + inactiveState: 'inactive', + }, + ], + transforms: ['position-radial-labels'], + }); + + await graph.render(); + + return graph; +}; diff --git a/packages/g6/__tests__/demos/element-edge-cubic-radial.ts b/packages/g6/__tests__/demos/element-edge-cubic-radial.ts new file mode 100644 index 00000000000..5e3c825e9fc --- /dev/null +++ b/packages/g6/__tests__/demos/element-edge-cubic-radial.ts @@ -0,0 +1,25 @@ +import data from '@@/dataset/algorithm-category.json'; +import { Graph, treeToGraphData } from '@antv/g6'; + +export const elementEdgeCubicRadial: TestCase = async (context) => { + const graph = new Graph({ + ...context, + autoFit: 'view', + data: treeToGraphData(data), + edge: { + type: 'cubic-radial', + }, + layout: [ + { + type: 'dendrogram', + radial: true, + nodeSep: 30, + rankSep: 200, + }, + ], + }); + + await graph.render(); + + return graph; +}; diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts index 8c88fae237e..cd2322be201 100644 --- a/packages/g6/__tests__/demos/index.ts +++ b/packages/g6/__tests__/demos/index.ts @@ -25,6 +25,7 @@ export { caseDecisionTree } from './case-decision-tree'; export { caseIndentedTree } from './case-indented-tree'; export { caseMindmap } from './case-mindmap'; export { caseOrgChart } from './case-org-chart'; +export { caseRadialDendrogram } from './case-radial-dendrogram'; export { caseWhyDoCats } from './case-why-do-cats'; export { commonGraph } from './common-graph'; export { controllerViewport } from './controller-viewport'; @@ -34,6 +35,7 @@ export { elementCombo } from './element-combo'; export { elementEdgeArrow } from './element-edge-arrow'; export { elementEdgeCubic } from './element-edge-cubic'; export { elementEdgeCubicHorizontal } from './element-edge-cubic-horizontal'; +export { elementEdgeCubicRadial } from './element-edge-cubic-radial'; export { elementEdgeCubicVertical } from './element-edge-cubic-vertical'; export { elementEdgeCustomArrow } from './element-edge-custom-arrow'; export { elementEdgeLine } from './element-edge-line'; @@ -87,6 +89,7 @@ export { layoutCustomIterative } from './layout-custom-iterative'; export { layoutD3Force } from './layout-d3-force'; export { layoutDagre } from './layout-dagre'; export { layoutDendrogramBasic } from './layout-dendrogram-basic'; +export { layoutDendrogramRadial } from './layout-dendrogram-radial'; export { layoutDendrogramTb } from './layout-dendrogram-tb'; export { layoutForce } from './layout-force'; export { layoutForceCollision } from './layout-force-collision'; @@ -132,5 +135,6 @@ export { pluginTooltip } from './plugin-tooltip'; export { pluginWatermark } from './plugin-watermark'; export { pluginWatermarkImage } from './plugin-watermark-image'; export { theme } from './theme'; +export { transformPositionRadialLabels } from './transform-position-radial-labels'; export { transformProcessParallelEdges } from './transform-process-parallel-edges'; export { viewportFit } from './viewport-fit'; diff --git a/packages/g6/__tests__/demos/layout-dendrogram-radial.ts b/packages/g6/__tests__/demos/layout-dendrogram-radial.ts new file mode 100644 index 00000000000..2a607c34c7e --- /dev/null +++ b/packages/g6/__tests__/demos/layout-dendrogram-radial.ts @@ -0,0 +1,27 @@ +import data from '@@/dataset/algorithm-category.json'; +import { Graph, treeToGraphData } from '@antv/g6'; + +export const layoutDendrogramRadial: TestCase = async (context) => { + const graph = new Graph({ + ...context, + autoFit: 'view', + data: treeToGraphData(data), + node: { + style: { + labelText: (d) => d.id, + }, + }, + layout: [ + { + type: 'dendrogram', + radial: true, + nodeSep: 30, + rankSep: 200, + }, + ], + }); + + await graph.render(); + + return graph; +}; diff --git a/packages/g6/__tests__/demos/transform-position-radial-labels.ts b/packages/g6/__tests__/demos/transform-position-radial-labels.ts new file mode 100644 index 00000000000..50b399a594b --- /dev/null +++ b/packages/g6/__tests__/demos/transform-position-radial-labels.ts @@ -0,0 +1,28 @@ +import data from '@@/dataset/algorithm-category.json'; +import { Graph, treeToGraphData } from '@antv/g6'; + +export const transformPositionRadialLabels: TestCase = async (context) => { + const graph = new Graph({ + ...context, + autoFit: 'view', + data: treeToGraphData(data), + node: { + style: { + labelText: (d) => d.id, + }, + }, + layout: [ + { + type: 'dendrogram', + radial: true, + nodeSep: 30, + rankSep: 200, + }, + ], + transforms: ['position-radial-labels'], + }); + + await graph.render(); + + return graph; +}; diff --git a/packages/g6/__tests__/snapshots/elements/edges/cubic-radial/default.svg b/packages/g6/__tests__/snapshots/elements/edges/cubic-radial/default.svg new file mode 100644 index 00000000000..efdf6ba145f --- /dev/null +++ b/packages/g6/__tests__/snapshots/elements/edges/cubic-radial/default.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/snapshots/transforms/transform-position-radial-labels/default.svg b/packages/g6/__tests__/snapshots/transforms/transform-position-radial-labels/default.svg new file mode 100644 index 00000000000..825033c8351 --- /dev/null +++ b/packages/g6/__tests__/snapshots/transforms/transform-position-radial-labels/default.svg @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Modeling Methods + + + + + + + + + + + + Classification + + + + + + + + + + + + Logistic regression + + + + + + + + + + + + Linear discriminant analysis + + + + + + + + + + + + Rules + + + + + + + + + + + + Decision trees + + + + + + + + + + + + Naive Bayes + + + + + + + + + + + + K nearest neighbor + + + + + + + + + + + + Probabilistic neural network + + + + + + + + + + + + Support vector machine + + + + + + + + + + + + Consensus + + + + + + + + + + + + Models diversity + + + + + + + + + + + + Different initializations + + + + + + + + + + + + Different parameter choices + + + + + + + + + + + + Different architectures + + + + + + + + + + + + Different modeling methods + + + + + + + + + + + + Different training sets + + + + + + + + + + + + Different feature sets + + + + + + + + + + + + Methods + + + + + + + + + + + + Classifier selection + + + + + + + + + + + + Classifier fusion + + + + + + + + + + + + Common + + + + + + + + + + + + Bagging + + + + + + + + + + + + Boosting + + + + + + + + + + + + AdaBoost + + + + + + + + + + + + Regression + + + + + + + + + + + + Multiple linear regression + + + + + + + + + + + + Partial least squares + + + + + + + + + + + + Multi-layer feed forward neural network + + + + + + + + + + + + General regression neural network + + + + + + + + + + + + Support vector regression + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/unit/elements/edges/cubic-radial.spec.ts b/packages/g6/__tests__/unit/elements/edges/cubic-radial.spec.ts new file mode 100644 index 00000000000..67d85e7c2bd --- /dev/null +++ b/packages/g6/__tests__/unit/elements/edges/cubic-radial.spec.ts @@ -0,0 +1,11 @@ +import { elementEdgeCubicRadial } from '@@/demos'; +import { createDemoGraph } from '@@/utils'; + +describe('element edge cubic radial', () => { + it('render', async () => { + const graph = await createDemoGraph(elementEdgeCubicRadial); + await expect(graph).toMatchSnapshot(__filename); + + graph.destroy(); + }); +}); diff --git a/packages/g6/__tests__/unit/registry.spec.ts b/packages/g6/__tests__/unit/registry.spec.ts index 4ea1275c3ff..b0a95b012d3 100644 --- a/packages/g6/__tests__/unit/registry.spec.ts +++ b/packages/g6/__tests__/unit/registry.spec.ts @@ -3,6 +3,7 @@ import { CircleCombo, Cubic, CubicHorizontal, + CubicRadial, CubicVertical, Diamond, Donut, @@ -47,6 +48,7 @@ describe('registry', () => { quadratic: Quadratic, 'cubic-horizontal': CubicHorizontal, 'cubic-vertical': CubicVertical, + 'cubic-radial': CubicRadial, }); expect(getExtensions(ExtensionCategory.COMBO)).toEqual({ circle: CircleCombo, diff --git a/packages/g6/__tests__/unit/runtime/data.spec.ts b/packages/g6/__tests__/unit/runtime/data.spec.ts index 8ccefef6052..bf4d4a6032b 100644 --- a/packages/g6/__tests__/unit/runtime/data.spec.ts +++ b/packages/g6/__tests__/unit/runtime/data.spec.ts @@ -590,6 +590,14 @@ describe('DataController', () => { expect(controller.getNodeLikeData()).toEqual([...data.combos, ...data.nodes]); }); + it('getRootsData', () => { + const controller = new DataController(); + + controller.addData(treeToGraphData(tree)); + + expect(controller.getRootsData('tree').map(idOf)).toEqual(['Modeling Methods']); + }); + it('getAncestorsData getParentData getChildrenData', () => { const controller = new DataController(); diff --git a/packages/g6/__tests__/unit/transforms/transform-position-radial-labels.spec.ts b/packages/g6/__tests__/unit/transforms/transform-position-radial-labels.spec.ts new file mode 100644 index 00000000000..75049143e46 --- /dev/null +++ b/packages/g6/__tests__/unit/transforms/transform-position-radial-labels.spec.ts @@ -0,0 +1,11 @@ +import { transformPositionRadialLabels } from '@@/demos'; +import { createDemoGraph } from '@@/utils'; + +describe('transform position radial labels', () => { + it('render', async () => { + const graph = await createDemoGraph(transformPositionRadialLabels); + await expect(graph).toMatchSnapshot(__filename); + + graph.destroy(); + }); +}); diff --git a/packages/g6/__tests__/unit/utils/point.spec.ts b/packages/g6/__tests__/unit/utils/point.spec.ts index a6ced8c43ae..e52a9efe7bc 100644 --- a/packages/g6/__tests__/unit/utils/point.spec.ts +++ b/packages/g6/__tests__/unit/utils/point.spec.ts @@ -9,6 +9,7 @@ import { getEllipseIntersectPoint, getPolygonIntersectPoint, getRectIntersectPoint, + getSymmetricPoint, isCollinear, isHorizontal, isOrthogonal, @@ -100,6 +101,11 @@ describe('Point Functions', () => { expect(isCollinear([100, 100], [50, 50], [150, 100])).toEqual(false); }); + it('getSymmetricPoint', () => { + expect(getSymmetricPoint([50, 50], [100, 100])).toEqual([150, 150]); + expect(getSymmetricPoint([-50, -50], [0, 0])).toEqual([50, 50]); + }); + it('getRectIntersectPoint', () => { const rect = new Rect({ style: { @@ -110,6 +116,7 @@ describe('Point Functions', () => { }, }); expect(getRectIntersectPoint([110, 110], rect.getBounds())).toEqual([102, 102]); + expect(getRectIntersectPoint([110, 110], rect.getBounds(), true)).toEqual([100, 100]); }); it('getEllipseIntersectPoint', () => { @@ -121,6 +128,7 @@ describe('Point Functions', () => { }, }); expect(getEllipseIntersectPoint([110, 100], circle.getBounds())).toEqual([101, 100]); + expect(getEllipseIntersectPoint([110, 100], circle.getBounds(), true)).toEqual([99, 100]); const circle2 = new Circle({ style: { diff --git a/packages/g6/__tests__/unit/utils/relation.spec.ts b/packages/g6/__tests__/unit/utils/relation.spec.ts index e23869249a6..8c12b080409 100644 --- a/packages/g6/__tests__/unit/utils/relation.spec.ts +++ b/packages/g6/__tests__/unit/utils/relation.spec.ts @@ -32,11 +32,15 @@ describe('relation', () => { expect(getElementNthDegreeIds(graph, 'edge', '1-2', 1)).toEqual(['1', '2', '1-2']); expect(getElementNthDegreeIds(graph, 'edge', '1-2', 2)).toEqual(['1', '1-2', '1-3', '2', '3', '2-4', '4']); expect(getElementNthDegreeIds(graph, 'combo', 'combo1', 1)).toEqual(['combo1', 'combo1-6', '6']); + expect(getElementNthDegreeIds(graph, 'node', '1', 1, 'in')).toEqual(['1']); + expect(getElementNthDegreeIds(graph, 'node', '1', 1, 'out')).toEqual(['1', '1-2', '1-3', '2', '3']); }); it('getNodeNthDegreeIds', () => { expect(getNodeNthDegreeIds(graph, '1', 0)).toEqual(['1']); expect(getNodeNthDegreeIds(graph, '1', 1)).toEqual(['1', '1-2', '1-3', '2', '3']); expect(getNodeNthDegreeIds(graph, '1', 2)).toEqual(['1', '1-2', '1-3', '2', '2-4', '3', '3-5', '4', '5']); + expect(getNodeNthDegreeIds(graph, '1', 1, 'in')).toEqual(['1']); + expect(getNodeNthDegreeIds(graph, '1', 1, 'out')).toEqual(['1', '1-2', '1-3', '2', '3']); }); }); diff --git a/packages/g6/__tests__/unit/utils/vector.spec.ts b/packages/g6/__tests__/unit/utils/vector.spec.ts index 9cd1c61bcce..80480ce82e2 100644 --- a/packages/g6/__tests__/unit/utils/vector.spec.ts +++ b/packages/g6/__tests__/unit/utils/vector.spec.ts @@ -11,6 +11,7 @@ import { multiply, normalize, perpendicular, + rad, scale, subtract, toVector2, @@ -105,4 +106,9 @@ describe('Vector Functions', () => { expect(toVector3([1, 2, 3])).toEqual([1, 2, 3]); expect(toVector3([1, 2])).toEqual([1, 2, 0]); }); + + it('rad', () => { + expect(rad([1, 0])).toEqual(0); + expect(rad([0, 1])).toEqual(Math.PI / 2); + }); }); diff --git a/packages/g6/package.json b/packages/g6/package.json index 09f051892db..8356392b55e 100644 --- a/packages/g6/package.json +++ b/packages/g6/package.json @@ -63,7 +63,7 @@ "@antv/g-canvas": "^2.0.10", "@antv/g-plugin-dragndrop": "^2.0.8", "@antv/graphlib": "^2.0.3", - "@antv/hierarchy": "^0.6.12", + "@antv/hierarchy": "^0.6.13", "@antv/layout": "^1.2.14-beta.6", "@antv/util": "^3.3.7", "bubblesets-js": "^2.3.3", diff --git a/packages/g6/src/behaviors/hover-activate.ts b/packages/g6/src/behaviors/hover-activate.ts index 6d6e0c3267a..7ad59d30f54 100644 --- a/packages/g6/src/behaviors/hover-activate.ts +++ b/packages/g6/src/behaviors/hover-activate.ts @@ -2,7 +2,7 @@ import { isFunction } from '@antv/util'; import { CommonEvent } from '../constants'; import { ELEMENT_TYPES } from '../constants/element'; import type { RuntimeContext } from '../runtime/types'; -import type { Element, ElementType, ID, IDragEvent, IPointerEvent, State } from '../types'; +import type { EdgeDirection, Element, ElementType, ID, IDragEvent, IPointerEvent, State } from '../types'; import { idsOf } from '../utils/id'; import { getElementNthDegreeIds } from '../utils/relation'; import type { BaseBehaviorOptions } from './base-behavior'; @@ -39,6 +39,19 @@ export interface HoverActivateOptions extends BaseBehaviorOptions { * @defaultValue 0 */ degree?: number; + /** + * 指定边的方向 + * - `'both'`: 表示激活当前节点的所有关系 + * - `'in'`: 表示激活当前节点的入边和入节点 + * - `'out'`: 表示激活当前节点的出边和出节点 + * + * Specify the direction of the edge + * - `'both'`: Activate all relationships of the current node + * - `'in'`: Activate the incoming edges and nodes of the current node + * - `'out'`: Activate the outgoing edges and nodes of the current node + * @defaultValue 'both' + */ + direction?: EdgeDirection; /** * 激活元素的状态,默认为 `active` * @@ -80,6 +93,7 @@ export class HoverActivate extends BaseBehavior { animation: false, enable: true, degree: 0, + direction: 'both', state: 'active', inactiveState: undefined, }; @@ -123,11 +137,11 @@ export class HoverActivate extends BaseBehavior { if (!this.options.state && !this.options.inactiveState) return; const { graph } = this.context; - const { state, degree, animation, inactiveState } = this.options; + const { state, degree, direction, animation, inactiveState } = this.options; const { targetType, target } = event; const activeIds = degree - ? getElementNthDegreeIds(graph, targetType as ElementType, target.id, degree) + ? getElementNthDegreeIds(graph, targetType as ElementType, target.id, degree, direction) : [target.id]; const states: Record = {}; diff --git a/packages/g6/src/elements/combos/circle.ts b/packages/g6/src/elements/combos/circle.ts index 2cd059fae34..a1340cb6505 100644 --- a/packages/g6/src/elements/combos/circle.ts +++ b/packages/g6/src/elements/combos/circle.ts @@ -54,8 +54,8 @@ export class CircleCombo extends BaseCombo { return [expandedR * 2, expandedR * 2, 0]; } - public getIntersectPoint(point: Point): Point { + public getIntersectPoint(point: Point, useExtendedLine = false): Point { const keyShapeBounds = this.getShape('key').getBounds(); - return getEllipseIntersectPoint(point, keyShapeBounds); + return getEllipseIntersectPoint(point, keyShapeBounds, useExtendedLine); } } diff --git a/packages/g6/src/elements/edges/cubic-radial.ts b/packages/g6/src/elements/edges/cubic-radial.ts new file mode 100644 index 00000000000..b6cb13942d0 --- /dev/null +++ b/packages/g6/src/elements/edges/cubic-radial.ts @@ -0,0 +1,77 @@ +import type { DisplayObjectConfig } from '@antv/g'; +import type { NodeData } from '../../spec'; +import type { Point } from '../../types'; +import { positionOf } from '../../utils/position'; +import { mergeOptions } from '../../utils/style'; +import { distance, rad, subtract } from '../../utils/vector'; +import type { CubicStyleProps } from './cubic'; +import { Cubic } from './cubic'; + +/** + * 径向贝塞尔曲线样式配置项 + * + * Radial cubic style props + */ +export interface CubicRadialStyleProps extends CubicStyleProps {} + +/** + * 径向贝塞尔曲线 + * + * Radial cubic edge + */ +export class CubicRadial extends Cubic { + static defaultStyleProps: Partial = { + curvePosition: 0.5, + curveOffset: 20, + }; + + constructor(options: DisplayObjectConfig) { + super(mergeOptions({ style: CubicRadial.defaultStyleProps }, options)); + } + + private get ref(): NodeData { + return this.context.model.getRootsData()[0]; + } + + protected getEndpoints(attributes: Required): [Point, Point] { + if (this.sourceNode.id === this.ref.id) { + return super.getEndpoints(attributes); + } + + const refPoint = positionOf(this.ref); + const sourcePoint = this.sourceNode.getIntersectPoint(refPoint, true); + const targetPoint = this.targetNode.getIntersectPoint(refPoint); + + return [sourcePoint, targetPoint]; + } + + private toRadialCoordinate(p: Point) { + const refPoint = positionOf(this.ref); + const r = distance(p, refPoint); + const radian = rad(subtract(p, refPoint)); + return [r, radian]; + } + + protected getControlPoints( + sourcePoint: Point, + targetPoint: Point, + curvePosition: [number, number], + curveOffset: [number, number], + ): [Point, Point] { + const [r1, rad1] = this.toRadialCoordinate(sourcePoint); + const [r2] = this.toRadialCoordinate(targetPoint); + + const rDist = r2 - r1; + + return [ + [ + sourcePoint[0] + (rDist * curvePosition[0] + curveOffset[0]) * Math.cos(rad1), + sourcePoint[1] + (rDist * curvePosition[0] + curveOffset[0]) * Math.sin(rad1), + ], + [ + targetPoint[0] - (rDist * curvePosition[1] - curveOffset[0]) * Math.cos(rad1), + targetPoint[1] - (rDist * curvePosition[1] - curveOffset[0]) * Math.sin(rad1), + ], + ]; + } +} diff --git a/packages/g6/src/elements/edges/index.ts b/packages/g6/src/elements/edges/index.ts index dcbc4068d01..fe53486a4e8 100644 --- a/packages/g6/src/elements/edges/index.ts +++ b/packages/g6/src/elements/edges/index.ts @@ -1,6 +1,7 @@ export { BaseEdge } from './base-edge'; export { Cubic } from './cubic'; export { CubicHorizontal } from './cubic-horizontal'; +export { CubicRadial } from './cubic-radial'; export { CubicVertical } from './cubic-vertical'; export { Line } from './line'; export { Polyline } from './polyline'; @@ -9,6 +10,7 @@ export { Quadratic } from './quadratic'; export type { BaseEdgeStyleProps } from './base-edge'; export type { CubicStyleProps } from './cubic'; export type { CubicHorizontalStyleProps } from './cubic-horizontal'; +export type { CubicRadialStyleProps } from './cubic-radial'; export type { CubicVerticalStyleProps } from './cubic-vertical'; export type { LineStyleProps } from './line'; export type { PolylineStyleProps } from './polyline'; diff --git a/packages/g6/src/elements/nodes/base-node.ts b/packages/g6/src/elements/nodes/base-node.ts index f6ef8aa3fd1..bfd91bd038e 100644 --- a/packages/g6/src/elements/nodes/base-node.ts +++ b/packages/g6/src/elements/nodes/base-node.ts @@ -354,11 +354,12 @@ export abstract class BaseNode, container: Group): void { diff --git a/packages/g6/src/elements/nodes/circle.ts b/packages/g6/src/elements/nodes/circle.ts index 4f806dbdd57..454c053a698 100644 --- a/packages/g6/src/elements/nodes/circle.ts +++ b/packages/g6/src/elements/nodes/circle.ts @@ -45,8 +45,8 @@ export class Circle extends BaseNode { return style ? ({ width: size, height: size, ...style } as IconStyleProps) : false; } - public getIntersectPoint(point: Point): Point { + public getIntersectPoint(point: Point, useExtendedLine = false): Point { const keyShapeBounds = this.getShape('key').getBounds(); - return getEllipseIntersectPoint(point, keyShapeBounds); + return getEllipseIntersectPoint(point, keyShapeBounds, useExtendedLine); } } diff --git a/packages/g6/src/elements/nodes/ellipse.ts b/packages/g6/src/elements/nodes/ellipse.ts index a63ba0050cc..c2ce377516b 100644 --- a/packages/g6/src/elements/nodes/ellipse.ts +++ b/packages/g6/src/elements/nodes/ellipse.ts @@ -51,8 +51,8 @@ export class Ellipse extends BaseNode { return style ? ({ width: size, height: size, ...style } as IconStyleProps) : false; } - public getIntersectPoint(point: Point): Point { + public getIntersectPoint(point: Point, useExtendedLine = false): Point { const keyShapeBounds = this.getShape('key').getBounds(); - return getEllipseIntersectPoint(point, keyShapeBounds); + return getEllipseIntersectPoint(point, keyShapeBounds, useExtendedLine); } } diff --git a/packages/g6/src/elements/shapes/polygon.ts b/packages/g6/src/elements/shapes/polygon.ts index 7499f504a34..8a90bf90a6d 100644 --- a/packages/g6/src/elements/shapes/polygon.ts +++ b/packages/g6/src/elements/shapes/polygon.ts @@ -43,9 +43,9 @@ export abstract class Polygon e protected abstract getPoints(attributes: Required): Point[]; - public getIntersectPoint(point: Point): Point { + public getIntersectPoint(point: Point, useExtendedLine = false): Point { const { points } = this.getShape('key').attributes; const center: Point = [+(this.attributes?.x || 0), +(this.attributes?.y || 0)]; - return getPolygonIntersectPoint(point, center, points!).point; + return getPolygonIntersectPoint(point, center, points!, true, useExtendedLine).point; } } diff --git a/packages/g6/src/exports.ts b/packages/g6/src/exports.ts index 7de656410d4..f1389e529a3 100644 --- a/packages/g6/src/exports.ts +++ b/packages/g6/src/exports.ts @@ -27,7 +27,17 @@ export { NodeEvent, } from './constants'; export { BaseCombo, CircleCombo, RectCombo } from './elements/combos'; -export { BaseEdge, Cubic, CubicHorizontal, CubicVertical, Line, Polyline, Quadratic } from './elements/edges'; +export { + BaseEdge, + Cubic, + CubicHorizontal, + CubicRadial, + CubicVertical, + Line, + Polyline, + Quadratic, +} from './elements/edges'; +export { effect } from './elements/effect'; export { BaseNode, Circle, @@ -47,20 +57,20 @@ export { BaseLayout, CircularLayout, ComboCombinedLayout, + compactBox as CompactBoxLayout, ConcentricLayout, D3ForceLayout, DagreLayout, + dendrogram as DendrogramLayout, ForceAtlas2Layout, ForceLayout, FruchtermanLayout, GridLayout, + indented as IndentedLayout, MDSLayout, + mindmap as MindmapLayout, RadialLayout, RandomLayout, - compactBox, - dendrogram, - indented, - mindmap, } from './layouts'; export { BasePlugin, @@ -84,10 +94,11 @@ export { export { getExtension, getExtensions } from './registry/get'; export { register } from './registry/register'; export { Graph } from './runtime/graph'; -export { BaseTransform } from './transforms'; +export { BaseTransform, PositionRadialLabels, ProcessParallelEdges } from './transforms'; export { isCollapsed } from './utils/collapsibility'; export { idOf } from './utils/id'; export { invokeLayoutMethod } from './utils/layout'; +export { positionOf } from './utils/position'; export { omitStyleProps, subStyleProps } from './utils/prefix'; export { Shortcut } from './utils/shortcut'; export { parseSize } from './utils/size'; @@ -135,6 +146,7 @@ export type { BaseComboStyleProps, CircleComboStyleProps, RectComboStyleProps } export type { BaseEdgeStyleProps, CubicHorizontalStyleProps, + CubicRadialStyleProps, CubicStyleProps, CubicVerticalStyleProps, LineStyleProps, @@ -205,7 +217,7 @@ export type { CustomBehaviorOption } from './spec/behavior'; export type { AnimationStage } from './spec/element/animation'; export type { LayoutOptions, STDLayoutOptions, SingleLayoutOptions } from './spec/layout'; export type { CustomPluginOption } from './spec/plugin'; -export type { BaseTransformOptions } from './transforms'; +export type { BaseTransformOptions, PositionRadialLabelsOptions, ProcessParallelEdgesOptions } from './transforms'; export type { DrawData } from './transforms/types'; export type { BaseElementStyleProps, diff --git a/packages/g6/src/registry/build-in.ts b/packages/g6/src/registry/build-in.ts index 8c0aa31bc85..3197e525432 100644 --- a/packages/g6/src/registry/build-in.ts +++ b/packages/g6/src/registry/build-in.ts @@ -32,6 +32,7 @@ import { CircleCombo, Cubic, CubicHorizontal, + CubicRadial, CubicVertical, Diamond, Donut, @@ -92,6 +93,7 @@ import { CollapseExpandCombo, CollapseExpandNode, GetEdgeActualEnds, + PositionRadialLabels, ProcessParallelEdges, UpdateRelatedEdge, } from '../transforms'; @@ -138,6 +140,7 @@ const BUILT_IN_EXTENSIONS: ExtensionRegistry = { polyline: Polyline, quadratic: Quadratic, 'cubic-horizontal': CubicHorizontal, + 'cubic-radial': CubicRadial, 'cubic-vertical': CubicVertical, }, layout: { @@ -207,6 +210,7 @@ const BUILT_IN_EXTENSIONS: ExtensionRegistry = { 'collapse-expand-node': CollapseExpandNode, 'process-parallel-edges': ProcessParallelEdges, 'get-edge-actual-ends': GetEdgeActualEnds, + 'position-radial-labels': PositionRadialLabels, }, shape: { circle: GCircle, diff --git a/packages/g6/src/runtime/data.ts b/packages/g6/src/runtime/data.ts index aa062399db9..1e41c0be3d3 100644 --- a/packages/g6/src/runtime/data.ts +++ b/packages/g6/src/runtime/data.ts @@ -166,6 +166,10 @@ export class DataController { }, [] as ComboData[]); } + public getRootsData(hierarchyKey: HierarchyKey = TREE_KEY) { + return this.model.getRoots(hierarchyKey).map(toG6Data); + } + public getAncestorsData(id: ID, hierarchyKey: HierarchyKey): NodeLikeData[] { const { model } = this; if (!model.hasNode(id) || !model.hasTreeStructure(hierarchyKey)) return []; diff --git a/packages/g6/src/runtime/layout.ts b/packages/g6/src/runtime/layout.ts index 26f9fdb4d2f..061d2312172 100644 --- a/packages/g6/src/runtime/layout.ts +++ b/packages/g6/src/runtime/layout.ts @@ -69,6 +69,13 @@ export class LayoutController { } } emit(graph, new GraphLifeCycleEvent(GraphEvent.AFTER_LAYOUT)); + this.transformDataAfterLayout(); + } + + private transformDataAfterLayout() { + const transforms = this.context.transform.getTransformInstance(); + + Object.values(transforms).forEach((transform) => transform.afterLayout()); } /** diff --git a/packages/g6/src/transforms/base-transform.ts b/packages/g6/src/transforms/base-transform.ts index 5fa9f67ade7..15c7a998647 100644 --- a/packages/g6/src/transforms/base-transform.ts +++ b/packages/g6/src/transforms/base-transform.ts @@ -9,4 +9,6 @@ export abstract class BaseTransform 根据径向布局自动调整节点标签样式的配置项 + * + * Options for automatically adjusting the style of node labels according to the radial layout + */ +export interface PositionRadialLabelsOptions extends BaseTransformOptions { + /** + * 偏移量 + * + * Offset + */ + offset?: number; +} + +/** + * 根据径向布局自动调整节点标签样式,包括位置和旋转角度 + * + * Automatically adjust the style of node labels according to the radial layout, including position and rotation angle + */ +export class PositionRadialLabels extends BaseTransform { + static defaultOptions: Partial = { + offset: 5, + }; + + constructor(context: RuntimeContext, options: PositionRadialLabelsOptions) { + super(context, Object.assign({}, PositionRadialLabels.defaultOptions, options)); + } + + private get ref(): NodeData { + return this.context.model.getRootsData()[0]; + } + + public afterLayout() { + const refPoint = positionOf(this.ref); + + const { graph, model } = this.context; + const data = model.getData(); + + data.nodes?.forEach((datum) => { + if (idOf(datum) === idOf(this.ref)) return; + + const radian = rad(subtract(positionOf(datum), refPoint)); + const isLeft = Math.abs(radian) > Math.PI / 2; + + const isLeaf = !datum.children || datum.children.length === 0; + const nodeHalfWidth = parseSize(graph.getElementRenderStyle(idOf(datum)).size)[0] / 2; + const offset = (isLeaf ? 1 : -1) * (nodeHalfWidth + this.options.offset); + + const translate = `translate(${offset * Math.cos(radian)},${offset * Math.sin(radian)})`; + const rotate = `rotate(${isLeft ? rad2deg(radian) + 180 : rad2deg(radian)}deg)`; + + model.updateNodeData([ + { + id: idOf(datum), + style: { + labelTextAlign: isLeft === isLeaf ? 'right' : 'left', + labelTextBaseline: 'middle', + labelTransform: `${translate} ${rotate}`, + }, + }, + ]); + }); + + graph.draw(); + } +} diff --git a/packages/g6/src/types/element.ts b/packages/g6/src/types/element.ts index ad826e301d7..5c9713aff1a 100644 --- a/packages/g6/src/types/element.ts +++ b/packages/g6/src/types/element.ts @@ -29,13 +29,14 @@ export interface Node extends DisplayObject, ElementHooks, ElementMethods { * * Get the intersection point * @param point - 外部位置 | external position + * @param useExtendedLine - 是否使用延长线 | whether to use the extended line * @returns 交点位置 | intersection point * @remarks * 给定一个外部位置,返回当前节点与该位置的连边与节点的交点位置 * * Given an external position, return the intersection point of the edge between the current node and the position and the node */ - getIntersectPoint(point: Point): Point; + getIntersectPoint(point: Point, useExtendedLine?: boolean): Point; } /** diff --git a/packages/g6/src/utils/point.ts b/packages/g6/src/utils/point.ts index 9095ed3a8f2..2848c762ff3 100644 --- a/packages/g6/src/utils/point.ts +++ b/packages/g6/src/utils/point.ts @@ -132,6 +132,18 @@ export function isCollinear(p1: Point, p2: Point, p3: Point): boolean { return isLinesParallel([p1, p2], [p2, p3]); } +/** + * 计算一个点相对于另一个点的中心对称点 + * + * Calculate the center symmetric point of a point relative to another point + * @param p - 要计算的点 | the point to calculate + * @param center - 中心点 | the center point + * @returns 中心对称点 | the center symmetric point + */ +export function getSymmetricPoint(p: Point, center: Point): Point { + return [2 * center[0] - p[0], 2 * center[1] - p[1]]; +} + /** * 获取从多边形中心到给定点的连线与多边形边缘的交点 * @@ -140,6 +152,7 @@ export function isCollinear(p1: Point, p2: Point, p3: Point): boolean { * @param center - 多边形中心 | the center of the polygon * @param points - 多边形顶点 | the vertices of the polygon * @param isRelativePos - 顶点坐标是否相对中心点 | whether the vertex coordinates are relative to the center point + * @param useExtendedLine - 是否使用延长线 | whether to use the extended line * @returns 交点与相交线段 | intersection and intersecting line segment */ export function getPolygonIntersectPoint( @@ -147,6 +160,7 @@ export function getPolygonIntersectPoint( center: Point, points: Point[], isRelativePos = true, + useExtendedLine = false, ): { point: Point; line?: LineSegment } { for (let i = 0; i < points.length; i++) { let start = points[i]; @@ -157,7 +171,8 @@ export function getPolygonIntersectPoint( end = add(center, end); } - const intersect = getLinesIntersection([center, p], [start, end]); + const refP = useExtendedLine ? getSymmetricPoint(p, center) : p; + const intersect = getLinesIntersection([center, refP], [start, end]); if (intersect) { return { point: intersect, @@ -200,14 +215,15 @@ export function isPointInPolygon(point: Point, points: Point[], start?: number, } /** - * 获取从矩形中心到给定点的连线与矩形边缘的交点 + * 获取给定点到矩形中心的连线与矩形边缘的交点 * * Gets the intersection point between the line from the center of a rectangle to a given point and the rectangle's edge * @param p - 从矩形中心到矩形边缘的连线的外部点 | The point outside the rectangle from which the line to the rectangle's center is drawn * @param bbox - 矩形包围盒 | the bounding box of the rectangle + * @param useExtendedLine - 是否使用延长线 | whether to use the extended line * @returns 交点 | intersection */ -export function getRectIntersectPoint(p: Point, bbox: AABB): Point { +export function getRectIntersectPoint(p: Point, bbox: AABB, useExtendedLine = false): Point { const center = getXYByPlacement(bbox, 'center'); const corners = [ getXYByPlacement(bbox, 'left-top'), @@ -215,21 +231,23 @@ export function getRectIntersectPoint(p: Point, bbox: AABB): Point { getXYByPlacement(bbox, 'right-bottom'), getXYByPlacement(bbox, 'left-bottom'), ]; - return getPolygonIntersectPoint(p, center, corners, false).point; + return getPolygonIntersectPoint(p, center, corners, false, useExtendedLine).point; } /** - * 获取从椭圆中心到给定点的连线与椭圆边缘的交点 + * 获取给定点到椭圆中心的连线与椭圆边缘的交点 * * Gets the intersection point between the line from the center of an ellipse to a given point and the ellipse's edge * @param p - 从椭圆中心到椭圆边缘的连线的外部点 | The point outside the ellipse from which the line to the ellipse's center is drawn * The point outside the ellipse from which the line to the ellipse's center is drawn. * @param bbox - 椭圆包围盒 | the bounding box of the ellipse + * @param useExtendedLine - 是否使用延长线 | whether to use the extended line * @returns 交点 | intersection */ -export function getEllipseIntersectPoint(p: Point, bbox: AABB): Point { +export function getEllipseIntersectPoint(p: Point, bbox: AABB, useExtendedLine = false): Point { const center = bbox.center; - const vec = subtract(p, bbox.center); + const refP = useExtendedLine ? getSymmetricPoint(p, center) : p; + const vec = subtract(refP, bbox.center); const angle = Math.atan2(vec[1], vec[0]); if (isNaN(angle)) return center; diff --git a/packages/g6/src/utils/relation.ts b/packages/g6/src/utils/relation.ts index 6c8142e76c7..28e7e5eb007 100644 --- a/packages/g6/src/utils/relation.ts +++ b/packages/g6/src/utils/relation.ts @@ -1,5 +1,5 @@ import type { Graph } from '../runtime/graph'; -import type { ElementType, ID } from '../types'; +import type { EdgeDirection, ElementType, ID } from '../types'; import { idOf } from './id'; import { bfs } from './traverse'; @@ -17,18 +17,25 @@ import { bfs } from './traverse'; * @param elementType - 元素类型 | element type * @param elementId - 起始元素的 ID | start element ID * @param degree - 指定的度数 | the specified degree + * @param direction - 边的方向 | edge direction * @returns - 返回节点和边的 ID 数组 | Returns an array of node and edge IDs */ -export function getElementNthDegreeIds(graph: Graph, elementType: ElementType, elementId: ID, degree: number): ID[] { +export function getElementNthDegreeIds( + graph: Graph, + elementType: ElementType, + elementId: ID, + degree: number, + direction: EdgeDirection = 'both', +): ID[] { if (elementType === 'combo' || elementType === 'node') { - return getNodeNthDegreeIds(graph, elementId, degree); + return getNodeNthDegreeIds(graph, elementId, degree, direction); } const edgeData = graph.getEdgeData(elementId); if (!edgeData) return []; - const sourceRelations = getNodeNthDegreeIds(graph, edgeData.source, degree - 1); - const targetRelations = getNodeNthDegreeIds(graph, edgeData.target, degree - 1); + const sourceRelations = getNodeNthDegreeIds(graph, edgeData.source, degree - 1, direction); + const targetRelations = getNodeNthDegreeIds(graph, edgeData.target, degree - 1, direction); return Array.from(new Set([...sourceRelations, ...targetRelations, elementId])); } @@ -39,14 +46,20 @@ export function getElementNthDegreeIds(graph: Graph, elementType: ElementType, e * Get all elements IDs within n-degree relationship of the specified node * @remarks * 节点的 0 度关系是节点本身,1 度关系是节点的直接相邻节点和边,以此类推 - * + * @param direction * 0-degree relationship of a node is the node itself; 1-degree relationship is the node's neighboring nodes and related edges, etc * @param graph - 图实例 | graph instance * @param startNodeId - 起始节点的 ID | The ID of the starting node * @param degree - 指定的度数 | The specified degree + * @param direction - 边的方向 | The direction of the edge * @returns - 返回节点和边的 ID 数组 | Returns an array of node and edge IDs */ -export function getNodeNthDegreeIds(graph: Graph, startNodeId: ID, degree: number): ID[] { +export function getNodeNthDegreeIds( + graph: Graph, + startNodeId: ID, + degree: number, + direction: EdgeDirection = 'both', +): ID[] { const visitedNodes = new Set(); const visitedEdges = new Set(); const relations = new Set(); @@ -57,7 +70,7 @@ export function getNodeNthDegreeIds(graph: Graph, startNodeId: ID, degree: numbe if (depth > degree) return; relations.add(nodeId); - graph.getRelatedEdgesData(nodeId).forEach((edge) => { + graph.getRelatedEdgesData(nodeId, direction).forEach((edge) => { const edgeId = idOf(edge); if (!visitedEdges.has(edgeId) && depth < degree) { relations.add(edgeId); @@ -67,7 +80,7 @@ export function getNodeNthDegreeIds(graph: Graph, startNodeId: ID, degree: numbe }, (nodeId: ID) => { return graph - .getRelatedEdgesData(nodeId) + .getRelatedEdgesData(nodeId, direction) .map((edge) => (edge.source === nodeId ? edge.target : edge.source)) .filter((neighborNodeId) => { if (!visitedNodes.has(neighborNodeId)) { diff --git a/packages/g6/src/utils/vector.ts b/packages/g6/src/utils/vector.ts index a0543619176..112588b355c 100644 --- a/packages/g6/src/utils/vector.ts +++ b/packages/g6/src/utils/vector.ts @@ -206,3 +206,16 @@ export function toVector2(a: Vector2 | Vector3): Vector2 { export function toVector3(a: Vector2 | Vector3): Vector3 { return isVector2(a) ? [a[0], a[1], 0] : a; } + +/** + * 计算向量与 x 轴正方向的夹角(弧度制) + * + * The angle between the vector and the positive direction of the x-axis (radians) + * @param a - 向量 | The vector + * @returns 弧度值 | The angle in radians + */ +export function rad(a: Vector2 | Vector3): number { + const [x, y] = a; + if (!x && !y) return 0; + return Math.atan2(y, x); +} diff --git a/packages/site/examples/layout/compact-box/demo/basic.js b/packages/site/examples/layout/compact-box/demo/basic.js index 0452c1055db..d8158274d8d 100644 --- a/packages/site/examples/layout/compact-box/demo/basic.js +++ b/packages/site/examples/layout/compact-box/demo/basic.js @@ -1,5 +1,14 @@ import { Graph, treeToGraphData } from '@antv/g6'; +/** + * If the node is a leaf node + * @param {*} d - node data + * @returns {boolean} - whether the node is a leaf node + */ +function isLeafNode(d) { + return !d.children || d.children.length === 0; +} + fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json') .then((res) => res.json()) .then((data) => { @@ -10,17 +19,10 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.j behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'], node: { style: { - labelText: (data) => data.id, - labelPlacement: 'right', - labelMaxWidth: 200, - ports: [ - { - placement: 'right', - }, - { - placement: 'left', - }, - ], + labelText: (d) => d.id, + labelPlacement: (d) => (isLeafNode(d) ? 'right' : 'left'), + labelBackground: true, + ports: [{ placement: 'right' }, { placement: 'left' }], }, animation: { enter: false, diff --git a/packages/site/examples/layout/compact-box/demo/horizontal.js b/packages/site/examples/layout/compact-box/demo/horizontal.js deleted file mode 100644 index 95562d6545e..00000000000 --- a/packages/site/examples/layout/compact-box/demo/horizontal.js +++ /dev/null @@ -1,60 +0,0 @@ -import { Graph, treeToGraphData } from '@antv/g6'; - -fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json') - .then((res) => res.json()) - .then((data) => { - const graph = new Graph({ - container: 'container', - autoFit: 'view', - data: treeToGraphData(data), - behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'], - node: { - style: { - labelText: (data) => data.id, - labelPlacement: 'right', - labelMaxWidth: 200, - size: 12, - lineWidth: 1, - fill: '#fff', - ports: [ - { - placement: 'right', - }, - { - placement: 'left', - }, - ], - }, - animation: { - enter: false, - }, - }, - edge: { - type: 'cubic-horizontal', - animation: { - enter: false, - }, - }, - layout: { - type: 'compact-box', - direction: 'LR', - getId: function getId(d) { - return d.id; - }, - getHeight: function getHeight() { - return 16; - }, - getVGap: function getVGap() { - return 10; - }, - getHGap: function getHGap() { - return 100; - }, - getWidth: function getWidth(d) { - return d.id.length + 20; - }, - }, - }); - - graph.render(); - }); diff --git a/packages/site/examples/layout/compact-box/demo/meta.json b/packages/site/examples/layout/compact-box/demo/meta.json index 20a45c308d6..0b9211e4796 100644 --- a/packages/site/examples/layout/compact-box/demo/meta.json +++ b/packages/site/examples/layout/compact-box/demo/meta.json @@ -10,7 +10,7 @@ "zh": "紧凑树", "en": "CompactBox Layout" }, - "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*-FgIT7w4OXwAAAAAAAAAAAAADmJ7AQ/original" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Hu02Ro6UCegAAAAAAAAAAAAADmJ7AQ/original" }, { "filename": "vertical.js", @@ -18,15 +18,15 @@ "zh": "从上向下布局", "en": "Top to Bottom CompactBox" }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*KrAqTrFbNjMAAAAAAAAAAABkARQnAQ" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*YFO6Rb1hM24AAAAAAAAAAAAADmJ7AQ/original" }, { - "filename": "horizontal.js", + "filename": "radial.js", "title": { - "zh": "节点左对齐的紧凑树", - "en": "CompactBox with Left Align Nodes" + "zh": "径向布局", + "en": "Radial Layout" }, - "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*X26MRo25GKgAAAAAAAAAAAAADmJ7AQ/original" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*nwPmQqzJprwAAAAAAAAAAAAADmJ7AQ/original" } ] } diff --git a/packages/site/examples/layout/compact-box/demo/radial.js b/packages/site/examples/layout/compact-box/demo/radial.js new file mode 100644 index 00000000000..a4671e19476 --- /dev/null +++ b/packages/site/examples/layout/compact-box/demo/radial.js @@ -0,0 +1,43 @@ +import { Graph, treeToGraphData } from '@antv/g6'; + +fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json') + .then((res) => res.json()) + .then((data) => { + const graph = new Graph({ + container: 'container', + autoFit: 'view', + data: treeToGraphData(data), + behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'], + node: { + style: { + labelText: d => d.id, + labelBackground: true, + }, + animation: { + enter: false, + }, + }, + layout: { + type: 'compact-box', + radial: true, + direction: 'RL', + getId: function getId(d) { + return d.id; + }, + getHeight: () => { + return 26; + }, + getWidth: () => { + return 26; + }, + getVGap: () => { + return 20; + }, + getHGap: () => { + return 40; + }, + }, + }); + + graph.render(); + }); diff --git a/packages/site/examples/layout/compact-box/demo/vertical.js b/packages/site/examples/layout/compact-box/demo/vertical.js index 4acc9625828..7c065b6ba5f 100644 --- a/packages/site/examples/layout/compact-box/demo/vertical.js +++ b/packages/site/examples/layout/compact-box/demo/vertical.js @@ -1,5 +1,14 @@ import { Graph, treeToGraphData } from '@antv/g6'; +/** + * If the node is a leaf node + * @param {*} d - node data + * @returns {boolean} - whether the node is a leaf node + */ +function isLeafNode(d) { + return !d.children || d.children.length === 0; +} + fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json') .then((res) => res.json()) .then((data) => { @@ -7,24 +16,24 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.j container: 'container', autoFit: 'view', data: treeToGraphData(data), + behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'], node: { - style: { - labelText: (data) => data.id, - labelPlacement: 'right', - labelMaxWidth: 200, - transform: 'rotate(90deg)', - size: 26, - fill: '#EFF4FF', - lineWidth: 1, - stroke: '#5F95FF', - ports: [ - { - placement: 'bottom', - }, - { - placement: 'top', - }, - ], + style: d => { + const style = { + labelText: d.id, + labelPlacement: 'right', + labelOffsetX: 2, + labelBackground: true, + ports: [{ placement: 'top' }, { placement: 'bottom' }], + } + if (isLeafNode(d)) { + Object.assign(style, { + labelTransform: 'rotate(90deg) translate(18px)', + labelBaseline: 'center', + labelTextAlign: 'left', + }) + } + return style }, animation: { enter: false, @@ -39,9 +48,6 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.j layout: { type: 'compact-box', direction: 'TB', - getId: function getId(d) { - return d.id; - }, getHeight: function getHeight() { return 16; }, @@ -55,7 +61,6 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.j return 20; }, }, - behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'], }); graph.render(); diff --git a/packages/site/examples/layout/dendrogram/demo/basic.js b/packages/site/examples/layout/dendrogram/demo/basic.js index 531bf123fdc..de48be14c92 100644 --- a/packages/site/examples/layout/dendrogram/demo/basic.js +++ b/packages/site/examples/layout/dendrogram/demo/basic.js @@ -1,5 +1,14 @@ import { Graph, treeToGraphData } from '@antv/g6'; +/** + * If the node is a leaf node + * @param {*} d - node data + * @returns {boolean} - whether the node is a leaf node + */ +function isLeafNode(d) { + return !d.children || d.children.length === 0; +} + fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json') .then((res) => res.json()) .then((data) => { @@ -10,7 +19,8 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.j node: { style: { labelText: (d) => d.id, - labelPlacement: (model) => (model.children?.length ? 'left' : 'right'), + labelPlacement: (d) => (isLeafNode(d) ? 'right' : 'left'), + labelBackground: true, ports: [{ placement: 'right' }, { placement: 'left' }], }, animation: { diff --git a/packages/site/examples/layout/dendrogram/demo/meta.json b/packages/site/examples/layout/dendrogram/demo/meta.json index 9b3311bac15..d82a16c1406 100644 --- a/packages/site/examples/layout/dendrogram/demo/meta.json +++ b/packages/site/examples/layout/dendrogram/demo/meta.json @@ -10,7 +10,7 @@ "zh": "生态树", "en": "Dendrogram Layout" }, - "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*P-qOSoDNuckAAAAAAAAAAAAADmJ7AQ/original" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*7tDPSa-LHbAAAAAAAAAAAAAADmJ7AQ/original" }, { "filename": "vertical.js", @@ -18,7 +18,15 @@ "zh": "垂直布局", "en": "Vertical Layout" }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_f8c6a0/afts/img/A*nTKmRKkyUVUAAAAAAAAAAABkARQnAQ" + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*mp3BRbyBzCEAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "radial.js", + "title": { + "zh": "径向布局", + "en": "Radial Layout" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*nBIIR5yhTlQAAAAAAAAAAAAADmJ7AQ/original" } ] } diff --git a/packages/site/examples/layout/dendrogram/demo/radial.js b/packages/site/examples/layout/dendrogram/demo/radial.js new file mode 100644 index 00000000000..f70e0c19d27 --- /dev/null +++ b/packages/site/examples/layout/dendrogram/demo/radial.js @@ -0,0 +1,29 @@ +import { Graph, treeToGraphData } from '@antv/g6'; + + +fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json') + .then((res) => res.json()) + .then((data) => { + const graph = new Graph({ + container: 'container', + autoFit: 'view', + data: treeToGraphData(data), + behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element'], + node: { + style: { + labelText: d => d.id + }, + animation: { + enter: false, + }, + }, + layout: { + type: 'dendrogram', + radial: true, + nodeSep: 40, + rankSep: 140, + }, + }); + + graph.render(); + }); diff --git a/packages/site/examples/layout/dendrogram/demo/vertical.js b/packages/site/examples/layout/dendrogram/demo/vertical.js index c21f01f578f..784a68c7820 100644 --- a/packages/site/examples/layout/dendrogram/demo/vertical.js +++ b/packages/site/examples/layout/dendrogram/demo/vertical.js @@ -1,5 +1,14 @@ import { Graph, treeToGraphData } from '@antv/g6'; +/** + * If the node is a leaf node + * @param {*} d - node data + * @returns {boolean} - whether the node is a leaf node + */ +function isLeafNode(d) { + return !d.children || d.children.length === 0; +} + fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.json') .then((res) => res.json()) .then((data) => { @@ -7,20 +16,24 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.j container: 'container', autoFit: 'view', data: treeToGraphData(data), + behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'], node: { - style: (data) => { - const isLeaf = !data.children?.length; - return { - labelText: data.id, - labelWordWrap: true, - labelWordWrapWidth: 150, - labelDx: isLeaf ? 20 : 0, - labelDy: isLeaf ? 0 : 20, - labelTextAlign: isLeaf ? 'start' : 'center', - labelTextBaseline: 'middle', - labelTransform: isLeaf ? 'rotate(90deg)' : '', - ports: [{ placement: 'bottom' }, { placement: 'top' }], + style: (d) => { + const style = { + labelText: d.id, + labelPlacement: 'right', + labelOffsetX: 2, + labelBackground: true, + ports: [{ placement: 'top' }, { placement: 'bottom' }], }; + if (isLeafNode(d)) { + Object.assign(style, { + labelTransform: 'rotate(90deg) translate(18px)', + labelBaseline: 'center', + labelTextAlign: 'left', + }); + } + return style; }, animation: { enter: false, @@ -35,10 +48,9 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/algorithm-category.j layout: { type: 'dendrogram', direction: 'TB', // H / V / LR / RL / TB / BT - nodeSep: 40, - rankSep: 100, + nodeSep: 50, + rankSep: 120, }, - behaviors: ['drag-canvas', 'zoom-canvas', 'drag-element', 'collapse-expand'], }); graph.render(); diff --git a/packages/site/examples/scene-case/default/demo/meta.json b/packages/site/examples/scene-case/default/demo/meta.json index 1b25184bd9d..e6ca846de8f 100644 --- a/packages/site/examples/scene-case/default/demo/meta.json +++ b/packages/site/examples/scene-case/default/demo/meta.json @@ -36,6 +36,22 @@ }, "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*P39BR7WoI5oAAAAAAAAAAAAADmJ7AQ/original" }, + { + "filename": "radial-dendrogram.js", + "title": { + "zh": "径向生态树", + "en": "Radial Dendrogram" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*WRXHSrxqBYsAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "radial-compact-tree.js", + "title": { + "zh": "径向紧凑树", + "en": "Radial Compact Tree" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*uKYKSYJ6iMYAAAAAAAAAAAAADmJ7AQ/original" + }, { "filename": "decision-tree.js", "title": { diff --git a/packages/site/examples/scene-case/default/demo/radial-compact-tree.js b/packages/site/examples/scene-case/default/demo/radial-compact-tree.js new file mode 100644 index 00000000000..5adececf81c --- /dev/null +++ b/packages/site/examples/scene-case/default/demo/radial-compact-tree.js @@ -0,0 +1,66 @@ +import { Graph, treeToGraphData } from '@antv/g6'; + +fetch('https://assets.antv.antgroup.com/g6/flare.json') + .then((res) => res.json()) + .then((data) => { + const graph = new Graph({ + container: 'container', + autoFit: 'view', + data: treeToGraphData(data), + node: { + style: { + size: 20, + labelText: (d) => d.id, + labelBackground: true, + }, + state: { + active: { + fill: '#00C9C9', + }, + }, + }, + edge: { + type: 'cubic-radial', + state: { + active: { + lineWidth: 3, + stroke: '#009999', + }, + }, + }, + layout: [ + { + type: 'compact-box', + radial: true, + direction: 'RL', + getHeight: () => { + return 20; + }, + getWidth: () => { + return 20; + }, + getVGap: () => { + return 20; + }, + getHGap: () => { + return 80; + }, + }, + ], + behaviors: [ + 'drag-canvas', + 'zoom-canvas', + 'drag-element', + { + key: 'hover-activate', + type: 'hover-activate', + degree: 5, + direction: 'in', + inactiveState: 'inactive', + }, + ], + transforms: ['position-radial-labels'], + }); + + graph.render(); + }); diff --git a/packages/site/examples/scene-case/default/demo/radial-dendrogram.js b/packages/site/examples/scene-case/default/demo/radial-dendrogram.js new file mode 100644 index 00000000000..e6544f33cb4 --- /dev/null +++ b/packages/site/examples/scene-case/default/demo/radial-dendrogram.js @@ -0,0 +1,55 @@ +import { Graph, treeToGraphData } from '@antv/g6'; + +fetch('https://assets.antv.antgroup.com/g6/flare.json') + .then((res) => res.json()) + .then((data) => { + const graph = new Graph({ + container: 'container', + autoFit: 'view', + data: treeToGraphData(data), + node: { + style: { + size: 20, + labelText: (d) => d.id, + labelBackground: true, + }, + state: { + active: { + fill: '#00C9C9', + }, + }, + }, + edge: { + type: 'cubic-radial', + state: { + active: { + lineWidth: 3, + stroke: '#009999', + }, + }, + }, + layout: [ + { + type: 'dendrogram', + radial: true, + nodeSep: 30, + rankSep: 200, + }, + ], + behaviors: [ + 'drag-canvas', + 'zoom-canvas', + 'drag-element', + { + key: 'hover-activate', + type: 'hover-activate', + degree: 5, + direction: 'in', + inactiveState: 'inactive', + }, + ], + transforms: ['position-radial-labels'], + }); + + graph.render(); + });