UNPKG

@plait/graph-viz

Version:
750 lines (729 loc) 28.4 kB
import { cacheSelectedElements, getPointBetween, PlaitBoard, normalizePoint, drawCircle, createForeignObject, createG, createPath, PlaitElement, PlaitNode, getSelectedElements, RectangleClient, setSelectionOptions, PlaitPluginKey, toViewBoxPoint, toHostPoint, isHitElement, PlaitPointerType } from '@plait/core'; import { CommonElementFlavour, Generator, animate, linear } from '@plait/common'; import Graph from 'graphology'; import circular from 'graphology-layout/circular'; import forceAtlas2 from 'graphology-layout-forceatlas2'; const DEFAULT_STYLES = { fillStyle: 'solid', strokeWidth: 1 }; var EdgeDirection; (function (EdgeDirection) { EdgeDirection[EdgeDirection["IN"] = 0] = "IN"; EdgeDirection[EdgeDirection["OUT"] = 1] = "OUT"; EdgeDirection[EdgeDirection["NONE"] = 2] = "NONE"; })(EdgeDirection || (EdgeDirection = {})); const DEFAULT_EDGE_STYLES = { ...DEFAULT_STYLES, stroke: '#ddd' }; const DEFAULT_NODE_SIZE = 30; const DEFAULT_ACTIVE_NODE_SIZE_MULTIPLIER = 1.2; const DEFAULT_ACTIVE_WAVE_NODE_SIZE_MULTIPLIER = 1.5; const DEFAULT_NODE_LABEL_MARGIN_TOP = 4; const DEFAULT_NODE_LABEL_FONT_SIZE = 12; const DEFAULT_NODE_LABEL_WIDTH = 72; const DEFAULT_NODE_LABEL_HEIGHT = 22; 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;`; const NODE_LABEL_CLASS_NAME = 'force-atlas-node-label'; const SECOND_DEPTH_NODE_ALPHA = 0.5; const SECOND_DEPTH_LINE_ALPHA = 0.5; const ACTIVE_BACKGROUND_NODE_ALPHA = 0.1; const NODE_ICON_CLASS_NAME = 'force-atlas-node-icon'; const ACTIVE_NODE_ICON_CLASS_NAME = 'force-atlas-node-icon-active'; const NODE_ICON_FONT_SIZE = 16; const ACTIVE_NODE_ICON_FONT_SIZE = 18; const DEFAULT_NODE_BACKGROUND_COLOR = '#9c9cfb'; const DEFAULT_NODE_ICON_COLOR = '#fff'; const DEFAULT_NODE_STYLES = { ...DEFAULT_STYLES, fill: DEFAULT_NODE_BACKGROUND_COLOR, strokeWidth: 0 }; const DEFAULT_NODE_SCALING_RATIO = 20; const DEFAULT_LINE_STYLES = { color: { [EdgeDirection.IN]: '#73D897', [EdgeDirection.OUT]: '#6698FF', [EdgeDirection.NONE]: `#eee` }, opacity: { [EdgeDirection.IN]: 0.4, [EdgeDirection.OUT]: 0.4, [EdgeDirection.NONE]: 1 } }; const DEFAULT_EDGE_PARTICLE_SIZE = 4; const ForceAtlasElement = { isForceAtlas: (value) => { return value?.type === 'force-atlas'; }, isForceAtlasNodeElement: (value) => { return value && value.label && value.icon; }, isForceAtlasEdgeElement: (value) => { return value && value.source && value.target; } }; class ForceAtlasFlavour extends CommonElementFlavour { constructor() { super(); } initializeGraph() { this.graph = new Graph(); this.element.children?.forEach((child) => { if (ForceAtlasElement.isForceAtlasNodeElement(child)) { if (typeof child?.size === 'undefined') { child.size = DEFAULT_NODE_SIZE; } if (child.isActive) { cacheSelectedElements(this.board, [child]); } this.graph.addNode(child.id, child); } else if (ForceAtlasElement.isForceAtlasEdgeElement(child)) { this.graph.addEdge(child.source, child.target); } }); circular.assign(this.graph); const nodeCount = this.graph.nodes().length; const settings = forceAtlas2.inferSettings(this.graph); settings.scalingRatio = 450; if (nodeCount > 5) { settings.gravity = 0.2; settings.adjustSizes = false; } else { settings.gravity = 0; settings.adjustSizes = true; } const positions = forceAtlas2(this.graph, { iterations: 500, settings }); this.element.children?.forEach((child) => { if (ForceAtlasElement.isForceAtlasNodeElement(child)) { const pos = positions[child.id]; child.points = [[pos.x, pos.y]]; } }); } initialize() { super.initialize(); this.initializeGraph(); } onContextChanged(value, previous) { } updateText(previousElement, currentElement) { } destroy() { super.destroy(); } } // Credits to perfect-arrows // https://github.com/steveruizok/perfect-arrows/blob/master/src/lib/utils.ts const PI = Math.PI; /** * Modulate a value between two ranges. * @param value * @param rangeA from [low, high] * @param rangeB to [low, high] * @param clamp */ function modulate(value, rangeA, rangeB, clamp = false) { const [fromLow, fromHigh] = rangeA; const [toLow, toHigh] = rangeB; const result = toLow + ((value - fromLow) / (fromHigh - fromLow)) * (toHigh - toLow); if (clamp === true) { if (toLow < toHigh) { if (result < toLow) { return toLow; } if (result > toHigh) { return toHigh; } } else { if (result > toLow) { return toLow; } if (result < toHigh) { return toHigh; } } } return result; } /** * Rotate a point around a center. * @param x The x-axis coordinate of the point. * @param y The y-axis coordinate of the point. * @param cx The x-axis coordinate of the point to rotate round. * @param cy The y-axis coordinate of the point to rotate round. * @param angle The distance (in radians) to rotate. */ function rotatePoint(x, y, cx, cy, angle) { const s = Math.sin(angle); const c = Math.cos(angle); const px = x - cx; const py = y - cy; const nx = px * c - py * s; const ny = px * s + py * c; return [nx + cx, ny + cy]; } /** * Get the distance between two points. * @param x0 The x-axis coordinate of the first point. * @param y0 The y-axis coordinate of the first point. * @param x1 The x-axis coordinate of the second point. * @param y1 The y-axis coordinate of the second point. */ function getDistance(x0, y0, x1, y1) { return Math.hypot(y1 - y0, x1 - x0); } /** * Get an angle (radians) between two points. * @param x0 The x-axis coordinate of the first point. * @param y0 The y-axis coordinate of the first point. * @param x1 The x-axis coordinate of the second point. * @param y1 The y-axis coordinate of the second point. */ function getAngle(x0, y0, x1, y1) { return Math.atan2(y1 - y0, x1 - x0); } /** * Move a point in an angle by a distance. * @param x0 * @param y0 * @param a angle (radians) * @param d distance */ function projectPoint(x0, y0, a, d) { return [Math.cos(a) * d + x0, Math.sin(a) * d + y0]; } /** * Get the sector of an angle (e.g. quadrant, octant) * @param a The angle to check. * @param s The number of sectors to check. */ function getSector(a, s = 8) { return Math.floor(s * (0.5 + ((a / (PI * 2)) % s))); } /** * Get a normal value representing how close two points are from being at a 45 degree angle. * @param x0 The x-axis coordinate of the first point. * @param y0 The y-axis coordinate of the first point. * @param x1 The x-axis coordinate of the second point. * @param y1 The y-axis coordinate of the second point. */ function getAngliness(x0, y0, x1, y1) { return Math.abs((x1 - x0) / 2 / ((y1 - y0) / 2)); } // Credits to perfect-arrows // https://github.com/steveruizok/perfect-arrows/blob/master/src/lib/getArrow.ts /** * getArrow * Get the points for a linking line between two points. * @description Draw an arrow between two points. * @param x0 The x position of the "from" point. * @param y0 The y position of the "from" point. * @param x1 The x position of the "to" point. * @param y1 The y position of the "to" point. * @param options Additional options for computing the line. * @returns [sx, sy, cx, cy, e1, e2, ae, as, ac] * @example * const arrow = getArrow(0, 0, 100, 200, { bow: 0 stretch: .5 stretchMin: 0 stretchMax: 420 padStart: 0 padEnd: 0 flip: false straights: true * }) * * const [ * startX, startY, * controlX, controlY, * endX, endY, * endAngle, * startAngle, * controlAngle * ] = arrow */ function getArrow(x0, y0, x1, y1, options = {}) { const { bow = 0, stretch = 0.5, stretchMin = 0, stretchMax = 420, padStart = 0, padEnd = 0, flip = false, straights = true } = options; const angle = getAngle(x0, y0, x1, y1); const dist = getDistance(x0, y0, x1, y1); const angliness = getAngliness(x0, y0, x1, y1); // Step 0 ⤜⤏ Should the arrow be straight? if (dist < (padStart + padEnd) * 2 || // Too short (bow === 0 && stretch === 0) || // No bow, no stretch (straights && [0, 1, Infinity].includes(angliness)) // 45 degree angle ) { // ⤜⤏ Arrow is straight! Just pad start and end points. // Padding distances const ps = Math.max(0, Math.min(dist - padStart, padStart)); const pe = Math.max(0, Math.min(dist - ps, padEnd)); // Move start point toward end point let [px0, py0] = projectPoint(x0, y0, angle, ps); // Move end point toward start point let [px1, py1] = projectPoint(x1, y1, angle + Math.PI, pe); // Get midpoint between new points const [mx, my] = getPointBetween(px0, py0, px1, py1, 0.5); return [px0, py0, mx, my, px1, py1, angle, angle, angle]; } // ⤜⤏ Arrow is an arc! // Is the arc clockwise or counterclockwise? let rot = (getSector(angle) % 2 === 0 ? 1 : -1) * (flip ? -1 : 1); // Calculate how much the line should "bow" away from center const arc = bow + modulate(dist, [stretchMin, stretchMax], [1, 0], true) * stretch; // Step 1 ⤜⤏ Find padded points. // Get midpoint. const [mx, my] = getPointBetween(x0, y0, x1, y1, 0.5); // Get control point. let [cx, cy] = getPointBetween(x0, y0, x1, y1, 0.5 - arc); // Rotate control point (clockwise or counterclockwise). [cx, cy] = rotatePoint(cx, cy, mx, my, (Math.PI / 2) * rot); // Get padded start point. const a0 = getAngle(x0, y0, cx, cy); const [px0, py0] = projectPoint(x0, y0, a0, padStart); // Get padded end point. const a1 = getAngle(x1, y1, cx, cy); const [px1, py1] = projectPoint(x1, y1, a1, padEnd); // Step 2 ⤜⤏ Find start and end angles. // Start angle const as = getAngle(cx, cy, x0, y0); // End angle const ae = getAngle(cx, cy, x1, y1); // Step 3 ⤜⤏ Find control point for padded points. // Get midpoint between padded start / end points. const [mx1, my1] = getPointBetween(px0, py0, px1, py1, 0.5); // Get control point for padded start / end points. let [cx1, cy1] = getPointBetween(px0, py0, px1, py1, 0.5 - arc); // Rotate control point (clockwise or counterclockwise). [cx1, cy1] = rotatePoint(cx1, cy1, mx1, my1, (Math.PI / 2) * rot); // Finally, average the two control points. let [cx2, cy2] = getPointBetween(cx, cy, cx1, cy1, 0.5); return [px0, py0, cx2, cy2, px1, py1, ae, as, angle]; } function drawNode(board, node, point, options) { const roughSVG = PlaitBoard.getRoughSVG(board); const nodeStyles = { ...DEFAULT_NODE_STYLES, ...(node.styles || {}) }; let { x, y } = normalizePoint(point); let diameter = node.size ?? DEFAULT_NODE_SIZE; if (options.isActive) { diameter = diameter * DEFAULT_ACTIVE_NODE_SIZE_MULTIPLIER; } const nodeG = drawCircle(roughSVG, [x, y], diameter, nodeStyles); 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); textForeignObject.setAttribute('y', `${y + waveDiameter / 2}`); } else { textForeignObject.setAttribute('y', `${y + diameter / 2}`); nodeG.setAttribute('opacity', (options.opacity ?? 1).toString()); } if (options.iconG) { nodeG.append(options.iconG); } nodeG.append(textForeignObject); return nodeG; } function drawEdge(startPoint, endPoint, direction, isMutual, isTargetSelf) { 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: nodeRadius, padStart: nodeRadius }); const [sx, sy, cx, cy, ex, ey, ae, as, ec] = arrow; const g = createG(); const path = createPath(); if (!isTargetSelf) { path.setAttribute('d', `M${sx},${sy} Q${cx},${cy} ${ex},${ey}`); } else { const x = startPoint[0]; const y = startPoint[1]; const besselX = 40; const besselY = 75; const angle = 55; const angleRad = (angle * Math.PI) / 180; const offsetX = nodeRadius * Math.cos(angleRad); const offsetY = nodeRadius * Math.sin(angleRad); path.setAttribute('d', `M ${x - offsetX},${y - offsetY} C ${x - besselX},${y - besselY}, ${x + besselX} ${y - besselY} ${x + offsetX},${y - offsetY}`); } path.setAttribute('fill', 'none'); path.setAttribute('stroke', DEFAULT_LINE_STYLES.color[direction]); path.setAttribute('opacity', DEFAULT_LINE_STYLES.opacity[direction].toString()); g.append(path); return { g, path }; } function drawParticle(board, startPoint, direction) { const roughSVG = PlaitBoard.getRoughSVG(board); const pointG = drawCircle(roughSVG, [0, 0], DEFAULT_EDGE_PARTICLE_SIZE, { ...DEFAULT_STYLES, strokeWidth: 0, fill: DEFAULT_LINE_STYLES.color[direction] }); pointG.setAttribute('transform', `translate(${startPoint[0]}, ${startPoint[1]})`); return pointG; } class ForceAtlasEdgeGenerator extends Generator { static { this.key = 'force-atlas-edge'; } constructor(board) { super(board); } canDraw(element) { return true; } draw(element, data) { const edgeG = createG(); 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); edgeElement.g.append(particle); this.particleAnimation = playEdgeParticleAnimate(edgeElement.path, particle); } return edgeG; } destroy() { super.destroy(); this.particleAnimation?.stop(); } } function getEdges(forceAtlasElement, andCallBack) { return forceAtlasElement.children?.filter((f) => ForceAtlasElement.isForceAtlasEdgeElement(f) && (andCallBack?.(f) ?? true)); } function getEdgeById(id, forceAtlasElement) { const edge = getEdges(forceAtlasElement, (e) => e.id === id)?.[0]; if (!edge) { throw new Error('can not find edge.'); } return edge; } function getEdgesInSourceOrTarget(id, forceAtlasElement) { const edges = getEdges(forceAtlasElement, (edge) => edge.source === id || edge.target === id); return edges; } function getEdgeGenerator(edge) { const edgeRef = PlaitElement.getElementRef(edge); return edgeRef.getGenerator(ForceAtlasEdgeGenerator.key); } function getEdgeDirection(isSourceActive, isTargetActive) { if (isSourceActive) { return EdgeDirection.OUT; } else if (isTargetActive) { return EdgeDirection.IN; } return EdgeDirection.NONE; } function getEdgeGeneratorData(edge, board) { const forceAtlasElement = PlaitNode.parent(board, PlaitBoard.findPath(board, edge)); const sourceNode = getNodeById(edge.source, forceAtlasElement); const targetNode = getNodeById(edge.target, forceAtlasElement); if (!sourceNode?.points || !targetNode?.points) { throw new Error("Source or target node doesn't have points"); } const startPoint = sourceNode.points[0]; const endPoint = targetNode.points[0]; const selectElements = getSelectedElements(board); const isSourceActive = getIsNodeActive(sourceNode.id, selectElements); const isTargetActive = getIsNodeActive(targetNode.id, selectElements); const direction = getEdgeDirection(isSourceActive, isTargetActive); return { startPoint, endPoint, direction, isSourceActive, isTargetActive, isTargetSelf: sourceNode.id === targetNode.id }; } function playEdgeParticleAnimate(path, pointG) { const pathLength = path.getTotalLength(); let anim = animate((t) => { const point = path.getPointAtLength(t * pathLength); pointG.setAttribute('transform', `translate(${point.x}, ${point.y})`); }, 1000, linear, () => { anim = playEdgeParticleAnimate(path, pointG); }); return { stop: () => { anim.stop(); }, start: () => { anim.start(); } }; } function getNodes(forceAtlasElement, andBack) { return forceAtlasElement.children?.filter((f) => ForceAtlasElement.isForceAtlasNodeElement(f) && (andBack?.(f) ?? true)); } function getNodeById(id, forceAtlasElement) { const node = getNodes(forceAtlasElement, (node) => node.id === id)?.[0]; if (!node) { throw new Error('can not find node.'); } return node; } function getIsNodeActive(id, selectElements) { return selectElements.some((node) => node.id === id); } function isHitNode(node, point) { const { x, y } = normalizePoint(node.points[0]); const size = node.size; const hitFlowNode = RectangleClient.isHit(RectangleClient.getRectangleByPoints(point), { x: x - size / 2, y: y - size / 2, width: size, height: size }); return hitFlowNode; } function getAssociatedNodesById(id, forceAtlasElement) { const edges = getEdgesInSourceOrTarget(id, forceAtlasElement); const nodes = []; edges.forEach((edge) => { nodes.push(getNodeById(edge.source, forceAtlasElement)); nodes.push(getNodeById(edge.target, forceAtlasElement)); }); return nodes; } function getNodeGenerator(node) { const edgeRef = PlaitElement.getElementRef(node); return edgeRef.getGenerator(ForceAtlasNodeGenerator.key); } function isFirstDepthNode(currentNodeId, activeNodeId, forceAtlasElement) { const edges = getEdges(forceAtlasElement); return edges.some((s) => (s.source === activeNodeId && s.target === currentNodeId) || (s.target === activeNodeId && s.source === currentNodeId)); } function getNodeIcon(node) { const iconItem = typeof node.icon === 'object' && node.icon.name ? node.icon : null; return { name: iconItem ? iconItem.name : node.icon, fontSize: (iconItem && iconItem.fontSize) || NODE_ICON_FONT_SIZE, color: (iconItem && iconItem.color) || DEFAULT_NODE_ICON_COLOR }; } class ForceAtlasNodeGenerator extends Generator { static { this.key = 'force-atlas-node'; } constructor(board) { super(board); } canDraw(element) { return true; } draw(element, data) { const iconRef = this.drawIcon(element, data); return drawNode(this.board, element, element?.points?.[0] || [0, 0], { ...data, iconG: iconRef.iconG }); } drawIcon(element, data) { const iconG = createG(); let { x, y } = normalizePoint(element.points?.[0] || [0, 0]); const size = element.size; const foreignObject = createForeignObject(x - size / 2, y - size / 2, size, size); iconG.append(foreignObject); const container = document.createElement('div'); container.classList.add(NODE_ICON_CLASS_NAME); if (data.isActive) { container.classList.add(ACTIVE_NODE_ICON_CLASS_NAME); } foreignObject.append(container); const nodeIcon = getNodeIcon(element); const props = { iconItem: { name: nodeIcon.name, fontSize: data.isActive ? ACTIVE_NODE_ICON_FONT_SIZE : nodeIcon.fontSize, color: nodeIcon.color }, board: this.board, element: element }; const ref = this.board.renderNodeIcon(container, props); return { ref, iconG }; } } class ForceAtlasNodeFlavour extends CommonElementFlavour { constructor() { super(); } initializeGenerator() { this.nodeGenerator = new ForceAtlasNodeGenerator(this.board); this.getRef().addGenerator(ForceAtlasNodeGenerator.key, this.nodeGenerator); } initialize() { super.initialize(); this.initializeGenerator(); const parent = PlaitNode.parent(this.board, PlaitBoard.findPath(this.board, this.element)); const selectElements = getSelectedElements(this.board); const activeNodeId = selectElements[0]?.id; const isActive = activeNodeId === this.element.id; this.nodeGenerator.processDrawing(this.element, this.getElementG(), { isActive, opacity: isFirstDepthNode(this.element.id, activeNodeId, parent) ? 1 : SECOND_DEPTH_NODE_ALPHA }); } onContextChanged(value, previous) { if (value !== previous && value.selected !== previous.selected) { const parent = value.parent; if (value.selected) { cacheSelectedElements(this.board, [value.element]); } const selectElements = getSelectedElements(this.board); const nodes = getNodes(parent); nodes.forEach((node) => { const nodeGenerator = getNodeGenerator(node); nodeGenerator.destroy(); const isFirstDepth = selectElements.length > 0 && isFirstDepthNode(node.id, selectElements[0].id, parent); nodeGenerator.processDrawing(node, this.getElementG(), { isActive: selectElements?.[0]?.id === node.id, opacity: selectElements.length === 0 ? 1 : isFirstDepth ? 1 : SECOND_DEPTH_NODE_ALPHA }); }); const associatedEdges = getEdgesInSourceOrTarget(value.element.id, parent); associatedEdges.forEach((edge) => { const edgeGenerator = getEdgeGenerator(edge); edgeGenerator.destroy(); edgeGenerator.processDrawing(edge, PlaitBoard.getElementLowerHost(this.board), getEdgeGeneratorData(edge, this.board)); }); } } updateText(previousElement, currentElement) { } destroy() { super.destroy(); } } class ForceAtlasEdgeFlavour extends CommonElementFlavour { constructor() { super(); } initializeGenerator() { this.edgeGenerator = new ForceAtlasEdgeGenerator(this.board); this.getRef().addGenerator(ForceAtlasEdgeGenerator.key, this.edgeGenerator); } initialize() { super.initialize(); this.initializeGenerator(); this.edgeGenerator.processDrawing(this.element, PlaitBoard.getElementLowerHost(this.board), getEdgeGeneratorData(this.element, this.board)); } onContextChanged(value, previous) { } updateText(previousElement, currentElement) { } destroy() { super.destroy(); this.edgeGenerator.destroy(); } } const withNodeIcon = (board) => { const newBoard = board; newBoard.renderNodeIcon = (container, props) => { throw new Error('No implementation for renderLabeIcon method.'); }; return newBoard; }; const withForceAtlas = (board) => { const { drawElement, getRectangle, isRectangleHit, isHit, isInsidePoint, isMovable, isAlign, getRelatedFragment } = board; board.drawElement = (context) => { if (ForceAtlasElement.isForceAtlas(context.element)) { return ForceAtlasFlavour; } else if (ForceAtlasElement.isForceAtlasNodeElement(context.element)) { return ForceAtlasNodeFlavour; } else if (ForceAtlasElement.isForceAtlasEdgeElement(context.element)) { return ForceAtlasEdgeFlavour; } return drawElement(context); }; board.getRectangle = (element) => { if (element.type === 'force-atlas') { return { width: 0, height: 0, x: 0, y: 0 }; } else if (ForceAtlasElement.isForceAtlasNodeElement(element)) { return RectangleClient.getRectangleByPoints(element.points || []); } else if (ForceAtlasElement.isForceAtlasEdgeElement(element)) { return { width: 0, height: 0, x: 0, y: 0 }; } return getRectangle(element); }; board.isRectangleHit = (element, selection) => { return isRectangleHit(element, selection); }; board.isRectangleHit = (element, range) => { if (ForceAtlasElement.isForceAtlasNodeElement(element)) { return isHitNode(element, [range.anchor, range.focus]); } return isRectangleHit(element, range); }; board.isHit = (element, point) => { if (ForceAtlasElement.isForceAtlasNodeElement(element)) { return isHitNode(element, [point, point]); } return isHit(element, point); }; board.isInsidePoint = (element, point) => { return isInsidePoint(element, point); }; setSelectionOptions(board, { isMultipleSelection: false, isPreventClearSelection: true }); board.setPluginOptions(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); }; class ForceAtlasNodeIconBaseComponent { initialize() { if (!this.iconItem.fontSize) { this.iconItem.fontSize = NODE_ICON_FONT_SIZE; } if (!this.iconItem.color) { this.iconItem.color = DEFAULT_NODE_ICON_COLOR; } this.nativeElement().style.fontSize = `${this.iconItem.fontSize}px`; this.nativeElement().style.color = `${this.iconItem.color}`; this.nativeElement().classList.add(NODE_ICON_CLASS_NAME); } } /* * Public API Surface of utils */ /** * Generated bundle index. Do not edit. */ export { ACTIVE_BACKGROUND_NODE_ALPHA, ACTIVE_NODE_ICON_CLASS_NAME, ACTIVE_NODE_ICON_FONT_SIZE, DEFAULT_ACTIVE_NODE_SIZE_MULTIPLIER, DEFAULT_ACTIVE_WAVE_NODE_SIZE_MULTIPLIER, DEFAULT_EDGE_PARTICLE_SIZE, DEFAULT_EDGE_STYLES, DEFAULT_LINE_STYLES, DEFAULT_NODE_BACKGROUND_COLOR, DEFAULT_NODE_ICON_COLOR, DEFAULT_NODE_LABEL_FONT_SIZE, DEFAULT_NODE_LABEL_HEIGHT, DEFAULT_NODE_LABEL_MARGIN_TOP, DEFAULT_NODE_LABEL_STYLE, DEFAULT_NODE_LABEL_WIDTH, DEFAULT_NODE_SCALING_RATIO, DEFAULT_NODE_SIZE, DEFAULT_NODE_STYLES, EdgeDirection, ForceAtlasElement, ForceAtlasNodeIconBaseComponent, NODE_ICON_CLASS_NAME, NODE_ICON_FONT_SIZE, NODE_LABEL_CLASS_NAME, SECOND_DEPTH_LINE_ALPHA, SECOND_DEPTH_NODE_ALPHA, withForceAtlas, withNodeIcon }; //# sourceMappingURL=plait-graph-viz.mjs.map