Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ For more details on the contents of a release, see [the GitHub release page] (ht

_**Note:** Yet to be released breaking changes appear here._

**Breaking Changes**:
- the `AbstractGraph.fit` method moved to `FitPlugin`, as well as the `minFitScale` and `maxFitScale` properties.
The method now accepts a single parameter, mainly to minimize the need to pass many default values.

## 0.20.0

Release date: `2025-05-16`
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/editor/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import type MaxPopupMenu from '../gui/MaxPopupMenu';
import { isNullish } from '../internal/utils';
import { isI18nEnabled, translate } from '../internal/i18n-utils';
import { error } from '../gui/guiUtils';
import type { FitPlugin } from '../view/plugins';

/**
* Extends {@link EventSource} to implement an application wrapper for a graph that
Expand Down Expand Up @@ -1027,7 +1028,7 @@ export class Editor extends EventSource {
});

this.addAction('fit', (editor: Editor) => {
editor.graph.fit();
editor.graph.getPlugin<FitPlugin>('fit')?.fit();
});

this.addAction('showProperties', (editor: Editor, cell: Cell) => {
Expand Down
140 changes: 0 additions & 140 deletions packages/core/src/view/AbstractGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,18 +354,6 @@ export abstract class AbstractGraph extends EventSource {
*/
multigraph = true;

/**
* Specifies the minimum scale to be applied in {@link fit}. Set this to `null` to allow any value.
* @default 0.1
*/
minFitScale: number | null = 0.1;

/**
* Specifies the maximum scale to be applied in {@link fit}. Set this to `null` to allow any value.
* @default 8
*/
maxFitScale: number | null = 8;

/**
* Specifies the {@link Image} for the image to be used to display a warning
* overlay. See {@link setCellWarning}. Default value is Client.imageBasePath +
Expand Down Expand Up @@ -801,134 +789,6 @@ export abstract class AbstractGraph extends EventSource {
);
}

/**
* Scales the graph such that the complete diagram fits into {@link AbstractGraph.container} and returns the current scale in the view.
* To fit an initial graph prior to rendering, set {@link GraphView.rendering} to `false` prior to changing the model
* and execute the following after changing the model.
*
* ```javascript
* graph.view.rendering = false;
* // here, change the model
* graph.fit();
* graph.view.rendering = true;
* graph.refresh();
* ```
*
* To fit and center the graph, use {@link FitPlugin.fitCenter}.
*
* @param border Optional number that specifies the border. Default is {@link border}.
* @param keepOrigin Optional boolean that specifies if the translate should be changed. Default is `false`.
* @param margin Optional margin in pixels. Default is `0`.
* @param enabled Optional boolean that specifies if the scale should be set or just returned. Default is `true`.
* @param ignoreWidth Optional boolean that specifies if the width should be ignored. Default is `false`.
* @param ignoreHeight Optional boolean that specifies if the height should be ignored. Default is `false`.
* @param maxHeight Optional maximum height.
*/
fit(
border: number = this.getBorder(),
keepOrigin = false,
margin = 0,
enabled = true,
ignoreWidth = false,
ignoreHeight = false,
maxHeight: number | null = null
): number {
const { container, view } = this;
if (container) {
// Adds spacing and border from css
const cssBorder = this.getBorderSizes();
let w1: number = container.offsetWidth - cssBorder.x - cssBorder.width - 1;
let h1: number =
maxHeight != null
? maxHeight
: container.offsetHeight - cssBorder.y - cssBorder.height - 1;
let bounds = view.getGraphBounds();

if (bounds.width > 0 && bounds.height > 0) {
if (keepOrigin && bounds.x != null && bounds.y != null) {
bounds = bounds.clone();
bounds.width += bounds.x;
bounds.height += bounds.y;
bounds.x = 0;
bounds.y = 0;
}

// LATER: Use unscaled bounding boxes to fix rounding errors
const originalScale = view.scale;
let w2 = bounds.width / originalScale;
let h2 = bounds.height / originalScale;

// Fits to the size of the background image if required
if (this.backgroundImage) {
w2 = Math.max(w2, this.backgroundImage.width - bounds.x / originalScale);
h2 = Math.max(h2, this.backgroundImage.height - bounds.y / originalScale);
}

const b: number = (keepOrigin ? border : 2 * border) + margin + 1;

w1 -= b;
h1 -= b;

let newScale = ignoreWidth
? h1 / h2
: ignoreHeight
? w1 / w2
: Math.min(w1 / w2, h1 / h2);

if (this.minFitScale != null) {
newScale = Math.max(newScale, this.minFitScale);
}

if (this.maxFitScale != null) {
newScale = Math.min(newScale, this.maxFitScale);
}

if (enabled) {
if (!keepOrigin) {
if (!hasScrollbars(container)) {
const x0 =
bounds.x != null
? Math.floor(
view.translate.x -
bounds.x / originalScale +
border / newScale +
margin / 2
)
: border;
const y0 =
bounds.y != null
? Math.floor(
view.translate.y -
bounds.y / originalScale +
border / newScale +
margin / 2
)
: border;

view.scaleAndTranslate(newScale, x0, y0);
} else {
view.setScale(newScale);
const newBounds = this.getGraphBounds();

if (newBounds.x != null) {
container.scrollLeft = newBounds.x;
}

if (newBounds.y != null) {
container.scrollTop = newBounds.y;
}
}
} else if (view.scale != newScale) {
view.setScale(newScale);
}
} else {
return newScale;
}
}
}
return view.scale;
}

/**
* Resizes the container for the given graph width and height.
*/
Expand Down
171 changes: 170 additions & 1 deletion packages/core/src/view/plugins/FitPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ limitations under the License.

import type { GraphPlugin } from '../../types';
import type { AbstractGraph } from '../AbstractGraph';
import { hasScrollbars } from '../../util/styleUtils';

function keep2digits(value: number): number {
return Number(value.toFixed(2));
Expand All @@ -34,6 +35,49 @@ export type FitCenterOptions = {
margin?: number;
};

/**
* Options of the {@link FitPlugin.fit} method.
* @since 0.21.0
* @category Navigation
*/
export type FitOptions = {
/**
* Optional number that specifies the border.
* @default{@link Graph.getBorder}
*/
border?: number;
/**
* Optional boolean that specifies if the "translate" should be changed.
* @default false
*/
keepOrigin?: boolean;
/**
* Optional margin in pixels.
* @default 0
*/
margin?: number;
/**
* Optional boolean that specifies if the scale should be set (when `true`) or just returned.
* @default true
*/
enabled?: boolean;
/**
* Optional boolean that specifies if the width should be ignored.
* @default false
*/
ignoreWidth?: boolean;
/**
* Optional boolean that specifies if the height should be ignored.
* @default false
*/
ignoreHeight?: boolean;
/**
* Optional maximum height. When set to `null`, the height is ignored i.e. use the maximum available height within the container.
* @default null
*/
maxHeight?: number | null;
};

/**
* A plugin providing methods to fit the graph within its container.
* @since 0.17.0
Expand All @@ -44,7 +88,14 @@ export class FitPlugin implements GraphPlugin {
static readonly pluginId = 'fit';

/**
* Specifies the maximum scale to be applied during fit operations. Set this to `null` to allow any value.
* Specifies the minimum scale to be applied in {@link fit}. Set this to `null` to allow any value.
* @default 0.1
* @since 0.21.0
*/
minFitScale: number | null = 0.1;

/**
* Specifies the maximum scale to be applied in {@link fit} and {@link fitCenter}. Set this to `null` to allow any value.
* @default 8
*/
maxFitScale: number | null = 8;
Expand All @@ -56,6 +107,124 @@ export class FitPlugin implements GraphPlugin {
*/
constructor(private readonly graph: AbstractGraph) {}

/**
* Scales the graph such that the complete diagram fits into {@link Graph.container} and returns the current scale in the view.
* To fit an initial graph prior to rendering, set {@link GraphView.rendering} to `false` prior to changing the model
* and execute the following after changing the model.
*
* ```javascript
* graph.view.rendering = false;
* // here, change the model
* graph.getPlugin<FitPlugin>('fit')?.fit();
* graph.view.rendering = true;
* graph.refresh();
* ```
*
* To fit and center the graph, use {@link fitCenter}.
*
* @param options Optional number that specifies the border.
* @since 0.21.0
*/
fit(options: FitOptions = {}): number {
const {
border = this.graph.getBorder(),
keepOrigin = false,
margin = 0,
enabled = true,
ignoreWidth = false,
ignoreHeight = false,
maxHeight = null,
} = options;
const { backgroundImage, container, view } = this.graph;
if (container) {
// Adds spacing and border from css
const cssBorder = this.graph.getBorderSizes();
let w1: number = container.offsetWidth - cssBorder.x - cssBorder.width - 1;
let h1: number =
maxHeight ?? container.offsetHeight - cssBorder.y - cssBorder.height - 1;
let bounds = view.getGraphBounds();

if (bounds.width > 0 && bounds.height > 0) {
if (keepOrigin && bounds.x != null && bounds.y != null) {
bounds = bounds.clone();
bounds.width += bounds.x;
bounds.height += bounds.y;
bounds.x = 0;
bounds.y = 0;
}

// LATER: Use unscaled bounding boxes to fix rounding errors
const originalScale = view.scale;
let w2 = bounds.width / originalScale;
let h2 = bounds.height / originalScale;

// Fits to the size of the background image if required
if (backgroundImage) {
w2 = Math.max(w2, backgroundImage.width - bounds.x / originalScale);
h2 = Math.max(h2, backgroundImage.height - bounds.y / originalScale);
}

const b: number = (keepOrigin ? border : 2 * border) + margin + 1;

w1 -= b;
h1 -= b;

let newScale = ignoreWidth
? h1 / h2
: ignoreHeight
? w1 / w2
: Math.min(w1 / w2, h1 / h2);

const minScale = this.minFitScale ?? 0;
const maxScale = this.maxFitScale ?? Infinity;
newScale = Math.max(Math.min(newScale, maxScale), minScale);

if (enabled) {
if (!keepOrigin) {
if (!hasScrollbars(container)) {
const x0 =
bounds.x != null
? Math.floor(
view.translate.x -
bounds.x / originalScale +
border / newScale +
margin / 2
)
: border;
const y0 =
bounds.y != null
? Math.floor(
view.translate.y -
bounds.y / originalScale +
border / newScale +
margin / 2
)
: border;

view.scaleAndTranslate(newScale, x0, y0);
} else {
view.setScale(newScale);
const newBounds = this.graph.getGraphBounds();

if (newBounds.x != null) {
container.scrollLeft = newBounds.x;
}

if (newBounds.y != null) {
container.scrollTop = newBounds.y;
}
}
} else if (view.scale != newScale) {
view.setScale(newScale);
}
} else {
return newScale;
}
}
}
return view.scale;
}

/**
* Fit and center the graph within its container.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/html/stories/OrgChart.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ const Template = ({ label, ...args }) => {
}

menu.addItem('Fit', 'images/zoom.gif', function () {
graph.fit();
graph.getPlugin('fit')?.fit();
});

menu.addItem('Actual', 'images/zoomactual.gif', function () {
Expand Down
Loading