Skip to content

Commit

Permalink
Pull fit to node logic into functions
Browse files Browse the repository at this point in the history
  • Loading branch information
ghsteff committed Jul 8, 2024
1 parent 28047d5 commit 94f1710
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 44 deletions.
2 changes: 0 additions & 2 deletions docs/Advanced/Refs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,6 @@ export interface CanvasRef {
*/
setScrollXY?: (xy: [number, number], animated?: boolean) => void;

observe: (el: HTMLDivElement) => void;

/**
* Factor of zoom.
*/
Expand Down
37 changes: 5 additions & 32 deletions src/layout/useLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import useDimensions from 'react-cool-dimensions';
import isEqual from 'react-fast-compare';
import { CanvasPosition, EdgeData, NodeData } from '../types';
import { CanvasDirection, ElkCanvasLayoutOptions, elkLayout } from './elkLayout';
import { findChildCount, findNode } from './utils';
import { calculateScrollPosition, calculateZoom, findNode } from './utils';

export interface ElkRoot {
x?: number;
Expand Down Expand Up @@ -220,43 +220,16 @@ export const useLayout = ({ maxWidth, maxHeight, nodes = [], edges = [], fit, pa
(nodeId: string, animated = true) => {
if (layout && layout.children) {
const node = findNode(layout.children, nodeId);
const childCount = findChildCount(node);

if (node) {
// center the chart
positionVector(CanvasPosition.CENTER);

const maxViewportCoverage = 0.9;
const minViewportCoverage = 0.2;
const updatedZoom = calculateZoom({ node, viewportWidth: width, viewportHeight: height, maxViewportCoverage: 0.9, minViewportCoverage: 0.2 });
const scrollPosition = calculateScrollPosition({ node, viewportWidth: width, viewportHeight: height, canvasWidth, canvasHeight, chartWidth: layout.width, chartHeight: layout.height, zoom: updatedZoom });

// viewport coverage is the percentage of the viewport that the node will take up
// nodes with more children look better when they take up more of the viewport
const viewportCoverage = Math.min(maxViewportCoverage, Math.max(minViewportCoverage, minViewportCoverage + childCount * 0.1));

const updatedHorizontalZoom = (viewportCoverage * width) / node.width;
const updatedVerticalZoom = (viewportCoverage * height) / node.height;
const updatedZoom = Math.min(updatedHorizontalZoom, updatedVerticalZoom);

setZoom(updatedZoom - 1);

// get updated node dimensions because they change based on the zoom level
const updatedNodeWidth = node.width * updatedZoom;
const updatedNodeHeight = node.height * updatedZoom;

// the chart is centered so we can assume the x and y positions
const chartPosition = {
x: (canvasWidth - layout.width * updatedZoom) / 2,
y: (canvasHeight - layout.height * updatedZoom) / 2
};

const nodeCenterXPosition = chartPosition.x + node.x * updatedZoom + updatedNodeWidth / 2;
const nodeCenterYPosition = chartPosition.y + node.y * updatedZoom + updatedNodeHeight / 2;

// scroll to the spot that centers the node in the viewport
const scrollX = nodeCenterXPosition - width / 2;
const scrollY = nodeCenterYPosition - height / 2;

scrollToXY([scrollX, scrollY], animated);
setZoom(updatedZoom);
scrollToXY(scrollPosition, animated);
}
}
},
Expand Down
27 changes: 24 additions & 3 deletions src/layout/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parsePadding, findNode, findChildCount } from './utils';
import { parsePadding, findNode, getChildCount, calculateZoom, calculateScrollPosition } from './utils';

test('should set all sides to input number, when a number is provided', () => {
const expectedPadding = {
Expand Down Expand Up @@ -50,7 +50,7 @@ test('should find a node by id', () => {
expect(node).toEqual({ x: 0, y: 0, id: '4', children: [] });
});

test('should find the number of children a node has', () => {
test('should get the number of children a node has', () => {
const node = {
x: 0,
y: 0,
Expand All @@ -60,7 +60,28 @@ test('should find the number of children a node has', () => {
{ x: 0, y: 0, id: '2', children: [{ x: 0, y: 0, id: '3', children: [] }] }
]
};
const count = findChildCount(node);
const count = getChildCount(node);

expect(count).toEqual(3);
});

test('should calculate the zoom for a node', () => {
const node = { width: 100, height: 100, x: 0, y: 0, id: '1' };
const zoom = calculateZoom({ node, viewportWidth: 1000, viewportHeight: 1000, minViewportCoverage: 0.2, maxViewportCoverage: 0.9 });

expect(zoom).toEqual(1);
});

test('should calculate the zoom for a node with many children', () => {
const node = { width: 100, height: 100, x: 0, y: 0, id: '0', children: [{ x: 0, y: 0, id: '1', children: [{ x: 0, y: 0, id: '2', children: [{ x: 0, y: 0, id: '3', children: [] }] }] }] };
const zoom = calculateZoom({ node, viewportWidth: 1000, viewportHeight: 1000, minViewportCoverage: 0.2, maxViewportCoverage: 0.9 });

expect(zoom).toEqual(4);
});

test('should calculate the scroll position for a node', () => {
const node = { width: 100, height: 100, x: 0, y: 0, id: '0' };
const scrollPosition = calculateScrollPosition({ node, viewportWidth: 1000, viewportHeight: 1000, canvasWidth: 2000, canvasHeight: 2000, chartWidth: 500, chartHeight: 500, zoom: 1 });

expect(scrollPosition).toEqual([300, 300]);
});
72 changes: 65 additions & 7 deletions src/layout/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,69 @@ export const findNode = (nodes: LayoutNodeData[], nodeId: string): any | undefin
* @param node - The node to search through
* @returns The number of children
*/
export const findChildCount = (node: LayoutNodeData): number => {
return node.children.reduce((acc, child) => {
if (child.children) {
return acc + 1 + findChildCount(child);
}
return acc + 1;
}, 0);
export const getChildCount = (node: LayoutNodeData): number => {
return (
node.children?.reduce((acc, child) => {
if (child.children) {
return acc + 1 + getChildCount(child);
}
return acc + 1;
}, 0) ?? 0
);
};

/**
* Calculates the zoom for a node when fitting it to the viewport
* @param node - The node to calculate the zoom for
* @param viewportWidth - The width of the viewport
* @param viewportHeight - The height of the viewport
* @param maxViewportCoverage - The maximum percentage of the viewport that the node will take up
* @param minViewportCoverage - The minimum percentage of the viewport that the node will take up
* @returns The zoom
*/
export const calculateZoom = ({ node, viewportWidth, viewportHeight, maxViewportCoverage = 0.9, minViewportCoverage = 0.2 }: { node: LayoutNodeData; viewportWidth: number; viewportHeight: number; maxViewportCoverage?: number; minViewportCoverage?: number }) => {
const childCount = getChildCount(node);

// viewport coverage is the percentage of the viewport that the node will take up
// nodes with more children look better when they take up more of the viewport
const viewportCoverage = Math.min(maxViewportCoverage, Math.max(minViewportCoverage, minViewportCoverage + childCount * 0.1));

const updatedHorizontalZoom = (viewportCoverage * viewportWidth) / node.width;
const updatedVerticalZoom = (viewportCoverage * viewportHeight) / node.height;
const updatedZoom = Math.min(updatedHorizontalZoom, updatedVerticalZoom);

return updatedZoom - 1;
};

/**
* Calculates the scroll position for the canvas when fitting a node to the viewport - assumes the chart is centered
* @param node - The node to calculate the zoom and position for
* @param viewportWidth - The width of the viewport
* @param viewportHeight - The height of the viewport
* @param canvasWidth - The width of the canvas
* @param canvasHeight - The height of the canvas
* @param chartWidth - The width of the chart
* @param chartHeight - The height of the chart
* @param zoom - The zoom level of the canvas
* @returns The scroll position
*/
export const calculateScrollPosition = ({ node, viewportWidth, viewportHeight, canvasWidth, canvasHeight, chartWidth, chartHeight, zoom }: { node: LayoutNodeData; viewportWidth: number; viewportHeight: number; canvasWidth: number; canvasHeight: number; chartWidth: number; chartHeight: number; zoom: number }): [number, number] => {
// get updated node dimensions because they change based on the zoom level
const updatedNodeWidth = node.width * zoom;
const updatedNodeHeight = node.height * zoom;

// the chart is centered so we can assume the x and y positions
const chartPosition = {
x: (canvasWidth - chartWidth * zoom) / 2,
y: (canvasHeight - chartHeight * zoom) / 2
};

const nodeCenterXPosition = chartPosition.x + node.x * zoom + updatedNodeWidth / 2;
const nodeCenterYPosition = chartPosition.y + node.y * zoom + updatedNodeHeight / 2;

// scroll to the spot that centers the node in the viewport
const scrollX = nodeCenterXPosition - viewportWidth / 2;
const scrollY = nodeCenterYPosition - viewportHeight / 2;

return [scrollX, scrollY];
};

0 comments on commit 94f1710

Please sign in to comment.