Merge pull request #5068 from xyflow/enhance/fitViewOptions
Enhance fitViewOptions
This commit is contained in:
@@ -146,6 +146,9 @@ const BasicFlow = () => {
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
fitView
|
||||
fitViewOptions={{
|
||||
padding: { top: '100px', left: '0%', right: '10%', bottom: 0.1 },
|
||||
}}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
selectNodesOnDrag={false}
|
||||
elevateEdgesOnSelect
|
||||
|
||||
@@ -91,11 +91,25 @@ export type FitViewParamsBase<NodeType extends NodeBase> = {
|
||||
maxZoom: number;
|
||||
};
|
||||
|
||||
export type PaddingUnit = 'px' | '%';
|
||||
export type PaddingWithUnit = `${number}${PaddingUnit}` | number;
|
||||
|
||||
export type Padding =
|
||||
| PaddingWithUnit
|
||||
| {
|
||||
top?: PaddingWithUnit;
|
||||
right?: PaddingWithUnit;
|
||||
bottom?: PaddingWithUnit;
|
||||
left?: PaddingWithUnit;
|
||||
x?: PaddingWithUnit;
|
||||
y?: PaddingWithUnit;
|
||||
};
|
||||
|
||||
/**
|
||||
* @inline
|
||||
*/
|
||||
export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
|
||||
padding?: number;
|
||||
padding?: Padding;
|
||||
includeHiddenNodes?: boolean;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
Transform,
|
||||
InternalNodeBase,
|
||||
NodeLookup,
|
||||
Padding,
|
||||
PaddingWithUnit,
|
||||
} from '../types';
|
||||
import { type Viewport } from '../types';
|
||||
import { getNodePositionWithOrigin, isInternalNodeBase } from './graph';
|
||||
@@ -173,6 +175,105 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a single padding value to a number
|
||||
* @internal
|
||||
* @param padding - Padding to parse
|
||||
* @param viewport - Width or height of the viewport
|
||||
* @returns The padding in pixels
|
||||
*/
|
||||
function parsePadding(padding: PaddingWithUnit, viewport: number): number {
|
||||
if (typeof padding === 'number') {
|
||||
return Math.floor(viewport - viewport / (1 + padding));
|
||||
}
|
||||
|
||||
if (typeof padding === 'string' && padding.endsWith('px')) {
|
||||
const paddingValue = parseFloat(padding);
|
||||
if (!Number.isNaN(paddingValue)) {
|
||||
return Math.floor(paddingValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof padding === 'string' && padding.endsWith('%')) {
|
||||
const paddingValue = parseFloat(padding);
|
||||
if (!Number.isNaN(paddingValue)) {
|
||||
return Math.floor(viewport * paddingValue * 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(
|
||||
`[React Flow] The padding value "${padding}" is invalid. Please provide a number or a string with a valid unit (px or %).`
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the paddings to an object with top, right, bottom, left, x and y paddings
|
||||
* @internal
|
||||
* @param padding - Padding to parse
|
||||
* @param width - Width of the viewport
|
||||
* @param height - Height of the viewport
|
||||
* @returns An object with the paddings in pixels
|
||||
*/
|
||||
function parsePaddings(
|
||||
padding: Padding,
|
||||
width: number,
|
||||
height: number
|
||||
): { top: number; bottom: number; left: number; right: number; x: number; y: number } {
|
||||
if (typeof padding === 'string' || typeof padding === 'number') {
|
||||
const paddingY = parsePadding(padding, height);
|
||||
const paddingX = parsePadding(padding, width);
|
||||
return {
|
||||
top: paddingY,
|
||||
right: paddingX,
|
||||
bottom: paddingY,
|
||||
left: paddingX,
|
||||
x: paddingX * 2,
|
||||
y: paddingY * 2,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof padding === 'object') {
|
||||
const top = parsePadding(padding.top ?? padding.y ?? 0, height);
|
||||
const bottom = parsePadding(padding.bottom ?? padding.y ?? 0, height);
|
||||
const left = parsePadding(padding.left ?? padding.x ?? 0, width);
|
||||
const right = parsePadding(padding.right ?? padding.x ?? 0, width);
|
||||
return { top, right, bottom, left, x: left + right, y: top + bottom };
|
||||
}
|
||||
|
||||
return { top: 0, right: 0, bottom: 0, left: 0, x: 0, y: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the resulting paddings if the new viewport is applied
|
||||
* @internal
|
||||
* @param bounds - Bounds to fit inside viewport
|
||||
* @param x - X position of the viewport
|
||||
* @param y - Y position of the viewport
|
||||
* @param zoom - Zoom level of the viewport
|
||||
* @param width - Width of the viewport
|
||||
* @param height - Height of the viewport
|
||||
* @returns An object with the minimum padding required to fit the bounds inside the viewport
|
||||
*/
|
||||
function calculateAppliedPaddings(bounds: Rect, x: number, y: number, zoom: number, width: number, height: number) {
|
||||
const { x: left, y: top } = rendererPointToPoint(bounds, [x, y, zoom]);
|
||||
|
||||
const { x: boundRight, y: boundBottom } = rendererPointToPoint(
|
||||
{ x: bounds.x + bounds.width, y: bounds.y + bounds.height },
|
||||
[x, y, zoom]
|
||||
);
|
||||
|
||||
const right = width - boundRight;
|
||||
const bottom = height - boundBottom;
|
||||
|
||||
return {
|
||||
left: Math.floor(left),
|
||||
top: Math.floor(top),
|
||||
right: Math.floor(right),
|
||||
bottom: Math.floor(bottom),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a viewport that encloses the given bounds with optional padding.
|
||||
* @public
|
||||
@@ -186,8 +287,8 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra
|
||||
* @returns A transforned {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport}
|
||||
* @example
|
||||
* const { x, y, zoom } = getViewportForBounds(
|
||||
*{ x: 0, y: 0, width: 100, height: 100},
|
||||
*1200, 800, 0.5, 2);
|
||||
* { x: 0, y: 0, width: 100, height: 100},
|
||||
* 1200, 800, 0.5, 2);
|
||||
*/
|
||||
export const getViewportForBounds = (
|
||||
bounds: Rect,
|
||||
@@ -195,18 +296,39 @@ export const getViewportForBounds = (
|
||||
height: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
padding: number
|
||||
padding: Padding
|
||||
): Viewport => {
|
||||
const xZoom = width / (bounds.width * (1 + padding));
|
||||
const yZoom = height / (bounds.height * (1 + padding));
|
||||
// First we resolve all the paddings to actual pixel values
|
||||
const p = parsePaddings(padding, width, height);
|
||||
|
||||
const xZoom = (width - p.x) / bounds.width;
|
||||
const yZoom = (height - p.y) / bounds.height;
|
||||
|
||||
// We calculate the new x, y, zoom for a centered view
|
||||
const zoom = Math.min(xZoom, yZoom);
|
||||
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
||||
|
||||
const boundsCenterX = bounds.x + bounds.width / 2;
|
||||
const boundsCenterY = bounds.y + bounds.height / 2;
|
||||
const x = width / 2 - boundsCenterX * clampedZoom;
|
||||
const y = height / 2 - boundsCenterY * clampedZoom;
|
||||
|
||||
return { x, y, zoom: clampedZoom };
|
||||
// Then we calculate the minimum padding, to respect asymmetric paddings
|
||||
const newPadding = calculateAppliedPaddings(bounds, x, y, clampedZoom, width, height);
|
||||
|
||||
// We only want to have an offset if the newPadding is smaller than the required padding
|
||||
const offset = {
|
||||
left: Math.min(newPadding.left - p.left, 0),
|
||||
top: Math.min(newPadding.top - p.top, 0),
|
||||
right: Math.min(newPadding.right - p.right, 0),
|
||||
bottom: Math.min(newPadding.bottom - p.bottom, 0),
|
||||
};
|
||||
|
||||
return {
|
||||
x: x - offset.left + offset.right,
|
||||
y: y - offset.top + offset.bottom,
|
||||
zoom: clampedZoom,
|
||||
};
|
||||
};
|
||||
|
||||
export const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0;
|
||||
|
||||
Reference in New Issue
Block a user