diff --git a/.eslintrc.js b/.eslintrc.js index b1c06d01e61..074753e74ad 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -56,6 +56,7 @@ module.exports = { // TODO: rules below will be set to 2 in the future 'jsdoc/require-jsdoc': 1, 'jsdoc/check-access': 1, + 'jsdoc/valid-types': 0, /** * js plugin rules */ diff --git a/.vscode/settings.json b/.vscode/settings.json index d6dc2978962..57661de7e33 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -38,6 +38,7 @@ "beforestagelayout", "beforetransform", "beforeviewportanimate", + "betweenness", "Bezier", "bubblesets", "cancelviewportanimate", @@ -55,6 +56,7 @@ "GSHAPE", "mindmap", "onframe", + "pagerank", "Phong", "pointset", "Polyline", diff --git a/packages/g6/__tests__/dataset/language-tree.json b/packages/g6/__tests__/dataset/language-tree.json new file mode 100644 index 00000000000..e90ded5e339 --- /dev/null +++ b/packages/g6/__tests__/dataset/language-tree.json @@ -0,0 +1,956 @@ +{ + "nodes": [ + { + "id": "Proto Indo-European", + "children": [ + "Balto-Slavic", + "Germanic", + "Celtic", + "Italic", + "Hellenic", + "Anatolian", + "Indo-Iranian", + "Tocharian", + "Phrygian", + "Armenian", + "Albanian", + "Thracian" + ] + }, + { + "id": "Balto-Slavic", + "children": ["Baltic", "Slavic"] + }, + { + "id": "Baltic", + "children": ["Old Prussian", "Lithuanian", "Latvian"] + }, + { + "id": "Old Prussian" + }, + { + "id": "Lithuanian" + }, + { + "id": "Latvian" + }, + { + "id": "Slavic", + "children": ["East Slavic", "West Slavic", "South Slavic"] + }, + { + "id": "East Slavic", + "children": ["Bulgarian", "Old Church Slavonic", "Macedonian", "Serbo-Croatian", "Slovene"] + }, + { + "id": "Bulgarian" + }, + { + "id": "Old Church Slavonic" + }, + { + "id": "Macedonian" + }, + { + "id": "Serbo-Croatian" + }, + { + "id": "Slovene" + }, + { + "id": "West Slavic", + "children": ["Polish", "Slovak", "Czech", "Wendish"] + }, + { + "id": "Polish" + }, + { + "id": "Slovak" + }, + { + "id": "Czech" + }, + { + "id": "Wendish" + }, + { + "id": "South Slavic", + "children": ["Russian", "Ukrainian", "Belarusian", "Rusyn"] + }, + { + "id": "Russian" + }, + { + "id": "Ukrainian" + }, + { + "id": "Belarusian" + }, + { + "id": "Rusyn" + }, + { + "id": "Germanic", + "children": ["North Germanic", "West Germanic", "East Germanic"] + }, + { + "id": "North Germanic", + "children": ["Old Norse", "Old Swedish", "Old Danish"] + }, + { + "id": "Old Norse", + "children": ["Old Icelandic", "Old Norwegian", "Faroese"] + }, + { + "id": "Old Icelandic", + "children": ["Icelandic"] + }, + { + "id": "Icelandic" + }, + { + "id": "Old Norwegian", + "children": ["Middle Norwegian"] + }, + { + "id": "Middle Norwegian", + "children": ["Norwegian"] + }, + { + "id": "Norwegian" + }, + { + "id": "Faroese" + }, + { + "id": "Old Swedish", + "children": ["Middle Swedish"] + }, + { + "id": "Middle Swedish", + "children": ["Swedish"] + }, + { + "id": "Swedish" + }, + { + "id": "Old Danish", + "children": ["Middle Danish"] + }, + { + "id": "Middle Danish", + "children": ["Danish"] + }, + { + "id": "Danish" + }, + { + "id": "West Germanic", + "children": ["Old English", "Old Frisian", "Old Dutch", "Old Low German", "Old High German"] + }, + { + "id": "Old English", + "children": ["Middle English"] + }, + { + "id": "Middle English", + "children": ["English"] + }, + { + "id": "English" + }, + { + "id": "Old Frisian", + "children": ["Frisian"] + }, + { + "id": "Frisian" + }, + { + "id": "Old Dutch", + "children": ["Middle Dutch"] + }, + { + "id": "Middle Dutch", + "children": ["Hollandic", "Flemish", "Dutch", "Limburgish", "Brabantian", "Rhinelandic"] + }, + { + "id": "Hollandic" + }, + { + "id": "Flemish" + }, + { + "id": "Dutch" + }, + { + "id": "Limburgish" + }, + { + "id": "Brabantian" + }, + { + "id": "Rhinelandic" + }, + { + "id": "Old Low German", + "children": ["Middle Low German"] + }, + { + "id": "Middle Low German", + "children": ["Low German"] + }, + { + "id": "Low German" + }, + { + "id": "Old High German", + "children": ["Middle High German"] + }, + { + "id": "Middle High German", + "children": ["(High) German", "Yiddish"] + }, + { + "id": "(High) German" + }, + { + "id": "Yiddish" + }, + { + "id": "East Germanic", + "children": ["Gothic"] + }, + { + "id": "Gothic" + }, + { + "id": "Celtic", + "children": ["Brythonic", "Goidelic"] + }, + { + "id": "Brythonic", + "children": ["Welsh", "Breton", "Cornish", "Cuymbric"] + }, + { + "id": "Welsh" + }, + { + "id": "Breton" + }, + { + "id": "Cornish" + }, + { + "id": "Cuymbric" + }, + { + "id": "Goidelic", + "children": ["Modern Irish", "Scottish Gaelic", "Manx"] + }, + { + "id": "Modern Irish" + }, + { + "id": "Scottish Gaelic" + }, + { + "id": "Manx" + }, + { + "id": "Italic", + "children": ["Osco-Umbrian", "Latino-Faliscan"] + }, + { + "id": "Osco-Umbrian", + "children": ["Umbrian", "Oscan"] + }, + { + "id": "Umbrian" + }, + { + "id": "Oscan" + }, + { + "id": "Latino-Faliscan", + "children": ["Latin", "Faliscan"] + }, + { + "id": "Latin", + "children": [ + "Portugese", + "Spanish", + "French", + "Romanian", + "Italian", + "Catalan", + "Franco-Provençal", + "Rhaeto-Romance" + ] + }, + { + "id": "Portugese" + }, + { + "id": "Spanish" + }, + { + "id": "French" + }, + { + "id": "Romanian" + }, + { + "id": "Italian" + }, + { + "id": "Catalan" + }, + { + "id": "Franco-Provençal" + }, + { + "id": "Rhaeto-Romance" + }, + { + "id": "Faliscan" + }, + { + "id": "Hellenic", + "children": ["Greek"] + }, + { + "id": "Greek" + }, + { + "id": "Anatolian", + "children": ["Hittite", "Palaic", "Luwic", "Lydian"] + }, + { + "id": "Hittite" + }, + { + "id": "Palaic" + }, + { + "id": "Luwic" + }, + { + "id": "Lydian" + }, + { + "id": "Indo-Iranian", + "children": ["Dardic", "Indic", "Iranian"] + }, + { + "id": "Dardic", + "children": ["Dard"] + }, + { + "id": "Dard" + }, + { + "id": "Indic", + "children": ["Sanskrit"] + }, + { + "id": "Sanskrit", + "children": [ + "Sindhi", + "Romani", + "Urdu", + "Hindi", + "Bihari", + "Assamese", + "Bengali", + "Marathi", + "Gujarati", + "Punjabi", + "Sinhalese" + ] + }, + { + "id": "Sindhi" + }, + { + "id": "Romani" + }, + { + "id": "Urdu" + }, + { + "id": "Hindi" + }, + { + "id": "Bihari" + }, + { + "id": "Assamese" + }, + { + "id": "Bengali" + }, + { + "id": "Marathi" + }, + { + "id": "Gujarati" + }, + { + "id": "Punjabi" + }, + { + "id": "Sinhalese" + }, + { + "id": "Iranian", + "children": ["Old Persian", "Balochi", "Kurdish", "Pashto", "Sogdian"] + }, + { + "id": "Old Persian", + "children": ["Middle Persian", "Pahlavi"] + }, + { + "id": "Middle Persian", + "children": ["Persian"] + }, + { + "id": "Persian" + }, + { + "id": "Pahlavi" + }, + { + "id": "Balochi" + }, + { + "id": "Kurdish" + }, + { + "id": "Pashto" + }, + { + "id": "Sogdian" + }, + { + "id": "Tocharian", + "children": ["Tocharian A", "Tocharian B"] + }, + { + "id": "Tocharian A" + }, + { + "id": "Tocharian B" + }, + { + "id": "Phrygian" + }, + { + "id": "Armenian" + }, + { + "id": "Albanian" + }, + { + "id": "Thracian" + } + ], + "edges": [ + { + "source": "Proto Indo-European", + "target": "Balto-Slavic" + }, + { + "source": "Proto Indo-European", + "target": "Germanic" + }, + { + "source": "Proto Indo-European", + "target": "Celtic" + }, + { + "source": "Proto Indo-European", + "target": "Italic" + }, + { + "source": "Proto Indo-European", + "target": "Hellenic" + }, + { + "source": "Proto Indo-European", + "target": "Anatolian" + }, + { + "source": "Proto Indo-European", + "target": "Indo-Iranian" + }, + { + "source": "Proto Indo-European", + "target": "Tocharian" + }, + { + "source": "Proto Indo-European", + "target": "Phrygian" + }, + { + "source": "Proto Indo-European", + "target": "Armenian" + }, + { + "source": "Proto Indo-European", + "target": "Albanian" + }, + { + "source": "Proto Indo-European", + "target": "Thracian" + }, + { + "source": "Balto-Slavic", + "target": "Baltic" + }, + { + "source": "Balto-Slavic", + "target": "Slavic" + }, + { + "source": "Baltic", + "target": "Old Prussian" + }, + { + "source": "Baltic", + "target": "Lithuanian" + }, + { + "source": "Baltic", + "target": "Latvian" + }, + { + "source": "Slavic", + "target": "East Slavic" + }, + { + "source": "Slavic", + "target": "West Slavic" + }, + { + "source": "Slavic", + "target": "South Slavic" + }, + { + "source": "East Slavic", + "target": "Bulgarian" + }, + { + "source": "East Slavic", + "target": "Old Church Slavonic" + }, + { + "source": "East Slavic", + "target": "Macedonian" + }, + { + "source": "East Slavic", + "target": "Serbo-Croatian" + }, + { + "source": "East Slavic", + "target": "Slovene" + }, + { + "source": "West Slavic", + "target": "Polish" + }, + { + "source": "West Slavic", + "target": "Slovak" + }, + { + "source": "West Slavic", + "target": "Czech" + }, + { + "source": "West Slavic", + "target": "Wendish" + }, + { + "source": "South Slavic", + "target": "Russian" + }, + { + "source": "South Slavic", + "target": "Ukrainian" + }, + { + "source": "South Slavic", + "target": "Belarusian" + }, + { + "source": "South Slavic", + "target": "Rusyn" + }, + { + "source": "Germanic", + "target": "North Germanic" + }, + { + "source": "Germanic", + "target": "West Germanic" + }, + { + "source": "Germanic", + "target": "East Germanic" + }, + { + "source": "North Germanic", + "target": "Old Norse" + }, + { + "source": "North Germanic", + "target": "Old Swedish" + }, + { + "source": "North Germanic", + "target": "Old Danish" + }, + { + "source": "Old Norse", + "target": "Old Icelandic" + }, + { + "source": "Old Norse", + "target": "Old Norwegian" + }, + { + "source": "Old Norse", + "target": "Faroese" + }, + { + "source": "Old Icelandic", + "target": "Icelandic" + }, + { + "source": "Old Norwegian", + "target": "Middle Norwegian" + }, + { + "source": "Middle Norwegian", + "target": "Norwegian" + }, + { + "source": "Old Swedish", + "target": "Middle Swedish" + }, + { + "source": "Middle Swedish", + "target": "Swedish" + }, + { + "source": "Old Danish", + "target": "Middle Danish" + }, + { + "source": "Middle Danish", + "target": "Danish" + }, + { + "source": "West Germanic", + "target": "Old English" + }, + { + "source": "West Germanic", + "target": "Old Frisian" + }, + { + "source": "West Germanic", + "target": "Old Dutch" + }, + { + "source": "West Germanic", + "target": "Old Low German" + }, + { + "source": "West Germanic", + "target": "Old High German" + }, + { + "source": "Old English", + "target": "Middle English" + }, + { + "source": "Middle English", + "target": "English" + }, + { + "source": "Old Frisian", + "target": "Frisian" + }, + { + "source": "Old Dutch", + "target": "Middle Dutch" + }, + { + "source": "Middle Dutch", + "target": "Hollandic" + }, + { + "source": "Middle Dutch", + "target": "Flemish" + }, + { + "source": "Middle Dutch", + "target": "Dutch" + }, + { + "source": "Middle Dutch", + "target": "Limburgish" + }, + { + "source": "Middle Dutch", + "target": "Brabantian" + }, + { + "source": "Middle Dutch", + "target": "Rhinelandic" + }, + { + "source": "Old Low German", + "target": "Middle Low German" + }, + { + "source": "Middle Low German", + "target": "Low German" + }, + { + "source": "Old High German", + "target": "Middle High German" + }, + { + "source": "Middle High German", + "target": "(High) German" + }, + { + "source": "Middle High German", + "target": "Yiddish" + }, + { + "source": "East Germanic", + "target": "Gothic" + }, + { + "source": "Celtic", + "target": "Brythonic" + }, + { + "source": "Celtic", + "target": "Goidelic" + }, + { + "source": "Brythonic", + "target": "Welsh" + }, + { + "source": "Brythonic", + "target": "Breton" + }, + { + "source": "Brythonic", + "target": "Cornish" + }, + { + "source": "Brythonic", + "target": "Cuymbric" + }, + { + "source": "Goidelic", + "target": "Modern Irish" + }, + { + "source": "Goidelic", + "target": "Scottish Gaelic" + }, + { + "source": "Goidelic", + "target": "Manx" + }, + { + "source": "Italic", + "target": "Osco-Umbrian" + }, + { + "source": "Italic", + "target": "Latino-Faliscan" + }, + { + "source": "Osco-Umbrian", + "target": "Umbrian" + }, + { + "source": "Osco-Umbrian", + "target": "Oscan" + }, + { + "source": "Latino-Faliscan", + "target": "Latin" + }, + { + "source": "Latino-Faliscan", + "target": "Faliscan" + }, + { + "source": "Latin", + "target": "Portugese" + }, + { + "source": "Latin", + "target": "Spanish" + }, + { + "source": "Latin", + "target": "French" + }, + { + "source": "Latin", + "target": "Romanian" + }, + { + "source": "Latin", + "target": "Italian" + }, + { + "source": "Latin", + "target": "Catalan" + }, + { + "source": "Latin", + "target": "Franco-Provençal" + }, + { + "source": "Latin", + "target": "Rhaeto-Romance" + }, + { + "source": "Hellenic", + "target": "Greek" + }, + { + "source": "Anatolian", + "target": "Hittite" + }, + { + "source": "Anatolian", + "target": "Palaic" + }, + { + "source": "Anatolian", + "target": "Luwic" + }, + { + "source": "Anatolian", + "target": "Lydian" + }, + { + "source": "Indo-Iranian", + "target": "Dardic" + }, + { + "source": "Indo-Iranian", + "target": "Indic" + }, + { + "source": "Indo-Iranian", + "target": "Iranian" + }, + { + "source": "Dardic", + "target": "Dard" + }, + { + "source": "Indic", + "target": "Sanskrit" + }, + { + "source": "Sanskrit", + "target": "Sindhi" + }, + { + "source": "Sanskrit", + "target": "Romani" + }, + { + "source": "Sanskrit", + "target": "Urdu" + }, + { + "source": "Sanskrit", + "target": "Hindi" + }, + { + "source": "Sanskrit", + "target": "Bihari" + }, + { + "source": "Sanskrit", + "target": "Assamese" + }, + { + "source": "Sanskrit", + "target": "Bengali" + }, + { + "source": "Sanskrit", + "target": "Marathi" + }, + { + "source": "Sanskrit", + "target": "Gujarati" + }, + { + "source": "Sanskrit", + "target": "Punjabi" + }, + { + "source": "Sanskrit", + "target": "Sinhalese" + }, + { + "source": "Iranian", + "target": "Old Persian" + }, + { + "source": "Iranian", + "target": "Balochi" + }, + { + "source": "Iranian", + "target": "Kurdish" + }, + { + "source": "Iranian", + "target": "Pashto" + }, + { + "source": "Iranian", + "target": "Sogdian" + }, + { + "source": "Old Persian", + "target": "Middle Persian" + }, + { + "source": "Old Persian", + "target": "Pahlavi" + }, + { + "source": "Middle Persian", + "target": "Persian" + }, + { + "source": "Tocharian", + "target": "Tocharian A" + }, + { + "source": "Tocharian", + "target": "Tocharian B" + } + ] +} diff --git a/packages/g6/__tests__/demos/case-language-tree.ts b/packages/g6/__tests__/demos/case-language-tree.ts new file mode 100644 index 00000000000..ee224ad6b70 --- /dev/null +++ b/packages/g6/__tests__/demos/case-language-tree.ts @@ -0,0 +1,55 @@ +import { labelPropagation } from '@antv/algorithm'; +import { Graph, NodeData } from '@antv/g6'; +import data from '../dataset/language-tree.json'; + +export const caseLanguageTree: TestCase = async (context) => { + const size = (node: NodeData) => Math.max(...(node.style?.size as [number, number, number])); + + const graph = new Graph({ + ...context, + autoFit: 'view', + data: { + ...data, + nodes: labelPropagation(data).clusters.flatMap((cluster) => cluster.nodes), + }, + node: { + style: { + labelText: (d) => d.id, + labelBackground: true, + iconSrc: 'https://gw.alipayobjects.com/zos/basement_prod/012bcf4f-423b-4922-8c24-32a89f8c41ce.svg', + }, + palette: { + field: (d) => d.clusterId, + }, + }, + layout: { + type: 'd3-force', + link: { + distance: (edge) => size(edge.source) + size(edge.target), + }, + collide: { + radius: (node: NodeData) => size(node) + 1, + }, + manyBody: { + strength: (node: NodeData) => -4 * size(node), + }, + animation: false, + }, + transforms: ['map-node-size'], + behaviors: [ + 'drag-canvas', + 'zoom-canvas', + { + key: 'hover-activate', + type: 'hover-activate', + degree: 1, + inactiveState: 'inactive', + }, + ], + animation: false, + }); + + await graph.render(); + + return graph; +}; diff --git a/packages/g6/__tests__/demos/index.ts b/packages/g6/__tests__/demos/index.ts index 6175dbebb71..2ec05e8963e 100644 --- a/packages/g6/__tests__/demos/index.ts +++ b/packages/g6/__tests__/demos/index.ts @@ -23,6 +23,7 @@ export { bugTooltipResize } from './bug-tooltip-resize'; export { canvasCursor } from './canvas-cursor'; export { caseFundFlow } from './case-fund-flow'; export { caseIndentedTree } from './case-indented-tree'; +export { caseLanguageTree } from './case-language-tree'; export { caseMindmap } from './case-mindmap'; export { caseOrgChart } from './case-org-chart'; export { caseRadialDendrogram } from './case-radial-dendrogram'; @@ -136,6 +137,7 @@ export { pluginTooltip } from './plugin-tooltip'; export { pluginWatermark } from './plugin-watermark'; export { pluginWatermarkImage } from './plugin-watermark-image'; export { theme } from './theme'; +export { transformMapNodeSize } from './transform-map-node-size'; export { transformPlaceRadialLabels } from './transform-place-radial-labels'; export { transformProcessParallelEdges } from './transform-process-parallel-edges'; export { viewportFit } from './viewport-fit'; diff --git a/packages/g6/__tests__/demos/transform-map-node-size.ts b/packages/g6/__tests__/demos/transform-map-node-size.ts new file mode 100644 index 00000000000..64dd4d28795 --- /dev/null +++ b/packages/g6/__tests__/demos/transform-map-node-size.ts @@ -0,0 +1,47 @@ +import { Graph } from '@antv/g6'; + +export const transformMapNodeSize: TestCase = async (context) => { + const graph = new Graph({ + ...context, + data: { + nodes: [{ id: 'node-1' }, { id: 'node-2' }, { id: 'node-3' }, { id: 'node-4' }, { id: 'node-5' }], + edges: [ + { source: 'node-1', target: 'node-2' }, + { source: 'node-1', target: 'node-3' }, + { source: 'node-1', target: 'node-4' }, + { source: 'node-4', target: 'node-5' }, + ], + }, + node: { + style: { + labelText: (d) => d.id, + }, + }, + layout: { + type: 'grid', + }, + transforms: [ + { + key: 'map-node-size', + type: 'map-node-size', + }, + ], + animation: false, + }); + + await graph.render(); + + const config = { 'centrality.type': 'eigenvector' }; + + transformMapNodeSize.form = (panel) => [ + panel + .add(config, 'centrality.type', ['degree', 'betweenness', 'closeness', 'eigenvector', 'pagerank']) + .name('Centrality Type') + .onChange((type: string) => { + graph.updateTransform({ key: 'map-node-size', centrality: { type } }); + graph.draw(); + }), + ]; + + return graph; +}; diff --git a/packages/g6/__tests__/snapshots/transforms/transform-map-node-size/default.svg b/packages/g6/__tests__/snapshots/transforms/transform-map-node-size/default.svg new file mode 100644 index 00000000000..5a9a1397c7e --- /dev/null +++ b/packages/g6/__tests__/snapshots/transforms/transform-map-node-size/default.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + node-1 + + + + + + + + + + + + node-2 + + + + + + + + + + + + node-3 + + + + + + + + + + + + node-4 + + + + + + + + + + + + node-5 + + + + + + + + \ No newline at end of file diff --git a/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts b/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts new file mode 100644 index 00000000000..ca9abf50505 --- /dev/null +++ b/packages/g6/__tests__/unit/transforms/transform-map-node-size.spec.ts @@ -0,0 +1,106 @@ +import type { Graph } from '@/src'; +import { transformMapNodeSize } from '@@/demos'; +import { createDemoGraph } from '@@/utils'; + +const nodeSizeMap = (graph: Graph) => + Object.fromEntries(graph.getNodeData().map((node) => [node.id, node.style?.size])); + +describe('transform map node size', () => { + let graph: Graph; + + beforeAll(async () => { + graph = await createDemoGraph(transformMapNodeSize, { animation: false }); + }); + + afterAll(() => { + graph.destroy(); + }); + + it('centrality', async () => { + await expect(graph).toMatchSnapshot(__filename); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'degree' }, + minSize: 10, + maxSize: 40, + scale: 'linear', + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [40, 40, 40], + 'node-2': [10, 10, 10], + 'node-3': [10, 10, 10], + 'node-4': [25, 25, 25], + 'node-5': [10, 10, 10], + }); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'betweenness' }, + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [40, 40, 40], + 'node-2': [10, 10, 10], + 'node-3': [10, 10, 10], + 'node-4': [28, 28, 28], + 'node-5': [10, 10, 10], + }); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'pagerank' }, + }); + await graph.render(); + + expect(nodeSizeMap(graph)['node-1']).toEqual([10, 10, 10]); + expect(nodeSizeMap(graph)['node-5']).toEqual([40, 40, 40]); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'eigenvector' }, + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [40, 40, 40], + 'node-2': [10, 10, 10], + 'node-3': [10, 10, 10], + 'node-4': [25, 25, 25], + 'node-5': [10, 10, 10], + }); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'eigenvector', directed: true }, + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [40, 40, 40], + 'node-2': [10, 10, 10], + 'node-3': [10, 10, 10], + 'node-4': [20, 20, 20], + 'node-5': [10, 10, 10], + }); + + graph.updateTransform({ + key: 'map-node-size', + centrality: { type: 'closeness' }, + minSize: 10, + maxSize: 50, + }); + await graph.render(); + + expect(nodeSizeMap(graph)).toEqual({ + 'node-1': [50, 50, 50], + 'node-2': [16.25, 16.25, 16.25], + 'node-3': [16.25, 16.25, 16.25], + 'node-4': [35, 35, 35], + 'node-5': [10, 10, 10], + }); + }); +}); diff --git a/packages/g6/__tests__/unit/utils/scale.spec.ts b/packages/g6/__tests__/unit/utils/scale.spec.ts new file mode 100644 index 00000000000..e9e9269c4cc --- /dev/null +++ b/packages/g6/__tests__/unit/utils/scale.spec.ts @@ -0,0 +1,29 @@ +import { linear, log, pow, sqrt } from '@/src/utils/scale'; + +describe('scale', () => { + it('linear', () => { + expect(linear(0.2, [0, 1], [0, 0])).toEqual(0); + expect(linear(0, [0, 1], [0, 100])).toEqual(0); + expect(linear(0.5, [0, 1], [0, 100])).toEqual(50); + expect(linear(1, [0, 1], [0, 100])).toEqual(100); + }); + + it('log', () => { + expect(log(0, [0, 1], [0, 100])).toEqual(0); + expect(log(0.5, [0, 1], [0, 100])).toEqual((Math.log(1.5) / Math.log(2)) * 100); + expect(log(1, [0, 1], [0, 100])).toEqual(100); + }); + + it('pow', () => { + expect(pow(0, [0, 1], [0, 100], 2)).toEqual(0); + expect(pow(0.5, [0, 1], [0, 100])).toEqual(25); + expect(pow(0.5, [0, 1], [0, 100], 2)).toEqual(25); + expect(pow(1, [0, 1], [0, 100], 2)).toEqual(100); + }); + + it('sqrt', () => { + expect(sqrt(0, [0, 1], [0, 100])).toEqual(0); + expect(sqrt(0.25, [0, 1], [0, 100])).toEqual(50); + expect(sqrt(1, [0, 1], [0, 100])).toEqual(100); + }); +}); diff --git a/packages/g6/package.json b/packages/g6/package.json index 10afe1f714c..30b6285669f 100644 --- a/packages/g6/package.json +++ b/packages/g6/package.json @@ -57,6 +57,7 @@ "version": "node ./scripts/version.mjs" }, "dependencies": { + "@antv/algorithm": "^0.1.26", "@antv/component": "^2.0.4", "@antv/event-emitter": "^0.1.3", "@antv/g": "^6.0.13", @@ -64,7 +65,7 @@ "@antv/g-plugin-dragndrop": "^2.0.9", "@antv/graphlib": "^2.0.3", "@antv/hierarchy": "^0.6.13", - "@antv/layout": "^1.2.14-beta.7", + "@antv/layout": "^1.2.14-beta.8", "@antv/util": "^3.3.8", "bubblesets-js": "^2.3.3", "hull.js": "^1.0.6" @@ -87,11 +88,11 @@ "limit-size": [ { "gzip": true, - "limit": "300 Kb", + "limit": "310 Kb", "path": "dist/g6.min.js" }, { - "limit": "1 Mb", + "limit": "1.1 Mb", "path": "dist/g6.min.js" } ] diff --git a/packages/g6/src/exports.ts b/packages/g6/src/exports.ts index 9c6531777d1..3aa6596bbb8 100644 --- a/packages/g6/src/exports.ts +++ b/packages/g6/src/exports.ts @@ -95,7 +95,7 @@ export { export { getExtension, getExtensions } from './registry/get'; export { register } from './registry/register'; export { Graph } from './runtime/graph'; -export { BaseTransform, PlaceRadialLabels, ProcessParallelEdges } from './transforms'; +export { BaseTransform, MapNodeSize, PlaceRadialLabels, ProcessParallelEdges } from './transforms'; export { isCollapsed } from './utils/collapsibility'; export { idOf } from './utils/id'; export { invokeLayoutMethod } from './utils/layout'; @@ -219,7 +219,12 @@ 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, PlaceRadialLabelsOptions, ProcessParallelEdgesOptions } from './transforms'; +export type { + BaseTransformOptions, + MapNodeSizeOptions, + PlaceRadialLabelsOptions, + 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 072f88c5a07..91c5b99d55d 100644 --- a/packages/g6/src/registry/build-in.ts +++ b/packages/g6/src/registry/build-in.ts @@ -93,6 +93,7 @@ import { CollapseExpandCombo, CollapseExpandNode, GetEdgeActualEnds, + MapNodeSize, PlaceRadialLabels, ProcessParallelEdges, UpdateRelatedEdge, @@ -204,13 +205,14 @@ const BUILT_IN_EXTENSIONS: ExtensionRegistry = { watermark: Watermark, }, transform: { - 'update-related-edges': UpdateRelatedEdge, 'arrange-draw-order': ArrangeDrawOrder, 'collapse-expand-combo': CollapseExpandCombo, 'collapse-expand-node': CollapseExpandNode, - 'process-parallel-edges': ProcessParallelEdges, 'get-edge-actual-ends': GetEdgeActualEnds, + 'map-node-size': MapNodeSize, 'place-radial-labels': PlaceRadialLabels, + 'process-parallel-edges': ProcessParallelEdges, + 'update-related-edges': UpdateRelatedEdge, }, shape: { circle: GCircle, diff --git a/packages/g6/src/transforms/index.ts b/packages/g6/src/transforms/index.ts index 9e861f8268e..283a8bd9a4a 100644 --- a/packages/g6/src/transforms/index.ts +++ b/packages/g6/src/transforms/index.ts @@ -3,10 +3,12 @@ export { BaseTransform } from './base-transform'; export { CollapseExpandCombo } from './collapse-expand-combo'; export { CollapseExpandNode } from './collapse-expand-node'; export { GetEdgeActualEnds } from './get-edge-actual-ends'; +export { MapNodeSize } from './map-node-size'; export { PlaceRadialLabels } from './place-radial-labels'; export { ProcessParallelEdges } from './process-parallel-edges'; export { UpdateRelatedEdge } from './update-related-edge'; export type { BaseTransformOptions } from './base-transform'; +export type { MapNodeSizeOptions } from './map-node-size'; export type { PlaceRadialLabelsOptions } from './place-radial-labels'; export type { ProcessParallelEdgesOptions } from './process-parallel-edges'; diff --git a/packages/g6/src/transforms/map-node-size.ts b/packages/g6/src/transforms/map-node-size.ts new file mode 100644 index 00000000000..16cf01b7556 --- /dev/null +++ b/packages/g6/src/transforms/map-node-size.ts @@ -0,0 +1,360 @@ +import { findShortestPath, pageRank } from '@antv/algorithm'; +import { deepMix } from '@antv/util'; +import type { RuntimeContext } from '../runtime/types'; +import type { GraphData } from '../spec'; +import type { EdgeDirection, ID, Node, Size, STDSize } from '../types'; +import { idOf } from '../utils/id'; +import { linear, log, pow, sqrt } from '../utils/scale'; +import { parseSize } from '../utils/size'; +import { reassignTo } from '../utils/transform'; +import type { BaseTransformOptions } from './base-transform'; +import { BaseTransform } from './base-transform'; +import type { DrawData } from './types'; + +export interface MapNodeSizeOptions extends BaseTransformOptions { + /** + * 节点中心性的度量方法 + * - `'degree'`:度中心性,通过节点的度数(连接的边的数量)来衡量其重要性。度中心性高的节点通常具有较多的直接连接,在网络中可能扮演着重要的角色 + * - `'betweenness'`:介数中心性,通过节点在所有最短路径中出现的次数来衡量其重要性。介数中心性高的节点通常在网络中起到桥梁作用,控制着信息的流动 + * - `'closeness'`:接近中心性,通过节点到其他所有节点的最短路径长度总和的倒数来衡量其重要性。接近中心性高的节点通常能够更快地到达网络中的其他节点 + * - `'eigenvector'`:特征向量中心性,通过节点与其他中心节点的连接程度来衡量其重要性。特征向量中心性高的节点通常连接着其他重要节点 + * - `'pagerank'`:PageRank 中心性,通过节点被其他节点引用的次数来衡量其重要性,常用于有向图。PageRank 中心性高的节点通常在网络中具有较高的影响力,类似于网页排名算法 + * - 自定义中心性计算方法:`(graphData: GraphData) => Map`,其中 `graphData` 为图数据,`Map` 为节点 ID 到中心性值的映射 + * + * The method of measuring the node centrality + * - `'degree'`: Degree centrality, measures centrality by the degree (number of connected edges) of a node. Nodes with high degree centrality usually have more direct connections and may play important roles in the network + * - `'betweenness'`: Betweenness centrality, measures centrality by the number of times a node appears in all shortest paths. Nodes with high betweenness centrality usually act as bridges in the network, controlling the flow of information + * - `'closeness'`: Closeness centrality, measures centrality by the reciprocal of the average shortest path length from a node to all other nodes. Nodes with high closeness centrality usually can reach other nodes in the network more quickly + * - `'eigenvector'`: Eigenvector centrality, measures centrality by the degree of connection between a node and other central nodes. Nodes with high eigenvector centrality usually connect to other important nodes + * - `'pagerank'`: PageRank centrality, measures centrality by the number of times a node is referenced by other nodes, commonly used in directed graphs. Nodes with high PageRank centrality usually have high influence in the network, similar to the page ranking algorithm + * - Custom centrality calculation method: `(graphData: GraphData) => Map`, where `graphData` is the graph data, and `Map` is the mapping from node ID to centrality value + * @defaultValue { type: 'eigenvector' } + */ + centrality?: + | { type: 'degree'; direction?: EdgeDirection } + | { type: 'betweenness'; directed?: boolean; weightPropertyName?: string } + | { type: 'closeness'; directed?: boolean; weightPropertyName?: string } + | { type: 'eigenvector'; directed?: boolean } + | { type: 'pagerank'; epsilon?: number; linkProb?: number } + | ((graphData: GraphData) => Map); + /** + * 节点最大尺寸 + * + * The maximum size of the node + * @defaultValue 80 + */ + maxSize?: Size; + /** + * 节点最小尺寸 + * + * The minimum size of the node + * @defaultValue 20 + */ + minSize?: Size; + /** + * 插值函数,用于将节点中心性映射到节点大小 + * - `'linear'`:线性插值函数,将一个值从一个范围线性映射到另一个范围,常用于处理中心性值的差异较小的情况 + * - `'log'`:对数插值函数,将一个值从一个范围对数映射到另一个范围,常用于处理中心性值的差异较大的情况 + * - `'pow'`:幂律插值函数,将一个值从一个范围幂律映射到另一个范围,常用于处理中心性值的差异较大的情况 + * - `'sqrt'`:平方根插值函数,将一个值从一个范围平方根映射到另一个范围,常用于处理中心性值的差异较大的情况 + * - 自定义插值函数:`(value: number, domain: [number, number], range: [number, number]) => number`,其中 `value` 为需要映射的值,`domain` 为输入值的范围,`range` 为输出值的范围 + * + * Scale type + * - `'linear'`: Linear scale, maps a value from one range to another range linearly, commonly used for cases where the difference in centrality values is small + * - `'log'`: Logarithmic scale, maps a value from one range to another range logarithmically, commonly used for cases where the difference in centrality values is large + * - `'pow'`: Power-law scale, maps a value from one range to another range using power law, commonly used for cases where the difference in centrality values is large + * - `'sqrt'`: Square root scale, maps a value from one range to another range using square root, commonly used for cases where the difference in centrality values is large + * - Custom scale: `(value: number, domain: [number, number], range: [number, number]) => number`,where `value` is the value to be mapped, `domain` is the input range, and `range` is the output range + * @defaultValue 'log' + */ + scale?: + | 'linear' + | 'log' + | 'pow' + | 'sqrt' + | ((value: number, domain: [number, number], range: [number, number]) => number); +} + +type CentralityResult = Map; + +/** + * 根据节点中心性调整节点的大小 + * + * Map node size based on node importance + * @remarks + * 在图可视化中,节点的大小通常用于传达节点的重要性或影响力。通过根据节点中心性调整节点的大小,我们可以更直观地展示网络中各个节点的重要性,从而帮助用户更好地理解和分析复杂的网络结构。 + * + * In graph visualization, the size of a node is usually used to convey the importance or influence of the node. By adjusting the size of the node based on the centrality of the node, we can more intuitively show the importance of each node in the network, helping users better understand and analyze complex network structures. + */ +export class MapNodeSize extends BaseTransform { + static defaultOptions: Partial = { + centrality: { type: 'degree' }, + maxSize: 80, + minSize: 20, + scale: 'log', + }; + + constructor(context: RuntimeContext, options: MapNodeSizeOptions) { + super(context, deepMix({}, MapNodeSize.defaultOptions, options)); + } + + public beforeDraw(input: DrawData): DrawData { + const { model } = this.context; + const nodes = model.getNodeData(); + + const maxSize = parseSize(this.options.maxSize); + const minSize = parseSize(this.options.minSize); + + const centralities = this.getCentralities(this.options.centrality); + + const maxCentrality = centralities.size > 0 ? Math.max(...centralities.values()) : 0; + const minCentrality = centralities.size > 0 ? Math.min(...centralities.values()) : 0; + nodes.forEach((datum) => { + const size = this.assignSizeByCentrality( + centralities.get(idOf(datum)) || 0, + minCentrality, + maxCentrality, + minSize, + maxSize, + this.options.scale, + ); + const element = this.context.element?.getElement(idOf(datum)); + reassignTo(input, element ? 'update' : 'add', 'node', deepMix(datum, { style: { size } })); + }); + return input; + } + + private getCentralities(centrality: Required['centrality']): CentralityResult { + const { model } = this.context; + const graphData = model.getData(); + + if (typeof centrality === 'function') return centrality(graphData); + + switch (centrality.type) { + case 'degree': { + const centralityResult = new Map(); + graphData.nodes?.forEach((node) => { + const degree = model.getRelatedEdgesData(idOf(node), centrality.direction).length; + centralityResult.set(idOf(node), degree); + }); + return centralityResult; + } + case 'betweenness': + return calculateBetweennessCentrality(graphData, centrality.directed, centrality.weightPropertyName); + case 'closeness': + return calculateClosenessCentrality(graphData, centrality.directed, centrality.weightPropertyName); + case 'eigenvector': + return calculateEigenvectorCentrality(graphData, centrality.directed); + case 'pagerank': + return calculatePageRankCentrality(graphData, centrality.epsilon, centrality.linkProb); + default: + return initCentralityResult(graphData); + } + } + + private assignSizeByCentrality = ( + centrality: number, + minCentrality: number, + maxCentrality: number, + minSize: STDSize, + maxSize: STDSize, + scale: MapNodeSizeOptions['scale'], + ): STDSize => { + const domain: [number, number] = [minCentrality, maxCentrality]; + const rangeX: [number, number] = [minSize[0], maxSize[0]]; + const rangeY: [number, number] = [minSize[1], maxSize[1]]; + const rangeZ: [number, number] = [minSize[2], maxSize[2]]; + + const interpolate = (centrality: number, range: [number, number]): number => { + if (typeof scale === 'function') { + return scale(centrality, domain, range); + } + switch (scale) { + case 'linear': + return linear(centrality, domain, range); + case 'log': + return log(centrality, domain, range); + case 'pow': + return pow(centrality, domain, range, 2); + case 'sqrt': + return sqrt(centrality, domain, range); + default: + return range[0]; + } + }; + + return [interpolate(centrality, rangeX), interpolate(centrality, rangeY), interpolate(centrality, rangeZ)]; + }; +} + +const initCentralityResult = (graphData: GraphData): CentralityResult => { + const centralityResult = new Map(); + graphData.nodes?.forEach((node) => { + centralityResult.set(idOf(node), 0); + }); + return centralityResult; +}; + +/** + * 计算图中每个节点的中介中心性 + * + * Calculate the betweenness centrality for each node in the graph + * @param graphData - 图数据 | Graph data + * @param directed - 是否为有向图 | Whether the graph is directed + * @param weightPropertyName - 边的权重属性名 | The weight property name of the edge + * @returns 每个节点的中介中心性值 | The betweenness centrality for each node + */ +const calculateBetweennessCentrality = ( + graphData: GraphData, + directed?: boolean, + weightPropertyName?: string, +): CentralityResult => { + const centralityResult = initCentralityResult(graphData); + const { nodes = [] } = graphData; + nodes.forEach((source) => { + nodes.forEach((target) => { + if (source !== target) { + const { allPath } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); + const pathCount = allPath.length; + (allPath as ID[][]).flat().forEach((nodeId) => { + if (nodeId !== idOf(source) && nodeId !== idOf(target)) { + centralityResult.set(nodeId, centralityResult.get(nodeId)! + 1 / pathCount); + } + }); + } + }); + }); + return centralityResult; +}; + +/** + * 计算图中每个节点的接近中心性 + * + * Calculate the closeness centrality for each node in the graph + * @param graphData - 图数据 | Graph data + * @param directed - 是否为有向图 | Whether the graph is directed + * @param weightPropertyName - 边的权重属性名 | The weight property name of the edge + * @returns 每个节点的接近中心性值 | The closeness centrality for each node + */ +const calculateClosenessCentrality = ( + graphData: GraphData, + directed?: boolean, + weightPropertyName?: string, +): CentralityResult => { + const centralityResult = new Map(); + const { nodes = [] } = graphData; + nodes.forEach((source) => { + const totalLength = nodes.reduce((acc, target) => { + if (source !== target) { + const { length } = findShortestPath(graphData, idOf(source), idOf(target), directed, weightPropertyName); + acc += length; + } + return acc; + }, 0); + centralityResult.set(idOf(source), 1 / totalLength); + }); + return centralityResult; +}; + +/** + * 计算图中每个节点的 PageRank 中心性 + * + * Calculate the PageRank centrality for each node in the graph + * @param graphData - 图数据 | Graph data + * @param epsilon - PageRank 算法的收敛容差 | The convergence tolerance of the PageRank algorithm + * @param linkProb - PageRank 算法的阻尼系数,指任意时刻,用户访问到某节点后继续访问该节点链接的下一个节点的概率,经验值 0.85 | The damping factor of the PageRank algorithm, which refers to the probability that a user will continue to visit the next node linked to a node at any time, with an empirical value of 0.85 + * @returns 每个节点的 PageRank 中心性值 | The PageRank centrality for each node + */ +const calculatePageRankCentrality = (graphData: GraphData, epsilon?: number, linkProb?: number): CentralityResult => { + const centralityResult = new Map(); + const data = pageRank(graphData, epsilon, linkProb); + graphData.nodes?.forEach((node) => { + centralityResult.set(idOf(node), data[idOf(node)]); + }); + return centralityResult; +}; + +/** + * 计算图中每个节点的特征向量中心性 + * + * Calculate the eigenvector centrality for each node in the graph. + * @param graphData - 图数据 | Graph data + * @param directed - 是否为有向图 | Whether the graph is directed + * @returns 每个节点的特征向量中心性值 The eigenvector centrality for each node. + */ +const calculateEigenvectorCentrality = (graphData: GraphData, directed?: boolean): CentralityResult => { + const { nodes = [] } = graphData; + const adjacencyMatrix = createAdjacencyMatrix(graphData, directed); + const eigenvector = powerIteration(adjacencyMatrix, nodes.length); + + const centralityResult = new Map(); + nodes.forEach((node, index) => { + centralityResult.set(idOf(node), eigenvector[index]); + }); + + return centralityResult; +}; + +/** + * 创建图的邻接矩阵 + * + * Create the adjacency matrix for the graph. + * @param graphData - 图数据 | Graph data + * @param directed - 是否为有向图 | Whether the graph is directed + * @returns 邻接矩阵 | The adjacency matrix + */ +const createAdjacencyMatrix = (graphData: GraphData, directed?: boolean): number[][] => { + const { nodes = [], edges = [] } = graphData; + const matrix: number[][] = Array(nodes.length) + .fill(null) + .map(() => Array(nodes.length).fill(0)); + + edges.forEach(({ source, target }) => { + const uIndex = nodes.findIndex((node) => idOf(node) === source); + const vIndex = nodes.findIndex((node) => idOf(node) === target); + if (directed) { + matrix[uIndex][vIndex] = 1; + } else { + matrix[uIndex][vIndex] = 1; + matrix[vIndex][uIndex] = 1; + } + }); + + return matrix; +}; + +/** + * 使用幂迭代法计算主特征向量 + * + * Calculate the principal eigenvector using the power iteration method + * @see https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors + * @param matrix - 邻接矩阵 | The adjacency matrix + * @param numNodes - 节点数量 | The number of nodes + * @param maxIterations - 最大迭代次数 | The maximum number of iterations + * @param tolerance - 收敛容差 | The convergence tolerance + * @returns 主特征向量 | The principal eigenvector + */ +const powerIteration = (matrix: number[][], numNodes: number, maxIterations = 100, tolerance = 1e-6): number[] => { + let eigenvector = Array(numNodes).fill(1); + let diff = Infinity; + + for (let iter = 0; iter < maxIterations && diff > tolerance; iter++) { + const newEigenvector = Array(numNodes).fill(0); + + for (let i = 0; i < numNodes; i++) { + for (let j = 0; j < numNodes; j++) { + newEigenvector[i] += matrix[i][j] * eigenvector[j]; + } + } + + const norm = Math.sqrt(newEigenvector.reduce((sum, val) => sum + val * val, 0)); + for (let i = 0; i < numNodes; i++) { + newEigenvector[i] /= norm; + } + + diff = Math.sqrt(newEigenvector.reduce((sum, val, index) => sum + (val - eigenvector[index]) * val, 0)); + eigenvector = newEigenvector; + } + + return eigenvector; +}; diff --git a/packages/g6/src/utils/scale.ts b/packages/g6/src/utils/scale.ts new file mode 100644 index 00000000000..88cca7a263d --- /dev/null +++ b/packages/g6/src/utils/scale.ts @@ -0,0 +1,70 @@ +/** + * 将一个值从一个范围线性映射到另一个范围 + * + * Linearly maps a value from one range to another range + * @param value - 需要映射的值 | The value to be mapped + * @param domain - 输入值的范围 [最小值, 最大值] | The input range [min, max] + * @param range - 输出值的范围 [最小值, 最大值] | The output range [min, max] + * @returns 映射后的值 | The mapped value + */ +export const linear = (value: number, domain: [number, number], range: [number, number]) => { + const [d0, d1] = domain; + const [r0, r1] = range; + + if (d1 === d0) return r0; + + const ratio = (value - d0) / (d1 - d0); + return r0 + ratio * (r1 - r0); +}; + +/** + * 将一个值从一个范围对数映射到另一个范围 + * + * Logarithmically maps a value from one range to another range + * @param value - 需要映射的值 | The value to be mapped + * @param domain - 输入值的范围 [最小值, 最大值] | The input range [min, max] + * @param range - 输出值的范围 [最小值, 最大值] | The output range [min, max] + * @returns 映射后的值 | The mapped value + */ +export const log = (value: number, domain: [number, number], range: [number, number]) => { + const [d0, d1] = domain; + const [r0, r1] = range; + + const ratio = Math.log(value - d0 + 1) / Math.log(d1 - d0 + 1); + return r0 + ratio * (r1 - r0); +}; + +/** + * 将一个值从一个范围幂映射到另一个范围 + * + * Maps a value from one range to another range + * @param value - 需要映射的值 | The value to be mapped + * @param domain - 输入值的范围 [最小值, 最大值] | The input range [min, max] + * @param range - 输出值的范围 [最小值, 最大值] | The output range [min, max] + * @param exponent - 幂指数 | The exponent + * @returns 映射后的值 | The mapped value + */ +export const pow = (value: number, domain: [number, number], range: [number, number], exponent: number = 2): number => { + const [d0, d1] = domain; + const [r0, r1] = range; + + const ratio = Math.pow((value - d0) / (d1 - d0), exponent); + return r0 + ratio * (r1 - r0); +}; + +/** + * 将一个值从一个范围平方根映射到另一个范围 + * + * Maps a value from one range to another range using square root + * @param value - 需要映射的值 | The value to be mapped + * @param domain - 输入值的范围 [最小值, 最大值] | The input range [min, max] + * @param range - 输出值的范围 [最小值, 最大值] | The output range [min, max] + * @returns 映射后的值 | The mapped value + */ +export const sqrt = (value: number, domain: [number, number], range: [number, number]) => { + const [d0, d1] = domain; + const [r0, r1] = range; + + const ratio = Math.sqrt((value - d0) / (d1 - d0)); + return r0 + ratio * (r1 - r0); +}; diff --git a/packages/g6/src/utils/transform.ts b/packages/g6/src/utils/transform.ts index ac14204b934..04b2df24e60 100644 --- a/packages/g6/src/utils/transform.ts +++ b/packages/g6/src/utils/transform.ts @@ -44,7 +44,7 @@ export const reassignTo = ( const exitsDatum: any = input.add[typeName].get(id) || input.update[typeName].get(id) || input.remove[typeName].get(id) || datum; Object.entries(input).forEach(([_type, value]) => { - if (type === _type) value[typeName].set(id, merge ? deepMix(exitsDatum, datum) : exitsDatum); + if (type === _type) value[typeName].set(id, merge ? deepMix(exitsDatum, datum) : datum); else value[typeName].delete(id); }); }; diff --git a/packages/site/examples/scene-case/default/demo/performance-diagnosis-flowchart.js b/packages/site/examples/scene-case/default/demo/performance-diagnosis-flowchart.js index bf585908b70..4cf828bddf3 100644 --- a/packages/site/examples/scene-case/default/demo/performance-diagnosis-flowchart.js +++ b/packages/site/examples/scene-case/default/demo/performance-diagnosis-flowchart.js @@ -18,8 +18,8 @@ const COLOR_MAP = { class HoverElement extends HoverActivate { getActiveIds(event) { const { model, graph } = this.context; - const { targetType, target } = event; - const targetId = target.id; + const targetId = event.target.id; + const targetType = graph.getElementType(targetId); const ids = [targetId]; if (targetType === 'edge') { diff --git a/packages/site/package.json b/packages/site/package.json index 4eb9bd14533..5de4a03698c 100644 --- a/packages/site/package.json +++ b/packages/site/package.json @@ -48,7 +48,7 @@ "@antv/g6": "workspace:*", "@antv/g6-extension-3d": "workspace:*", "@antv/g6-extension-react": "workspace:*", - "@antv/layout": "^1.2.14-beta.7", + "@antv/layout": "^1.2.14-beta.8", "@antv/layout-gpu": "^1.1.7", "@antv/layout-wasm": "^1.4.2", "@antv/util": "^3.3.8", diff --git a/packages/site/src/constants/locales/page-name.json b/packages/site/src/constants/locales/page-name.json index 5dbbeb94802..85d430719cc 100644 --- a/packages/site/src/constants/locales/page-name.json +++ b/packages/site/src/constants/locales/page-name.json @@ -76,5 +76,6 @@ "Tooltip": ["Tooltip", "提示框"], "Watermark": ["Watermark", "水印"], "ProcessParallelEdges": ["ProcessParallelEdges", "平行边"], - "PlaceRadialLabels": ["PlaceRadialLabels", "径向标签"] + "PlaceRadialLabels": ["PlaceRadialLabels", "径向标签"], + "MapNodeSize": ["MapNodeSize", "动态调整节点大小"] }