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
2 changes: 1 addition & 1 deletion packages/core/__tests__/view/plugins/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('getDefaultPlugins', () => {
test('returns an array with the correct length', () => {
const plugins = getDefaultPlugins();
// detect any changes in default plugins, order does not matter
expect(plugins).toHaveLength(7);
expect(plugins).toHaveLength(8);
});

test('returns an array containing only functions', () => {
Expand Down
18 changes: 1 addition & 17 deletions packages/core/src/view/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -844,23 +844,7 @@ class Graph extends EventSource {
* graph.refresh();
* ```
*
* To fit and center the graph, the following code can be used.
*
* ```javascript
* let margin = 2;
* let max = 3;
*
* let bounds = graph.getGraphBounds();
* let cw = graph.container.clientWidth - margin;
* let ch = graph.container.clientHeight - margin;
* let w = bounds.width / graph.view.scale;
* let h = bounds.height / graph.view.scale;
* let s = Math.min(max, Math.min(cw / w, ch / h));
*
* graph.view.scaleAndTranslate(s,
* (margin + cw - w * s) / (2 * s) - bounds.x / graph.view.scale,
* (margin + ch - h * s) / (2 * s) - bounds.y / graph.view.scale);
* ```
* 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`.
Expand Down
109 changes: 109 additions & 0 deletions packages/core/src/view/plugins/FitPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2025-present The maxGraph project Contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import type { GraphPlugin } from '../../types';
import { Graph } from '../Graph';

function keep2digits(value: number): number {
return Number(value.toFixed(2));
}

/**
* Options of the {@link FitPlugin.fitCenter} method.
* @since 0.17.0
* @category Navigation
*/
export type FitCenterOptions = {
/**
* Margin between the graph and the container.
* @default 2
*/
margin?: number;
};

/**
* A plugin providing methods to fit the graph within its container.
* @since 0.17.0
* @category Navigation
* @category Plugin
*/
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.
* @default 8
*/
maxFitScale: number | null = 8;

/**
* Constructs the plugin that provides `fit` methods.
*
* @param graph Reference to the enclosing {@link Graph}.
*/
constructor(private readonly graph: Graph) {}

/**
* Fit and center the graph within its container.
*
* @param options Optional options to customize the fit behavior.
* @returns The current scale in the view.
*/
fitCenter(options?: FitCenterOptions): number {
// Inspired by the former examples provided in the Graph.fit JSDoc: https://github.com/maxGraph/maxGraph/blob/v0.16.0/packages/core/src/view/Graph.ts#L845-L861
const margin = options?.margin ?? 2;
const { container, view } = this.graph;

const clientWidth = container.clientWidth - 2 * margin;
const clientHeight = container.clientHeight - 2 * margin;

const bounds = this.graph.getGraphBounds();
const originalScale = view.scale;
const width = bounds.width / originalScale;
const height = bounds.height / originalScale;

// Apply workarounds to avoid rounding impact if fitCenter is called multiple times
// Use precise scale value when computing translation values, but round the applied scale
// Translate using integer values as this is done in Graph.fit

let newScale = Math.min(
this.maxFitScale ?? Infinity,
clientWidth / width,
clientHeight / height
);

const translateX = Math.floor(
view.translate.x +
(container.clientWidth - width * newScale) / (2 * newScale) -
bounds.x / originalScale
);
const translateY = Math.floor(
view.translate.y +
(container.clientHeight - height * newScale) / (2 * newScale) -
bounds.y / originalScale
);

newScale = keep2digits(newScale);
view.scaleAndTranslate(newScale, translateX, translateY);

return newScale;
}

/** Do nothing here. */
onDestroy() {
// no-op
}
}
5 changes: 5 additions & 0 deletions packages/core/src/view/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import PopupMenuHandler from '../handler/PopupMenuHandler';
import ConnectionHandler from '../handler/ConnectionHandler';
import SelectionHandler from '../handler/SelectionHandler';
import PanningHandler from '../handler/PanningHandler';
import { FitPlugin } from './FitPlugin';

// Export all plugins to have them in the root barrel file
export * from './FitPlugin';

/**
* Returns the list of plugins used by default in `maxGraph`.
Expand All @@ -39,4 +43,5 @@ export const getDefaultPlugins = (): GraphPluginConstructor[] => [
ConnectionHandler,
SelectionHandler,
PanningHandler,
FitPlugin,
];
1 change: 1 addition & 0 deletions packages/core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"I18n",
"Layout",
"Logging",
"Navigation",
"Perimeter",
"Plugin",
"Serialization with Codecs",
Expand Down
29 changes: 23 additions & 6 deletions packages/html/stories/ZoomAndFit.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { DomHelpers, Graph, InternalEvent } from '@maxgraph/core';
import { DomHelpers, type FitPlugin, Graph, InternalEvent } from '@maxgraph/core';
import {
contextMenuTypes,
contextMenuValues,
Expand All @@ -32,17 +32,31 @@ export default {
...contextMenuTypes,
...globalTypes,
...rubberBandTypes,
graphWithLargeHeight: {
type: 'boolean',
defaultValue: true,
},
containerWithScrollbar: {
type: 'boolean',
defaultValue: false,
},
},
args: {
...contextMenuValues,
...globalValues,
...rubberBandValues,
graphWithLargeHeight: false,
containerWithScrollbar: false,
},
};

const Template = ({ label, ...args }: Record<string, string>) => {
const mainContainer = document.createElement('div');
const container = createGraphContainer(args);
if (args.containerWithScrollbar) {
container.style.overflow = 'auto';
}

if (!args.contextMenu) InternalEvent.disableContextMenu(container);
const graph = new Graph(container);
graph.setPanning(true);
Expand All @@ -68,17 +82,20 @@ const Template = ({ label, ...args }: Record<string, string>) => {
addControlButton('Zoom Out', function () {
graph.zoomOut();
});
const border = 10;
const margin = 20;
addControlButton('Fit', function () {
graph.fit(border);
graph.fit(undefined, false, margin);
});
addControlButton('Fit Center', function () {
graph.getPlugin<FitPlugin>('fit')?.fitCenter({ margin });
});
addControlButton('Fit Horizontal', function () {
// This is a pain to use so many parameters when lot of them are the same as default values
// Consider having a method with a single object. See https://github.com/maxGraph/maxGraph/pull/715#discussion_r1993871475
graph.fit(border, false, 0, true, false, true);
graph.fit(undefined, false, margin, true, false, true);
});
addControlButton('Fit Vertical', function () {
graph.fit(border, false, 0, true, true, false);
graph.fit(undefined, false, margin, true, true, false);
});

mainContainer.appendChild(container);
Expand All @@ -103,7 +120,7 @@ const Template = ({ label, ...args }: Record<string, string>) => {
value: 'hexagon',
});
const v4 = graph.insertVertex({
position: [60, 210],
position: [60, args.graphWithLargeHeight ? 410 : 210],
size: [100, 30],
value: 'rectangle 2',
});
Expand Down
4 changes: 4 additions & 0 deletions packages/ts-example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ <h1>maxGraph TypeScript Integration with Vite, using custom shapes</h1>
<li>Cells selection with Rubberband: use mouse left button</li>
</ul>
<div id="graph-container"></div>
<div class="controls">
<button id="reset-zoom" aria-label="Reset graph zoom to default">Reset Zoom</button>
<button id="fit-center" aria-label="Fit graph to center of view">Fit Center</button>
</div>
<footer></footer>
</body>
</html>
Expand Down
13 changes: 12 additions & 1 deletion packages/ts-example/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import '@maxgraph/core/css/common.css'; // required by RubberBandHandler
import './style.css';
import {
constants,
type FitPlugin,
getDefaultPlugins,
Graph,
InternalEvent,
Expand Down Expand Up @@ -111,11 +112,21 @@ const initializeGraph = (container: HTMLElement) => {
style: { endArrow: 'block' },
});
});

return graph;
};

// display the maxGraph version in the footer
const footer = document.querySelector('footer')!;
footer.innerText = `Built with maxGraph ${constants.VERSION}`;

// Creates the graph inside the given container
initializeGraph(document.querySelector('#graph-container')!);
const graph = initializeGraph(document.querySelector('#graph-container')!);

// Control buttons
document.getElementById('reset-zoom')!.addEventListener('click', () => {
graph.zoomActual();
});
document.getElementById('fit-center')!.addEventListener('click', () => {
graph.getPlugin<FitPlugin>('fit')?.fitCenter({ margin: 20 });
});
4 changes: 4 additions & 0 deletions packages/ts-example/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ footer {
overflow: hidden;
}

.controls {
margin-top: .75rem;
}

/* For rubber band selection, override maxGraph defaults */
div.mxRubberband {
border-color: #b18426;
Expand Down