Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
38 changes: 37 additions & 1 deletion plugins/org.eclipse.elk.core/src/org/eclipse/elk/core/Core.melk
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,42 @@ advanced option topdownLayout: boolean {
}

group topdown {

advanced option sizeCategories: int {
label "Number of size categories"
description
"Defines the number of categories to use for the FIXED_INTEGER_RATIO_BOXES size approximator."
default = 3
lowerBound = 1
targets parents
requires sizeApproximator == TopdownSizeApproximator.FIXED_INTEGER_RATIO_BOXES
}

advanced option sizeCategoriesRangeMax: double {
label "Top end of range to use to determine size category"
description
"The size categories are distributed evenly on a logarithmic scale from 1 to the value defined by this
option. If the default value of 64 is chosen and the number of categories is three then the categories will
be in the following ranges: CAT 0: 1 - 4, CAT 1: 4 - 16, CAT 2: 16 - 64."
default = 64.0
lowerBound = 2.0
targets parents
requires sizeCategories
}

advanced option sizeCategoriesHierarchicalNodeWeight: int {
label "Weight of a node containing children for determining the graph size"
description
"When determining the graph size for the size categorisation, this value determines how many times a node
containing children is weighted more than a simple node. For example setting this value to four would
result in a graph containing a simple node and a hierarchical node to be counted as having a size of five."
default = 4
lowerBound = 1
targets parents
requires sizeCategories

}

programmatic option scaleFactor: double {
label "Topdown Scale Factor"
description
Expand All @@ -916,7 +952,7 @@ group topdown {
requires nodeType == TopdownNodeTypes.HIERARCHICAL_NODE
}

advanced option sizeApproximator: TopdownSizeApproximator {
advanced option sizeApproximator: ITopdownSizeApproximator {
label "Topdown Size Approximator"
description
"The size approximator to be used to set sizes of hierarchical nodes during topdown layout. The default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.eclipse.elk.core.options.ContentAlignment;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.elk.core.options.HierarchyHandling;
import org.eclipse.elk.core.options.ITopdownSizeApproximator;
import org.eclipse.elk.core.options.TopdownNodeTypes;
import org.eclipse.elk.core.options.TopdownSizeApproximator;
import org.eclipse.elk.core.testing.TestController;
Expand Down Expand Up @@ -248,8 +249,9 @@ protected List<ElkEdge> layoutRecursively(final ElkNode layoutNode, final TestCo
KVector requiredSize = topdownLayoutProvider.getPredictedGraphSize(childNode);
childNode.setDimensions(Math.max(childNode.getWidth(), requiredSize.x),
Math.max(childNode.getHeight(), requiredSize.y));
} else if (childNode.getProperty(CoreOptions.TOPDOWN_SIZE_APPROXIMATOR) != null) {
TopdownSizeApproximator approximator =
} else if (childNode.getProperty(CoreOptions.TOPDOWN_SIZE_APPROXIMATOR) != null
&& childNode.getChildren() != null && childNode.getChildren().size() > 0) {
ITopdownSizeApproximator approximator =
childNode.getProperty(CoreOptions.TOPDOWN_SIZE_APPROXIMATOR);
KVector size = approximator.getSize(childNode);
ElkPadding padding = childNode.getProperty(CoreOptions.PADDING);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*******************************************************************************
* Copyright (c) 2024 Kiel University and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.elk.core.options;

import org.eclipse.elk.core.math.KVector;
import org.eclipse.elk.graph.ElkNode;

/**
* A topdown size approximator returns an estimated size of the graph drawing after performing layout using some
* heuristic.
*
*/
public interface ITopdownSizeApproximator {

/**
* Returns an approximated required size for a given node.
* @param node the node
* @return the size as a vector
*/
public KVector getSize(ElkNode node);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import org.eclipse.elk.core.AbstractLayoutProvider;
import org.eclipse.elk.core.data.LayoutAlgorithmData;
import org.eclipse.elk.core.data.LayoutAlgorithmResolver;
import org.eclipse.elk.core.math.ElkPadding;
import org.eclipse.elk.core.math.KVector;
import org.eclipse.elk.core.util.ElkUtil;
Expand All @@ -27,7 +28,7 @@
* of hierarchical nodes. This allows the use of a size approximation strategy to minimize white space
* in the final result.
*/
public enum TopdownSizeApproximator {
public enum TopdownSizeApproximator implements ITopdownSizeApproximator {

/**
* Computes the square root of the number of children and uses that as a multiplier for the base size
Expand Down Expand Up @@ -57,13 +58,11 @@ public KVector getSize(final ElkNode originalGraph) {
final LayoutAlgorithmData algorithmData = originalGraph.getProperty(CoreOptions.RESOLVED_ALGORITHM);

// clone the current hierarchy
// ElkNode node = ElkGraphUtil.createGraph();
ElkNode node = ElkGraphFactory.eINSTANCE.createElkNode();
node.copyProperties(originalGraph);
Map<ElkNode, ElkNode> oldToNewNodeMap = new HashMap<>();
// copy children
for (ElkNode child : originalGraph.getChildren()) {
// ElkNode newChild = ElkGraphUtil.createNode(node);
ElkNode newChild = ElkGraphFactory.eINSTANCE.createElkNode();
newChild.setParent(node);
newChild.copyProperties(child);
Expand All @@ -78,7 +77,6 @@ public KVector getSize(final ElkNode originalGraph) {
for (ElkEdge edge : child.getOutgoingEdges()) {
ElkNode newSrc = oldToNewNodeMap.get(child);
ElkNode newTar = oldToNewNodeMap.get(edge.getTargets().get(0));
// ElkEdge newEdge = ElkGraphUtil.createSimpleEdge(newSrc, newTar);
ElkEdge newEdge = ElkGraphFactory.eINSTANCE.createElkEdge();
newEdge.getSources().add(newSrc);
newEdge.getTargets().add(newTar);
Expand Down Expand Up @@ -135,13 +133,80 @@ public KVector getSize(final ElkNode originalGraph) {
// return new KVector(Math.max(minWidth, childAreaDesiredWidth), Math.max(minHeight, childAreaDesiredHeight));

}
};
},

/**
* Fixed Integer Ratio Approximator
* Dependent on the size of the child graphs, rectangles of fixed ratios are produced.
* The goal is to enable good packings and also give bigger subgraphs more space.
*/
FIXED_INTEGER_RATIO_BOXES {
@Override
public KVector getSize(final ElkNode originalGraph) {

double baseWidth = originalGraph.getProperty(CoreOptions.TOPDOWN_HIERARCHICAL_NODE_WIDTH);
double baseHeight = baseWidth / originalGraph.getProperty(CoreOptions.TOPDOWN_HIERARCHICAL_NODE_ASPECT_RATIO);

// four categories of box sizes, tiny = half-width, small = base-width, medium = double-width, large = quadruple-width
// how graph sizes are distributed into these categories has a great effect on the final result
double multiplier = TopdownSizeApproximatorUtil.getSizeCategoryMultiplier(originalGraph);

// Combine multiplier, spacings and base size to compute final size
ElkPadding padding = originalGraph.getProperty(CoreOptions.PADDING);
double nodeNodeSpacing = CoreOptions.SPACING_NODE_NODE.getDefault();
if (originalGraph.getParent() != null) {
nodeNodeSpacing = originalGraph.getParent().getProperty(CoreOptions.SPACING_NODE_NODE);
}
KVector resultSize = new KVector(baseWidth, baseHeight).scale(multiplier);
return resultSize.add(new KVector(
-(padding.left + padding.right) - nodeNodeSpacing,
-(padding.top + padding.bottom) - nodeNodeSpacing));
}
},

/**
* Returns an approximated required size for a given node.
* @param node the node
* @return the size as a vector
* This approximator simply lays out the next level and sets its algorithm to fixed so that it is later skipped.
*/
public abstract KVector getSize(ElkNode node);
LAYOUT_NEXT_LEVEL {
@Override public KVector getSize(final ElkNode originalGraph) {

// do size approximations for children
for (ElkNode childNode : originalGraph.getChildren()) {
ITopdownSizeApproximator approximator =
childNode.getProperty(CoreOptions.TOPDOWN_SIZE_APPROXIMATOR);
KVector size = approximator.getSize(childNode);
ElkPadding padding = childNode.getProperty(CoreOptions.PADDING);
// never reuse the old size, always reset, otherwise calling layout multiple times leads to growing regions
childNode.setDimensions(size.x + padding.left + padding.right,
size.y + padding.top + padding.bottom);
}

// layout children
// Get an instance of the layout provider
final LayoutAlgorithmData algorithmData = originalGraph.getProperty(CoreOptions.RESOLVED_ALGORITHM);
AbstractLayoutProvider layoutProvider = algorithmData.getInstancePool().fetch();

try {
// Perform layout on the current hierarchy level
layoutProvider.layout(originalGraph, new NullElkProgressMonitor());
algorithmData.getInstancePool().release(layoutProvider);
} catch (Exception exception) {
// The layout provider has failed - destroy it slowly and painfully
layoutProvider.dispose();
throw exception;
}

// set layout to fixed layout
originalGraph.setProperty(CoreOptions.ALGORITHM, FixedLayouterOptions.ALGORITHM_ID);
new LayoutAlgorithmResolver().visit(originalGraph);

ElkUtil.computeChildAreaDimensions(originalGraph);
double childAreaDesiredWidth = originalGraph.getProperty(CoreOptions.CHILD_AREA_WIDTH);
double childAreaDesiredHeight = originalGraph.getProperty(CoreOptions.CHILD_AREA_HEIGHT);

// apply size to graph
return new KVector(childAreaDesiredWidth, childAreaDesiredHeight);
}
};

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*******************************************************************************
* Copyright (c) 2024 Kiel University and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.elk.core.options;

import org.eclipse.elk.graph.ElkNode;

/**
* Utility functions for reuse across different size approximators.
*
*/
public class TopdownSizeApproximatorUtil {

/**
* Dynamically calculate the multiplier to be applied for the side length of the input node based on the number
* of children (with and without hierarchy) it and its siblings have. The distribution is mapped to a log scale,
* which is divided into a number of categories that determine the multiplier.
*
* Category i => 2^i
*
* @param originalGraph
* @return
*/
public static double getSizeCategoryMultiplier(final ElkNode originalGraph) {
ElkNode parent = originalGraph.getParent();
int thisGraphsSize = getGraphSize(originalGraph);

int CATEGORIES = originalGraph.getProperty(CoreOptions.TOPDOWN_SIZE_CATEGORIES);


if (parent != null) {
// 1. compute distribution of node sizes
int sizeMinFound = Integer.MAX_VALUE;
int sizeMaxFound = Integer.MIN_VALUE;

for (ElkNode child : parent.getChildren()) {
int size = getGraphSize(child);

if (size > sizeMaxFound) {
sizeMaxFound = size;
}
if (size < sizeMinFound) {
sizeMinFound = size;
}
}


double sizeMin = 1;
double sizeMax = originalGraph.getProperty(CoreOptions.TOPDOWN_SIZE_CATEGORIES_RANGE_MAX);
// shift the range to encompass the largest graph in the local neighbourhood
if (sizeMaxFound > sizeMax) {
sizeMax = sizeMaxFound;
}

// 2. set cutoffs at quarter percentiles on logarithmic scale
double x = (Math.log(sizeMax) - Math.log(sizeMin)) / CATEGORIES;
double factor = Math.exp(x);

// 3. assign node size according to dynamic cutoffs
double cutoff = sizeMin * factor;
for (int i = 0; i < CATEGORIES; i++) {
if (thisGraphsSize <= cutoff) {
return Math.pow(2, i);
} else {
cutoff *= factor;
}
}
// largest category
return Math.pow(2, CATEGORIES-1);

} else {
return 1.0;
}

}

/**
* Returns the "size" of the graph defined as the sum of the children's weights.
* Each simple node containing no children is counted with a weight of 1.
* Each node with further children is counted with a weight defined in
* `CoreOptions.TOPDOWN_SIZE_CATEGORIES_HIERARCHICAL_NODE_WEIGHT`
* @param originalGraph the graph
* @return the size of the graph
*/
public static int getGraphSize(final ElkNode originalGraph) {

int sum = 0;

final int HIERARCHICAL_NODE_WEIGHT = originalGraph.getProperty(CoreOptions.TOPDOWN_SIZE_CATEGORIES_HIERARCHICAL_NODE_WEIGHT);
for (ElkNode child : originalGraph.getChildren()) {
if (child.getChildren() != null && child.getChildren().size() > 0) {
sum += HIERARCHICAL_NODE_WEIGHT;
} else {
sum += 1;
}
}
return sum;
}

}