Skip to content

Commit

Permalink
fix(graph-viz): centering of selected node, node title ellipses and t…
Browse files Browse the repository at this point in the history
…arget self edge (#952)
  • Loading branch information
Xwatson authored Jul 11, 2024
1 parent 6ec85fe commit 37dad67
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 30 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-pigs-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plait/graph-viz': minor
---

fix centering of selected node, node title ellipses, displays the line where the target is itself
8 changes: 8 additions & 0 deletions packages/graph-viz/src/force-atlas/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ export const DEFAULT_NODE_LABEL_MARGIN_TOP = 4;

export const DEFAULT_NODE_LABEL_FONT_SIZE = 12;

export const DEFAULT_NODE_LABEL_WIDTH = 150;

export const DEFAULT_NODE_LABEL_HEIGHT = 22;

export const DEFAULT_NODE_LABEL_STYLE = `user-select:none;max-width:${DEFAULT_NODE_LABEL_WIDTH}px;text-align:center;line-height:${DEFAULT_NODE_LABEL_HEIGHT}px;font-size:${DEFAULT_NODE_LABEL_FONT_SIZE}px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`;

export const NODE_LABEL_CLASS_NAME = 'force-atlas-node-label';

export const SECOND_DEPTH_NODE_ALPHA = 0.5;

export const SECOND_DEPTH_LINE_ALPHA = 0.5;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ export class ForceAtlasEdgeGenerator extends Generator<ForceAtlasEdgeElement, Ed

draw(element: ForceAtlasEdgeElement, data: EdgeGeneratorData) {
const edgeG = createG();
const edgeElement = drawEdge(data.startPoint, data.endPoint, data.direction, data.isSourceActive && data.isTargetActive);
const edgeElement = drawEdge(
data.startPoint,
data.endPoint,
data.direction,
data.isSourceActive && data.isTargetActive,
data.isTargetSelf
);
edgeG.append(edgeElement.g);
if (data.direction !== EdgeDirection.NONE) {
const particle = drawParticle(this.board, data.startPoint, data.direction);
Expand Down
1 change: 1 addition & 0 deletions packages/graph-viz/src/force-atlas/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface EdgeGeneratorData extends GeneratorExtraData {
isSourceActive: boolean;
isTargetActive: boolean;
direction: EdgeDirection;
isTargetSelf?: boolean;
}

export interface NodeGeneratorData extends GeneratorExtraData {
Expand Down
53 changes: 36 additions & 17 deletions packages/graph-viz/src/force-atlas/utils/draw.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { EdgeDirection, NodeStyles } from '../types';
import { NS, PlaitBoard, Point, createForeignObject, createG, createPath, drawCircle, normalizePoint } from '@plait/core';
import { PlaitBoard, Point, createForeignObject, createG, createPath, drawCircle, normalizePoint } from '@plait/core';
import getArrow from '../../perfect-arrows/get-arrow';
import {
ACTIVE_BACKGROUND_NODE_ALPHA,
DEFAULT_ACTIVE_NODE_SIZE_MULTIPLIER,
DEFAULT_ACTIVE_WAVE_NODE_SIZE_MULTIPLIER,
DEFAULT_LINE_STYLES,
DEFAULT_NODE_LABEL_FONT_SIZE,
DEFAULT_NODE_LABEL_MARGIN_TOP,
DEFAULT_NODE_LABEL_HEIGHT,
DEFAULT_NODE_LABEL_STYLE,
DEFAULT_NODE_LABEL_WIDTH,
DEFAULT_NODE_SIZE,
DEFAULT_NODE_STYLES,
NODE_LABEL_CLASS_NAME,
SECOND_DEPTH_NODE_ALPHA
} from '../constants';
import { DEFAULT_STYLES } from '../../constants/default';
Expand All @@ -35,40 +37,57 @@ export function drawNode(
if (options.iconG) {
nodeG.append(options.iconG);
}
const text = document.createElementNS(NS, 'text');
text.textContent = node.label || '';
text.setAttribute('text-anchor', `middle`);
text.setAttribute('dominant-baseline', `hanging`);
text.setAttribute('x', `${x}`);
text.setAttribute('font-size', `${DEFAULT_NODE_LABEL_FONT_SIZE}px`);
text.setAttribute('style', `user-select: none;`);
const labelWidth = node.styles?.labelWidth ?? DEFAULT_NODE_LABEL_WIDTH;
const labelHeight = node.styles?.labelHeight ?? DEFAULT_NODE_LABEL_HEIGHT;
const textForeignObject = createForeignObject(x - labelWidth / 2, y, labelWidth, labelHeight);
const textContainer = document.createElement('div');
textContainer.classList.add(NODE_LABEL_CLASS_NAME);
textContainer.setAttribute('style', DEFAULT_NODE_LABEL_STYLE);
const text = document.createElement('span');
text.innerText = node.label;
textContainer.append(text);
textForeignObject.append(textContainer);
if (options.isActive) {
const waveDiameter = diameter * DEFAULT_ACTIVE_WAVE_NODE_SIZE_MULTIPLIER;
const waveCircle = drawCircle(roughSVG, [x, y], waveDiameter, nodeStyles);
waveCircle.setAttribute('opacity', ACTIVE_BACKGROUND_NODE_ALPHA.toString());
nodeG.append(waveCircle);
text.setAttribute('y', `${y + waveDiameter / 2 + DEFAULT_NODE_LABEL_MARGIN_TOP}`);
textForeignObject.setAttribute('y', `${y + waveDiameter / 2}`);
} else {
if (!options.isFirstDepth) {
nodeG.setAttribute('opacity', SECOND_DEPTH_NODE_ALPHA.toString());
}
text.setAttribute('y', `${y + diameter / 2 + DEFAULT_NODE_LABEL_MARGIN_TOP}`);
textForeignObject.setAttribute('y', `${y + diameter / 2}`);
}
nodeG.append(text);
nodeG.append(textForeignObject);
return nodeG;
}

export function drawEdge(startPoint: Point, endPoint: Point, direction: EdgeDirection, isMutual: boolean) {
export function drawEdge(startPoint: Point, endPoint: Point, direction: EdgeDirection, isMutual: boolean, isTargetSelf?: boolean) {
const nodeRadius = DEFAULT_NODE_SIZE / 2;
const arrow = getArrow(startPoint[0], startPoint[1], endPoint[0], endPoint[1], {
stretch: 0.4,
flip: direction === EdgeDirection.NONE ? false : isMutual,
padEnd: DEFAULT_NODE_SIZE / 2,
padStart: DEFAULT_NODE_SIZE / 2
padEnd: nodeRadius,
padStart: nodeRadius
});
const [sx, sy, cx, cy, ex, ey, ae, as, ec] = arrow;
const g = createG();
const path = createPath();
path.setAttribute('d', `M${sx},${sy} Q${cx},${cy} ${ex},${ey}`);
if (!isTargetSelf) {
path.setAttribute('d', `M${sx},${sy} Q${cx},${cy} ${ex},${ey}`);
} else {
const angle = 55;
const angleRad = (angle * Math.PI) / 180;
const x = nodeRadius * Math.cos(angleRad);
const y = nodeRadius * Math.sin(angleRad);
path.setAttribute(
'd',
`M -${x},-${y}
C -45,-85, 45 -85
${x},-${y}`
);
}
path.setAttribute('fill', 'none');
path.setAttribute('stroke', DEFAULT_LINE_STYLES.color[direction]);
g.append(path);
Expand Down
4 changes: 3 additions & 1 deletion packages/graph-viz/src/force-atlas/utils/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ export function getEdgeGeneratorData(edge: ForceAtlasEdgeElement, board: PlaitBo
const isSourceActive = getIsNodeActive(sourceNode.id, selectElements);
const isTargetActive = getIsNodeActive(targetNode.id, selectElements);
const direction = getEdgeDirection(isSourceActive, isTargetActive);

return {
startPoint,
endPoint,
direction,
isSourceActive,
isTargetActive
isTargetActive,
isTargetSelf: sourceNode.id === targetNode.id
};
}

Expand Down
15 changes: 13 additions & 2 deletions packages/graph-viz/src/force-atlas/utils/node.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { PlaitElement, Point, RectangleClient, normalizePoint } from '@plait/core';
import {
PlaitBoard,
PlaitElement,
Point,
RectangleClient,
Transforms,
clearViewportOrigination,
getRealScrollBarWidth,
getSelectedElements,
getViewBoxCenterPoint,
normalizePoint
} from '@plait/core';
import { ForceAtlasElement, ForceAtlasNodeElement } from '../../interfaces';
import { getEdges, getEdgesInSourceOrTarget } from './edge';
import { PlaitCommonElementRef } from '@plait/common';
import { animate, linear, PlaitCommonElementRef } from '@plait/common';
import { ForceAtlasNodeGenerator } from '../generators/node.generator';
import { DEFAULT_NODE_ICON_COLOR, NODE_ICON_FONT_SIZE } from '../constants';

Expand Down
23 changes: 14 additions & 9 deletions packages/graph-viz/src/force-atlas/with-force-atlas.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import {
isHitElement,
PlaitBoard,
PlaitElement,
PlaitOptionsBoard,
PlaitPluginElementContext,
PlaitPluginKey,
PlaitPointerType,
Point,
RectangleClient,
Selection,
WithSelectionPluginOptions,
setSelectionOptions
setSelectionOptions,
toHostPoint,
toViewBoxPoint,
WithHandPluginOptions
} from '@plait/core';
import { ForceAtlasFlavour } from './force-atlas.flavour';
import { ForceAtlasNodeFlavour } from './node.flavour';
Expand Down Expand Up @@ -73,14 +77,15 @@ export const withForceAtlas = (board: PlaitBoard) => {
return isInsidePoint(element, point);
};

board.isMovable = element => {
if (ForceAtlasElement.isForceAtlasNodeElement(element)) {
return true;
}
return isMovable(element);
};
setSelectionOptions(board, { isMultipleSelection: false, isPreventClearSelection: true });

setSelectionOptions(board, { isMultipleSelection: false });
(board as PlaitOptionsBoard).setPluginOptions<WithHandPluginOptions>(PlaitPluginKey.withHand, {
isHandMode: (board, event) => {
const point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y));
const isHitTarget = isHitElement(board, point);
return PlaitBoard.isPointer(board, PlaitPointerType.selection) && !isHitTarget;
}
});

return withNodeIcon(board);
};
5 changes: 5 additions & 0 deletions src/app/graph-viz/mock-force-atlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export const mockForceAtlasData: ForceAtlasElement[] = [
id: '1-5',
source: '4',
target: '1'
},
{
id: '1-6',
source: '1',
target: '1'
}
]
}
Expand Down

0 comments on commit 37dad67

Please sign in to comment.