From 63f43ce478d758b9ed515b213cdf63d4770f690c Mon Sep 17 00:00:00 2001 From: Yanyan Wang Date: Mon, 5 Sep 2022 18:21:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Annotation=20plugin;=20fix:=20combo=20a?= =?UTF-8?q?nd=20drag-node=20with=20heap=20maximum=20p=E2=80=A6=20(#3911)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Annotation plugin; fix: combo and drag-node with heap maximum problem, closes: #3886; fix: combo and graph re-read problem, closes: 3902; fix: d3 force layout with default animate; perf: bundling plugin ts problem, closes: #3904; * chore: refine * chore: refine --- CHANGELOG.md | 8 + packages/core/package.json | 2 +- packages/core/src/global.ts | 2 +- packages/core/src/graph/controller/layout.ts | 2 +- packages/core/src/graph/graph.ts | 2 +- packages/element/package.json | 4 +- packages/g6/package.json | 4 +- packages/g6/src/index.ts | 4 +- packages/pc/package.json | 8 +- packages/pc/src/global.ts | 2 +- packages/pc/src/graph/controller/layout.ts | 3 +- packages/pc/src/index.ts | 5 +- .../tests/unit/combo-collapse-layout-spec.ts | 8 +- packages/plugin/package.json | 6 +- packages/plugin/src/annotation/index.ts | 781 ++++++++++++++++++ packages/plugin/src/bundling/index.ts | 6 +- packages/plugin/src/index.ts | 7 +- packages/plugin/tests/unit/annotation-spec.ts | 74 ++ packages/plugin/tests/unit/grid-spec.ts | 24 +- .../middle/elements/nodes/jsx-node.zh.md | 2 +- 20 files changed, 914 insertions(+), 40 deletions(-) create mode 100644 packages/plugin/src/annotation/index.ts create mode 100644 packages/plugin/tests/unit/annotation-spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 63175e43ea..303da96930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # ChangeLog +#### 4.7.1 + +- feat: Annotation plugin; +- fix: combo and drag-node with heap maximum problem, closes: #3886; +- fix: combo and graph re-read problem, closes: 3902; +- fix: d3 force layout with default animate; +- perf: bundling plugin ts problem, closes: #3904; + #### 4.7.0 - fix: combo collapsed edge problems, closes: #3839; diff --git a/packages/core/package.json b/packages/core/package.json index 196a350762..e2241b67f4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-core", - "version": "0.7.0", + "version": "0.7.1", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts index dd3fe47b05..f876bfa917 100644 --- a/packages/core/src/global.ts +++ b/packages/core/src/global.ts @@ -64,7 +64,7 @@ const colorSet = { }; export default { - version: '0.7.0', + version: '0.7.1', rootContainerClassName: 'root-container', nodeContainerClassName: 'node-container', edgeContainerClassName: 'edge-container', diff --git a/packages/core/src/graph/controller/layout.ts b/packages/core/src/graph/controller/layout.ts index a7efa44f7c..2a50ddd8ed 100644 --- a/packages/core/src/graph/controller/layout.ts +++ b/packages/core/src/graph/controller/layout.ts @@ -203,7 +203,7 @@ export default abstract class LayoutController { layoutMethods?.forEach((layoutMethod: any, index: number) => { const currentCfg = layoutCfg[index] || layoutCfg; start = start.then(() => { - const relayoutPromise = this.execLayoutMethod(layoutMethod, currentCfg); + const relayoutPromise = this.execLayoutMethod(currentCfg, index); if (index === layoutMethods.length - 1) { layoutCfg.onAllLayoutEnd?.(); } diff --git a/packages/core/src/graph/graph.ts b/packages/core/src/graph/graph.ts index 26f5e9fb4a..694909984d 100644 --- a/packages/core/src/graph/graph.ts +++ b/packages/core/src/graph/graph.ts @@ -2438,7 +2438,7 @@ export default abstract class AbstractGraph extends EventEmitter implements IAbs this.initGroups(); // 清空画布时同时清除数据 - this.set({ itemMap: {}, nodes: [], edges: [], groups: [], combos: [], comboTrees: [] }); + this.set({ itemMap: {}, nodes: [], edges: [], vedges: [], groups: [], combos: [], comboTrees: [] }); if (!avoidEmit) this.emit('afterrender'); return this; } diff --git a/packages/element/package.json b/packages/element/package.json index 5a412d602f..12242589d6 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-element", - "version": "0.7.0", + "version": "0.7.1", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", @@ -61,7 +61,7 @@ }, "dependencies": { "@antv/g-base": "^0.5.1", - "@antv/g6-core": "0.7.0", + "@antv/g6-core": "0.7.1", "@antv/util": "~2.0.5" }, "devDependencies": { diff --git a/packages/g6/package.json b/packages/g6/package.json index b6386866bf..90a7250478 100644 --- a/packages/g6/package.json +++ b/packages/g6/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6", - "version": "4.7.0", + "version": "4.7.1", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", @@ -66,7 +66,7 @@ ] }, "dependencies": { - "@antv/g6-pc": "0.7.0" + "@antv/g6-pc": "0.7.1" }, "devDependencies": { "@babel/core": "^7.7.7", diff --git a/packages/g6/src/index.ts b/packages/g6/src/index.ts index 523aee773f..c3286aa4aa 100644 --- a/packages/g6/src/index.ts +++ b/packages/g6/src/index.ts @@ -1,7 +1,7 @@ import G6 from '@antv/g6-pc'; -G6.version = '4.7.0'; +G6.version = '4.7.1'; export * from '@antv/g6-pc'; export default G6; -export const version = '4.7.0'; +export const version = '4.7.1'; diff --git a/packages/pc/package.json b/packages/pc/package.json index d398276fb7..5c9999538c 100644 --- a/packages/pc/package.json +++ b/packages/pc/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-pc", - "version": "0.7.0", + "version": "0.7.1", "description": "A Graph Visualization Framework in JavaScript", "keywords": [ "antv", @@ -75,9 +75,9 @@ "@antv/g-canvas": "^0.5.2", "@antv/g-math": "^0.1.1", "@antv/g-svg": "^0.5.1", - "@antv/g6-core": "0.7.0", - "@antv/g6-element": "0.7.0", - "@antv/g6-plugin": "0.7.0", + "@antv/g6-core": "0.7.1", + "@antv/g6-element": "0.7.1", + "@antv/g6-plugin": "0.7.1", "@antv/hierarchy": "^0.6.7", "@antv/layout": "^0.3.0", "@antv/matrix-util": "^3.1.0-beta.3", diff --git a/packages/pc/src/global.ts b/packages/pc/src/global.ts index a78a38d72f..7f216e8817 100644 --- a/packages/pc/src/global.ts +++ b/packages/pc/src/global.ts @@ -7,7 +7,7 @@ const textColor = 'rgb(0, 0, 0)'; const colorSet = getColorsWithSubjectColor(subjectColor, backColor); export default { - version: '0.7.0', + version: '0.7.1', rootContainerClassName: 'root-container', nodeContainerClassName: 'node-container', edgeContainerClassName: 'edge-container', diff --git a/packages/pc/src/graph/controller/layout.ts b/packages/pc/src/graph/controller/layout.ts index ee1a2824de..e98fb9b601 100644 --- a/packages/pc/src/graph/controller/layout.ts +++ b/packages/pc/src/graph/controller/layout.ts @@ -132,11 +132,12 @@ export default class LayoutController extends AbstractLayout { const isForce = layoutType === 'force' || layoutType === 'g6force' || layoutType === 'gForce' || layoutType === 'force2'; if (isForce) { const { onTick, animate } = layoutCfg; + const d3ForceAnimate = animate === undefined && layoutType === 'force' const tick = () => { if (onTick) { onTick(); } - if (animate) graph.refreshPositions(); + if (animate || d3ForceAnimate) graph.refreshPositions(); }; layoutCfg.tick = tick; } else if (layoutType === 'comboForce' || layoutType === 'comboCombined') { diff --git a/packages/pc/src/index.ts b/packages/pc/src/index.ts index 33d50c37ba..7687e0aaf6 100644 --- a/packages/pc/src/index.ts +++ b/packages/pc/src/index.ts @@ -20,7 +20,7 @@ import './element'; import './behavior'; type IAlgorithm = typeof AlgorithmSync | typeof AlgorithmAsync; -const Algorithm: IAlgorithm = { ...AlgorithmSync, ... AlgorithmAsync }; +const Algorithm: IAlgorithm = { ...AlgorithmSync, ...AlgorithmAsync }; const Grid = Plugin.Grid; const Minimap = Plugin.Minimap; @@ -34,6 +34,7 @@ const ImageMinimap = Plugin.ImageMinimap; const EdgeFilterLens = Plugin.EdgeFilterLens; const SnapLine = Plugin.SnapLine; const Legend = Plugin.Legend; +const Annotation = Plugin.Annotation; export * from '@antv/g6-core'; export * from './types'; @@ -60,6 +61,7 @@ export { EdgeFilterLens, SnapLine, Legend, + Annotation, Arrow, Marker, Shape, @@ -94,6 +96,7 @@ export default { Fisheye, ImageMinimap, EdgeFilterLens, + Annotation, Algorithm, Arrow, Marker, diff --git a/packages/pc/tests/unit/combo-collapse-layout-spec.ts b/packages/pc/tests/unit/combo-collapse-layout-spec.ts index 53166702e1..0da00497a1 100644 --- a/packages/pc/tests/unit/combo-collapse-layout-spec.ts +++ b/packages/pc/tests/unit/combo-collapse-layout-spec.ts @@ -9,7 +9,12 @@ const data = { { "id": "ccc", "label": "ccc", - "collapsed": true + "collapsed": true, + parentId: 'd' + }, + { + "id": "d", + "label": "d", } ], "edges": [ @@ -28,7 +33,6 @@ const data = { } ] }; - describe('combo layout with collapsed', () => { it('combo layout with collapsed', (done) => { const graph = new G6.Graph({ diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 54f13cb025..6969c1c40d 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@antv/g6-plugin", - "version": "0.7.0", + "version": "0.7.1", "description": "G6 Plugin", "main": "lib/index.js", "module": "es/index.js", @@ -22,8 +22,8 @@ "@antv/g-base": "^0.5.1", "@antv/g-canvas": "^0.5.2", "@antv/g-svg": "^0.5.2", - "@antv/g6-core": "0.7.0", - "@antv/g6-element": "0.7.0", + "@antv/g6-core": "0.7.1", + "@antv/g6-element": "0.7.1", "@antv/matrix-util": "^3.1.0-beta.3", "@antv/scale": "^0.3.4", "@antv/util": "^2.0.9", diff --git a/packages/plugin/src/annotation/index.ts b/packages/plugin/src/annotation/index.ts new file mode 100644 index 0000000000..b27c27359e --- /dev/null +++ b/packages/plugin/src/annotation/index.ts @@ -0,0 +1,781 @@ +import { isNumber } from '@antv/util'; +import { Util, ShapeStyle } from '@antv/g6-core'; +import { modifyCSS, createDom } from '@antv/dom-util'; +import insertCss from 'insert-css'; +import { Canvas } from '@antv/g-canvas'; +import { IShape } from '@antv/g-base'; +import Base from '../base'; + +typeof document !== 'undefined' && + insertCss(` + .g6-annotation-container { + background-color: rgba(255, 255, 255, 0.3); + padding: 8px; + } + .g6-annotation-wrapper { + background-color: #fff; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.85); + border-radius: 5px; + } + .g6-annotation-header-wapper { + height: fit-content; + width: 100%; + background-color: #5B8FF9; + display: inline-flex; + cursor: move; + } + .g6-annotation-title { + margin: 4px 40px 4px 8px; + cursor: text; + } + .g6-annotation-collapse { + margin: 4px; + cursor: pointer; + } + .g6-annotation-expand { + margin: 4px; + cursor: pointer; + } + .g6-annotation-close { + margin: 4px 8px 4px 0; + cursor: pointer; + } + .g6-annotation-content { + padding: 8px; + width: fit-content; + cursor: text; + } + .g6-annotation-title-input-wrapper { + margin: 4px 40px 4px 8px; + } + .g6-annotation-content-input-wrapper { + margin: 8px; + } +`); + +interface AnnotationConfig { + trigger?: 'click' | 'fix', + containerCfg?: { // 无配置则没有自身容器,直接以图的容器为父容器 + position?: 'left' | 'right' | 'top' | 'bottom', + className?: string, + width?: number, + height?: number, + offsetX?: number, + offsetY?: number, + }, + editable?: boolean, // TODO + itemHighlightState?: string, + defaultData?: CardCfg[], + cardCfg?: CardCfg, + linkStyle?: ShapeStyle, + linkHighlightStyle?: ShapeStyle, + getTitle?: (item) => string | HTMLDivElement, + getContent?: (item) => string | HTMLDivElement +} + + +interface CardCfg { + id?: string, + width?: number | 'fit-content', + height?: number | 'fit-content', + minHeight?: number | string, + minWidth?: number | string, + collapsed?: boolean; + // 指定位置,视口坐标系。仅在无 containerCfg 时生效 + x?: number; + y?: number; + title?: string, + content?: string, +} + +interface CardInfoMap { + [id: string]: CardCfg & { + card: HTMLDivElement, + link: IShape, + cardBBox?: { + left: number, + right: number, + top: number, + bottom: number, + } + } +} + +export default class Annotation extends Base { + constructor(config?: AnnotationConfig) { + super(config); + } + public getDefaultCfgs(): AnnotationConfig { + return { + trigger: 'click', + editable: true, + itemHighlightState: 'highlight', + linkHighlightStyle: { + shadowColor: '#5B8FF9', + shadowBlur: 10 + }, + cardCfg: { + minHeight: 60, + width: 'fit-content', + height: 'fit-content' + }, + }; + } + + // class-methods-use-this + public getEvents() { + let events: { + [eventName: string]: string + } = { + 'viewportchange': 'updateLinks', + 'afterlayout': 'updateLinks', + 'aftergraphrefreshposition': 'updateLinks', + } + switch (this.get('trigger')) { + case 'click': + events = { + ...events, + 'node:click': 'showAnnotation', + 'edge:click': 'showAnnotation', + } + } + return events + } + + private getDOMContent(cfg) { + if (this.destroyed) return; + const { collapsed, title = '', content = '' } = cfg; + const collapseExpandDOM = collapsed ? + `

+

` : + `

-

`; + const contentDOM = collapsed ? '' : `

${content}

`; + + return `
+
+

${title}

+ ${collapseExpandDOM} +

x

+
+ ${contentDOM} +
` + } + + public init() { + const self = this; + if (self.destroyed) return; + const graph = self.get('graph'); + const graphCantainer = graph.getContainer(); + + let container = self.get('container') + const containerCfg = this.get('containerCfg'); + if (containerCfg) { + container = this.createContainer(); + graphCantainer.appendChild(container) + } else { + container = graphCantainer; + } + this.set('container', container) + + // 绘制连接 annotation 和元素的连线的画布 + const graphContainerBBox = graphCantainer.getBoundingClientRect(); + const linkCanvas = new Canvas({ + container: graphCantainer, + width: graphContainerBBox.right - graphContainerBBox.left, + height: graphContainerBBox.bottom - graphContainerBBox.top + }); + const { top, left } = graph.get('canvas').get('el').getBoundingClientRect(); + const graphTop = graphCantainer.offsetTop; + const graphLeft = graphCantainer.offsetLeft; + modifyCSS(linkCanvas.get('el'), { + position: 'absolute', + top: 0, + left: 0, + // top: `${top}px`, + // left: `${left}px`, + pointerEvents: 'none' + }) + window.addEventListener('resize', this.resizeCanvas); + + const linkGroup = linkCanvas.addGroup({ id: 'annotation-link-group' }); + self.set('linkGroup', linkGroup); + self.set('canvas', linkCanvas); + + if (!self.get('getTitle')) { + self.set('getTitle', (item) => { + const { label, id } = item?.getModel() || {}; + return label || id || '-'; + }); + } + if (!self.get('getContent')) { + self.set('getContent', (item) => { + if (!item) return '-'; + const { label, id } = item.getModel(); + return `${item.getType()}: ${label || id || ''}`; + }); + } + + // init with defaultData + const defaultData = self.get('defaultData'); + if (defaultData) this.readData(defaultData); + } + + private createContainer() { + if (this.destroyed) return; + const containerCfg = this.get('containerCfg'); + const graph = this.get('graph'); + const graphContainer = graph.getContainer(); + const { left: gLeft, right: gRight, top: gTop, bottom: gBottom } = graphContainer.getBoundingClientRect(); + const graphContainerHeight = gBottom - gTop; + const graphContainerWidth = gRight - gLeft; + const { position = 'top', offsetX = 0, offsetY = 0, ...otherStyle } = containerCfg; + let { height = 'fit-content', width = graph.getWidth() } = containerCfg; + if (height === '100%') height = graphContainerHeight; + if (width === '100%') width = graphContainerWidth; + let maxHeight = 'unset', maxWidth = 'unset'; + + let containerPosition: any = {}; + switch (position) { + case 'right': + maxHeight = `${graphContainerHeight}px`; + containerPosition = { top: 0, right: 0 }; + containerPosition.right += (gLeft + offsetX); + containerPosition.top += (gTop + offsetY); + break; + case 'bottom': + maxWidth = `${graphContainerWidth}px`; + containerPosition = { bottom: 0, left: 0 }; + containerPosition.left += (gLeft + offsetX); + containerPosition.bottom += (gTop + offsetY); + break; + case 'top': + maxWidth = `${graphContainerWidth}px`; + case 'left': + maxHeight = `${graphContainerHeight}px`; + default: + containerPosition = { top: 0, left: 0 }; + containerPosition.left += (gLeft + offsetX); + containerPosition.top += (gTop + offsetY); + break; + } + Object.keys(containerPosition).forEach(key => { + containerPosition[key] = `${containerPosition[key]}px`; + }); + + const container = createDom(`
`); + modifyCSS(container, { + position: 'absolute', + display: position === 'top' || position === 'bottom' ? 'inline-flex' : 'unset', + width: isNumber(width) ? `${width}px` : width, + height: isNumber(height) ? `${height}px` : height, + maxHeight, + maxWidth, + overflow: 'scroll', + ...containerPosition, + ...otherStyle + }); + graphContainer.appendChild(container); + container.addEventListener('scroll', e => { + this.updateLinks(); + }) + return container; + } + + private resizeCanvas(e) { + if (this.destroyed) return; + const cBBox = this.get('container').getBoundingClientRect(); + this.get('canvas').changeSize( + cBBox.right - cBBox.left, + cBBox.bottom - cBBox.top + ) + } + + public showAnnotation(evt) { + if (this.destroyed) return; + const { item } = evt; + this.toggleAnnotation(item); + } + + public hideCard(evt) { + if (this.destroyed) return; + const { item } = evt; + this.closeCard(item.getID()); + } + + public closeCards() { + const self = this; + if (self.destroyed) return; + const graph = self.get('graph'); + const cardInfoMap = self.get('cardInfoMap') || {}; + Object.keys(cardInfoMap).forEach((itemId) => { + const item = graph.findById(itemId); + if (item) self.hideCard({ item }); + }) + } + + public toggleAnnotation(item, cfg: CardCfg = {}) { + const self = this; + if (self.destroyed) return; + const cardInfoMap = self.get('cardInfoMap') || {}; + const graph = self.get('graph'); + const container = self.get('container'); + const containerCfg = self.get('containerCfg') + const { + minHeight, + minWidth, + width, + height, + collapsed = false, + x: propsX, + y: propsY, + title: propsTitle, + content: propsContent + } = Object.assign({}, self.get('cardCfg') || {}, cfg); + const linkGroup = self.get('linkGroup'); + const rows = this.get('rows') || [[]]; + + const itemId = item.getID(); + let { card, link, x, y, title, content } = cardInfoMap[itemId] || {}; + + const getTitle = this.get('getTitle'); + const getContent = this.get('getContent'); + const newCard = createDom(this.getDOMContent({ + collapsed, + title: title || propsTitle || getTitle?.(item), + content: content || propsContent || getContent?.(item) + })); + const minHeightPx = isNumber(minHeight) ? `${minHeight}px` : minHeight + modifyCSS(newCard, { + minHeight: collapsed ? 'unset' : minHeightPx, + minWidth: isNumber(minWidth) ? `${minWidth}px` : minWidth, + height, + width, + }); + if (card) { + // 移除相应连线 + link?.remove(true); + // 替换原来的卡片 + container.replaceChild(newCard, card); + } else { + container.appendChild(newCard); + } + + let containerBBox; + if (!containerCfg) { + containerBBox = container.getBoundingClientRect() || {}; + if (propsX !== undefined && propsY !== undefined) { + // 使用配置的位置 + x = propsX; + y = propsY; + } else if (!card) { + // 第一次创建,且无 conatiner,初始化位置 + const { left: containerLeft = 0, width: containerWidth, top: containerTop } = containerBBox; + const cardWidth = isNumber(minWidth) ? minWidth : 100; + console.log('card pos', container.scrollWidth, containerWidth, newCard.scrollWidth, newCard.getBoundingClientRect().width, containerLeft); + x = container.scrollWidth - newCard.scrollWidth - 16 - (rows.length - 1) * cardWidth; + console.log('card pos', container.scrollWidth, containerWidth, newCard.scrollWidth, newCard.getBoundingClientRect().width, containerLeft, x); + const currentRow = rows[rows.length - 1]; + const lastCardBBox = currentRow[currentRow.length - 1]; + y = ((lastCardBBox?.bottom - containerTop) || 0) + 8; + } + modifyCSS(newCard, { + position: 'absolute', + left: `${x}px`, + top: `${y}px`, + cusor: containerCfg ? 'unset' : 'move', + }); + } + this.bindListener(newCard, itemId); + + // 创建相关连线 + const cardBBox = newCard.getBoundingClientRect() + const path = getPathItem2Card(item, cardBBox, graph, this.get('canvas')); + const linkStyle = this.get('linkStyle'); + link = linkGroup.addShape('path', { + attrs: { + lineWidth: 1, + lineDash: [5, 5], + stroke: '#ccc', + path, + ...linkStyle + } + }); + cardInfoMap[itemId] = { + id: itemId, + collapsed, + card: newCard, + link, + x, + y, + cardBBox, + }; + self.set('cardInfoMap', cardInfoMap); + if (containerCfg) { + this.updateCardPositionsInConatainer(); + this.updateLinks(); + } else if (!card) { + // 没有 container、新增 card 时,记录当前列中最下方位置,方便换行 + const { bottom: containerBottom = 0 } = containerBBox; + console.log('bottom', containerBottom, cardBBox); + rows[rows.length - 1].push(cardBBox); + if (cardBBox.bottom > containerBottom - 16) { + rows.push([]); + } + this.set('rows', rows) + } + } + + public updateCardPositionsInConatainer() { + if (this.destroyed) return; + const cardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const container = this.get('container'); + const { position } = this.get('containerCfg'); + let { width: containerWidth } = container.getBoundingClientRect(); + const computeStyle = getComputedStyle(container); + const sidePadding = px2Num(computeStyle['paddingLeft']) + px2Num(computeStyle['paddingRight']) + containerWidth -= sidePadding; + Object.values(cardInfoMap).forEach(({ card }) => { + const { width: cardWidth } = card.getBoundingClientRect(); + switch (position) { + case 'right': + modifyCSS(card, { + marginLeft: containerWidth ? `${containerWidth - cardWidth}px` : '0px' + }); + break; + case 'top': + case 'bottom': + modifyCSS(card, { + marginLeft: '8px' + }); + default: + break; + } + }); + } + + public handleExpandCollapseCard(id) { + if (this.destroyed) return; + const graph = this.get('graph'); + const cardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const { collapsed } = cardInfoMap[id]; + const item = graph.findById(id); + if (!item) return; + this.toggleAnnotation(item, { collapsed: !collapsed }); + cardInfoMap[id] = { + ...cardInfoMap[id], + collapsed: !collapsed + } + } + + public closeCard(id) { + if (this.destroyed) return; + const cardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const { card, link } = cardInfoMap[id]; + modifyCSS(card, { display: 'none' }); + link?.hide(); + } + + private bindListener(card, itemId) { + if (this.destroyed) return; + // mouseenter and mouseleave to highlight the corresponding items + card.addEventListener('mouseenter', e => { + const cardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const graph = this.get('graph'); + const itemHighlightState = this.get('itemHighlightState') + const item = graph.findById(itemId); + graph.setItemState(item, itemHighlightState, true); + + const { link } = cardInfoMap[itemId]; + const linkHighlightStyle = this.get('linkHighlightStyle') || {}; + link.attr(linkHighlightStyle); + }) + card.addEventListener('mouseleave', e => { + const cardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const graph = this.get('graph'); + const itemHighlightState = this.get('itemHighlightState') + const item = graph.findById(itemId); + graph.setItemState(item, itemHighlightState, false); + + const { link } = cardInfoMap[itemId]; + const linkHighlightStyle = this.get('linkHighlightStyle') || {}; + Object.keys(linkHighlightStyle).forEach(key => { + link.attr(key, undefined); + link.attr(key, undefined); + }); + const linkStyle = this.get('linkStyle'); + link.attr(linkStyle); + }) + card.addEventListener('click', e => { + if (e.target.className === 'g6-annotation-collapse' || e.target.className === 'g6-annotation-expand') { + // collapse & expand + this.handleExpandCollapseCard(itemId); + } else if (e.target.className === 'g6-annotation-close') { + // close + this.closeCard(itemId); + } + }); + // dblclick to edit the title and content text + const editable = this.get('editable'); + if (editable) { + card.addEventListener('dblclick', e => { + const cardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const target = e.target; + const targetClass = target.className; + if (targetClass !== 'g6-annotation-title' && targetClass !== 'g6-annotation-content') return; + const { width } = target.getBoundingClientRect(); + const computeStyle = getComputedStyle(target); + const name = targetClass === 'g6-annotation-title' ? 'title' : 'content'; + const input = createDom(``); + const inputWrapper = createDom(`
`); + inputWrapper.appendChild(input); + target.parentNode.replaceChild(inputWrapper, target); + input.focus(); + const cardInfo = cardInfoMap[itemId]; + input.addEventListener('blur', blurEvt => { + if (input.value) { + target.innerHTML = input.value; + cardInfo[input.name || 'title'] = input.value; + } + inputWrapper.parentNode.replaceChild(target, inputWrapper); + this.updateCardSize(itemId); + }); + }); + } + let mousedown = false; + const unmovableClasses = ['g6-annotation-title', 'g6-annotation-content', 'g6-annotation-title-input', 'g6-annotation-content-input'] + card.addEventListener('mousedown', e => { + const targetClass = e.target.className; + if (unmovableClasses.includes(targetClass)) return; + mousedown = true; + }); + card.addEventListener('mousemove', e => { + e.preventDefault(); + const cardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + if (mousedown) { + // == dragstart + mousedown = false; + const { style } = card; + this.set('dragging', { + card, + x: e.clientX, + y: e.clientY, + left: px2Num(style.left), + top: px2Num(style.top) + }); + } + const { clientX, clientY } = e; + const dragging = this.get('dragging'); + if (dragging?.card !== card || isNaN(clientX) || isNaN(clientY)) return; + let { x, y, left, top } = dragging; + const dx = clientX - x; + const dy = clientY - y; + if (Math.abs(dx) > 100 || Math.abs(dy) > 100) return; + left += dx; + top += dy; + const graph = this.get('graph'); + const graphContainerBBox = graph.getContainer().getBoundingClientRect(); + const cardBBox = card.getBoundingClientRect(); + const cardWidth = cardBBox.right - cardBBox.left; + const cardHeight = cardBBox.bottom - cardBBox.top; + if (left > graphContainerBBox.right - cardWidth) left -= dx; + if (top > graphContainerBBox.bottom - cardHeight) top -= dy; + // 更新卡片位置 + modifyCSS(card, { + left: `${left}px`, + top: `${top}px`, + }); + x = clientX; + y = clientY; + + // 更新连线位置 + const { link } = cardInfoMap[itemId] || {}; + if (link) { + const item = graph.findById(itemId); + link.attr('path', getPathItem2Card(item, cardBBox, graph, this.get('canvas'))); + } + this.set('dragging', { x, y, left, top, card }); + }); + const dragendListener = e => { + const cardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const dragging = this.get('dragging'); + mousedown = false; + if (dragging) { + // = dragend + let { left, top } = dragging; + cardInfoMap[itemId].x = left; + cardInfoMap[itemId].y = top; + this.set('dragging', false); + } + } + card.addEventListener('mouseup', dragendListener); + card.addEventListener('mouseleave', dragendListener); + } + + public updateCardSize(id) { + const cardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const { card } = cardInfoMap[id]; + const { width } = card.getBoundingClientRect(); + const title = card.getElementsByClassName('g6-annotation-title')[0]; + if (title) { + const computeStyle = getComputedStyle(title); + const sideMargin = px2Num(computeStyle['marginLeft']); + const { width: titleWidth } = title.getBoundingClientRect(); + modifyCSS(title, { + marginRight: `${width - sideMargin - 24 - 16 - titleWidth}px` + }) + } + } + + public updateLinks() { + if (this.destroyed) return; + const cardInfoMap: CardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const graph = this.get('graph'); + const canvas = this.get('canvas'); + Object.values(cardInfoMap).forEach(cardInfo => { + const { link, id, card } = cardInfo; + const item = graph.findById(id); + const path = getPathItem2Card(item, card.getBoundingClientRect(), graph, canvas); + link.attr('path', path) + }) + } + + public saveData(saveClosed = false) { + const cardInfoMap: CardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) []; + const graph = this.get('graph'); + const getTitle = this.get('getTitle'); + const getContent = this.get('getContent'); + const data = []; + Object.values(cardInfoMap).forEach(info => { + const { title, content, x, y, id, collapsed, card } = info; + if (card.style.display === 'none' && !saveClosed) return; + const item = graph.findById(id); + data.push({ + id, + x, + y, + collapsed, + title: title || getTitle?.(item), + content: content || getContent?.(item), + visible: card.style.display !== 'none' + }) + }); + return data; + } + + public readData(data) { + const graph = this.get('graph'); + data.forEach(info => { + const { id, x, y, title, content, collapsed, visible } = info; + const item = graph.findById(id); + this.toggleAnnotation(item, { x, y, title, content, collapsed }); + if (!visible) this.closeCard(id); + }) + } + + /** + * Clear the cards and links + */ + public clear() { + const cardInfoMap: CardInfoMap = this.get('cardInfoMap'); + if (!cardInfoMap) return; + const container = this.get('container'); + Object.values(cardInfoMap).forEach(cardInfo => { + const { card, link } = cardInfo; + container.removeChild(card); + link?.remove(true); + }); + this.set('cardInfoMap', {}); + } + + /** + * Destroy the component + */ + public destroy() { + this.clear(); + this.get('canvas')?.destroy(); + window.removeEventListener('resize', this.resizeCanvas); + const graph = this.get('graph'); + if (!graph || graph.destroyed) return; + if (this.get('containerCfg')) { + graph.getContainer().removeChild(this.get('container')); + } + this.destroyed = true; + } +} + +const getPath = (startPoints, endPoints) => { + let startPoint, endPoint, posKeys, distance = Infinity; + Object.keys(startPoints).forEach(skey => { + const spos = startPoints[skey]; + Object.keys(endPoints).forEach(ekey => { + const epos = endPoints[ekey]; + const xdist = spos.x - epos.x; + const ydist = spos.y - epos.y; + const dist = xdist * xdist + ydist * ydist; + if (distance > dist) { + distance = dist; + startPoint = spos; + endPoint = epos; + posKeys = [skey, ekey]; + } + }); + }); + const curveOffset = 20; + const controlPoint = Util.getControlPoint(startPoint, endPoint, 0.5, curveOffset); + return [ + ['M', startPoint.x, startPoint.y], + ['Q', controlPoint.x, controlPoint.y, endPoint.x, endPoint.y], + ] +} + +const getPathItem2Card = (item, cardBBox, graph, annotationCanvas) => { + let itemLinkPoints; + const itemType = item.getType(); + if (itemType === 'edge') { + itemLinkPoints = [item.getKeyShape().getPoint(0.5)]; + } else { + let { minX, minY, maxX, maxY } = item.getKeyShape?.().getBBox(); + const { x, y } = item.getModel(); + minX += x; + minY += y; + maxX += x; + maxY += y; + itemLinkPoints = { + left: { x: minX, y: (minY + maxY) / 2 }, + right: { x: maxX, y: (minY + maxY) / 2 }, + top: { x: (minX + maxX) / 2, y: minY }, + bottom: { x: (minX + maxX) / 2, y: maxY }, + } + } + + // 由 graph 所在 canvas 转换为 Client 坐标系,然后再由 annotation 所在 canvas 转换为绘制坐标系 + Object.keys(itemLinkPoints).forEach(key => { + const { x, y } = itemLinkPoints[key]; + const clientPos = graph.getClientByPoint(x, y); + itemLinkPoints[key] = annotationCanvas.getPointByClient(clientPos.x, clientPos.y); + }); + + const { top: cardTop = 0, left: cardLeft = 0, right: cardRight = 0, bottom: cardBottom = 0 } = cardBBox; + const cardLinkPoints = { + left: annotationCanvas.getPointByClient(cardLeft, (cardTop + cardBottom) / 2), + right: annotationCanvas.getPointByClient(cardRight, (cardTop + cardBottom) / 2), + top: annotationCanvas.getPointByClient((cardLeft + cardRight) / 2, cardTop), + bottom: annotationCanvas.getPointByClient((cardLeft + cardRight) / 2, cardBottom), + }; + return getPath(itemLinkPoints, cardLinkPoints); +} + +const px2Num = px => Number(px.replace(/\s+|px/gi, "")) || 0; \ No newline at end of file diff --git a/packages/plugin/src/bundling/index.ts b/packages/plugin/src/bundling/index.ts index ddf75b576b..35b57ea9f9 100644 --- a/packages/plugin/src/bundling/index.ts +++ b/packages/plugin/src/bundling/index.ts @@ -76,8 +76,8 @@ export default class Bundling extends Base { iterRate: 0.6666667, // 迭代下降率 bundleThreshold: 0.6, eps: 1e-6, - onLayoutEnd() {}, // 布局完成回调 - onTick() {}, // 每一迭代布局回调 + onLayoutEnd() { }, // 布局完成回调 + onTick() { }, // 每一迭代布局回调 }; } @@ -222,7 +222,7 @@ export default class Bundling extends Base { } else { let edgeLength = 0; - if (!edgePoints[i] || edgePoints[i] === []) { + if (!edgePoints[i]?.length) { // it is a straight line edgeLength = getEucliDis({ x: source.x!, y: source.y! }, { x: target.x!, y: target.y! }); } else { diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index b33ba1c4e6..b2763f6aa2 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -11,6 +11,7 @@ import EdgeFilterLens from './edgeFilterLens'; import SnapLine from './snapline'; import PluginBase from './base'; import Legend from './legend'; +import Annotation from './annotation'; export { PluginBase, @@ -25,7 +26,8 @@ export { ImageMinimap, EdgeFilterLens, SnapLine, - Legend + Legend, + Annotation }; const Plugin = { @@ -41,7 +43,8 @@ const Plugin = { ImageMinimap, EdgeFilterLens, SnapLine, - Legend + Legend, + Annotation }; export default Plugin; diff --git a/packages/plugin/tests/unit/annotation-spec.ts b/packages/plugin/tests/unit/annotation-spec.ts new file mode 100644 index 0000000000..2d080c58db --- /dev/null +++ b/packages/plugin/tests/unit/annotation-spec.ts @@ -0,0 +1,74 @@ +import G6, { GraphData } from '@antv/g6'; +import insertCss from 'insert-css'; +import { data } from './data'; +import Anotation from '../../src/annotation'; + +// insertCss(` +// .g6-annotation-header-wapper { +// background-color: #f00 +// } +// `); + +const div = document.createElement('div'); +div.id = 'force-layout'; +document.body.appendChild(div); + +describe('edge bundling', () => { + const graph = new G6.Graph({ + container: div, + width: 500, + height: 500, + layout: { + type: 'force', + }, + defaultNode: { size: 10 }, + modes: { + default: ['drag-canvas', 'zoom-canvas'] + } + }); + + graph.data(data); + graph.render(); + + it('edge bundling on circular layout with default configs', () => { + const annotation = new Anotation({ + // containerCfg: { + // position: 'right', + // width: 'fit-content', + // height: 'fit-content', + // backgroundColor: 'rgba(255, 255, 255, 0.3)' + // }, + linkStyle: { + stroke: '#5B8FF9', + lineWidth: 3, + opacity: 0.5 + }, + itemHighlightState: 'selected' + // editable: false + }); + graph.addPlugin(annotation); + + annotation.toggleAnnotation(graph.getNodes()[0], { x: 100, y: 200 }) + let data; + graph.on('canvas:click', e => { + data = annotation.saveData(true); + console.log(JSON.stringify(data)); + // annotation.destroy(); + + // const annotation2 = new Anotation({ + // containerCfg: { + // position: 'right', + // width: 'fit-content', + // height: 'fit-content', + // backgroundColor: 'rgba(255, 255, 255, 0.3)' + // }, + // linkStyle: { + // stroke: '#f00' + // }, + // defaultData: data + // }); + // graph.addPlugin(annotation2); + // // annotation2.readData(data) + }) + }); +}); diff --git a/packages/plugin/tests/unit/grid-spec.ts b/packages/plugin/tests/unit/grid-spec.ts index b980a075c8..4fd38e9af2 100644 --- a/packages/plugin/tests/unit/grid-spec.ts +++ b/packages/plugin/tests/unit/grid-spec.ts @@ -42,36 +42,36 @@ describe('grid', () => { const minZoom = graph.get('minZoom'); const width = (800 * 80) / minZoom; // 3200000 const height = (600 * 80) / minZoom; // 2400000 - expect(gridContainer.style.width).toBe('3.2e+06px'); // width - expect(gridContainer.style.height).toBe('2.4e+06px'); // height - expect(gridContainer.style.left).toEqual('-1.6e+06px'); // -width . 2 - expect(gridContainer.style.top).toEqual('-1.2e+06px'); // -height / 2 + expect(gridContainer.style.width).toBe('320000px'); // width + expect(gridContainer.style.height).toBe('240000px'); // height + expect(gridContainer.style.left).toEqual('-160000px'); // -width . 2 + expect(gridContainer.style.top).toEqual('-120000px'); // -height / 2 expect(gridContainer.style.backgroundImage).not.toEqual(''); graph.translate(-100, -100); expect(gridContainer.style.transform).toEqual('matrix(1, 0, 0, 1, -100, -100)'); - expect(gridContainer.style.left).toEqual('-1.6e+06px'); // -width . 2 - expect(gridContainer.style.top).toEqual('-1.2e+06px'); // -height / 2 + expect(gridContainer.style.left).toEqual('-160000px'); // -width . 2 + expect(gridContainer.style.top).toEqual('-120000px'); // -height / 2 graph.zoom(0.5); expect(gridContainer.style.transform).toEqual('matrix(0.5, 0, 0, 0.5, -50, -50)'); - expect(gridContainer.style.left).toEqual('-1.6e+06px'); // -width . 2 - expect(gridContainer.style.top).toEqual('-1.2e+06px'); // -height / 2 + expect(gridContainer.style.left).toEqual('-160000px'); // -width . 2 + expect(gridContainer.style.top).toEqual('-120000px'); // -height / 2 graph.get('group').resetMatrix(); graph.translate(100, 100); expect(gridContainer.style.transform).toEqual('matrix(1, 0, 0, 1, 100, 100)'); - expect(gridContainer.style.left).toEqual('-1.6e+06px'); // -width . 2 - expect(gridContainer.style.top).toEqual('-1.2e+06px'); // -height / 2 + expect(gridContainer.style.left).toEqual('-160000px'); // -width . 2 + expect(gridContainer.style.top).toEqual('-120000px'); // -height / 2 graph.addItem('node', { x: -200, y: 200 }); graph.translate(100, 100); expect(gridContainer.style.transform).toEqual('matrix(1, 0, 0, 1, 200, 200)'); - expect(gridContainer.style.left).toEqual('-1.6e+06px'); // -width . 2 - expect(gridContainer.style.top).toEqual('-1.2e+06px'); // -height / 2 + expect(gridContainer.style.left).toEqual('-160000px'); // -width . 2 + expect(gridContainer.style.top).toEqual('-120000px'); // -height / 2 }); it('grid destroy', () => { const container = graph.get('container'); diff --git a/packages/site/docs/manual/middle/elements/nodes/jsx-node.zh.md b/packages/site/docs/manual/middle/elements/nodes/jsx-node.zh.md index 9c5773ed43..9971bc0783 100644 --- a/packages/site/docs/manual/middle/elements/nodes/jsx-node.zh.md +++ b/packages/site/docs/manual/middle/elements/nodes/jsx-node.zh.md @@ -126,7 +126,7 @@ const textXML = (cfg) => ` radius: [0, 0, 6, 6] }} keyshape="true" cursor="move"> - 'FULL' + FULL ${cfg.metric}: