Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New flowchart rtl #2141

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"postbuild": "rm -rf build/api",
"start": "REACT_APP_DATA_SOURCE=$DATA NODE_OPTIONS=\"--dns-result-order=ipv4first\" npm-run-all -p start:app start:lib",
"start:dev": "rm -rf node_modules/.cache && npm start",
"start:app": "PORT=4141 react-scripts start",
"start:app": "PORT=4143 react-scripts start",
"start:lib": "rm -rf lib && babel src --out-dir lib --copy-files --watch",
"lib": "npm-run-all -s lib:clean lib:copy lib:webpack lib:babel lib:prune",
"lib:clean": "rm -rf lib",
Expand Down
5 changes: 4 additions & 1 deletion src/actions/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export function updateGraph(graph) {
* @param {Object} state A subset of main state
* @return {Function} Promise function
*/
const layout = async (instance, state) => instance.graphNew(state);

const layout = async (instance, state) => {
return instance.graphNew(state);
}

// Prepare new layout worker
const layoutWorker = preventWorkerQueues(worker, layout);
Expand Down
17 changes: 12 additions & 5 deletions src/components/flowchart/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,22 @@ export const drawLayers = function () {
* Render layer name labels
*/
export const drawLayerNames = function () {
const {
chartSize: { sidebarWidth = 0 },
layers,
} = this.props;
const { chartSize, layers, orientation } = this.props;

// Calculate the layer name position based on orientation
const layerNamePosition = orientation
? chartSize.height - 100 || 0 // Vertical: position based on height
: chartSize.sidebarWidth || 0; // Horizontal: position based on sidebar width

// Apply the correct translation based on orientation
const transformValue = orientation
? `translateY(${layerNamePosition}px)` // Vertical: use translateY
: `translateX(${layerNamePosition}px)`; // Horizontal: use translateX

this.el.layerNameGroup
.transition('layer-names-sidebar-width')
.duration(this.DURATION)
.style('transform', `translateX(${sidebarWidth}px)`);
.style('transform', transformValue);

this.el.layerNames = this.el.layerNameGroup
.selectAll('.pipeline-layer-name')
Expand Down
12 changes: 9 additions & 3 deletions src/components/flowchart/flowchart.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export class FlowChart extends Component {
this.updateChartSize();
}

if (changed('layers', 'chartSize')) {
if (changed('layers', 'chartSize', 'orientation')) {
drawLayers.call(this);
drawLayerNames.call(this);
}
Expand Down Expand Up @@ -358,8 +358,13 @@ export class FlowChart extends Component {
// Update layer label y positions
if (this.el.layerNames) {
this.el.layerNames.style('transform', (d) => {
const updateY = y + (d.y + d.height / 2) * scale;
return `translateY(${updateY}px)`;
if (this.props.orientation) { // Vertical orientation
const updateX = x + (d.x + d.width / 4) * scale;
return `translateX(${updateX}px)`; // Use translateX for horizontal layout
} else { // Horizontal orientation
const updateY = y + (d.y + d.height / 2) * scale;
return `translateY(${updateY}px)`; // Use translateY for vertical layout
}
});
}

Expand Down Expand Up @@ -974,6 +979,7 @@ const emptyGraphSize = {};
export const mapStateToProps = (state, ownProps) => ({
clickedNode: state.node.clicked,
chartSize: getChartSize(state),
orientation: state.textLabels,
chartZoom: getChartZoom(state),
displayGlobalNavigation: state.display.globalNavigation,
displaySidebar: state.display.sidebar,
Expand Down
60 changes: 43 additions & 17 deletions src/selectors/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,50 @@ import { getVisibleLayerIDs } from './disabled';

const getGraph = (state) => state.graph;
const getLayerName = (state) => state.layer.name;
const getOrientation = (state) => state.textLabels;

/**
* Get layer positions
*/
export const getLayers = createSelector(
[getGraph, getVisibleLayerIDs, getLayerName],
({ nodes, size }, layerIDs, layerName) => {
[getGraph, getVisibleLayerIDs, getLayerName, getOrientation],
({ nodes, size }, layerIDs, layerName, orientation) => {
if (!nodes || !size || !nodes.length || !layerIDs.length) {
return [];
}
const { width, height } = size;

const bounds = {};

// Calculate the bounds for each layer based on node positions
for (const node of nodes) {
const layer = node.nearestLayer || node.layer;

if (layer) {
const bound = bounds[layer] || (bounds[layer] = [Infinity, -Infinity]);

if (node.y - node.height < bound[0]) {
bound[0] = node.y - node.height;
}
if (orientation) { // Vertical orientation (when true)
if (node.x - node.width < bound[0]) {
bound[0] = node.x - node.width;
}

if (node.x + node.width > bound[1]) {
bound[1] = node.x + node.width;
}

} else { // Horizontal orientation (when false)
if (node.y - node.height < bound[0]) {
bound[0] = node.y - node.height;
}

if (node.y + node.height > bound[1]) {
bound[1] = node.y + node.height;
if (node.y + node.height > bound[1]) {
bound[1] = node.y + node.height;
}
}
}
}

// Calculate the layer positions based on the orientation
return layerIDs.map((id, i) => {
const currentBound = bounds[id] || [0, 0];
const prevBound = bounds[layerIDs[i - 1]] || [
Expand All @@ -45,16 +59,28 @@ export const getLayers = createSelector(
];
const start = (prevBound[1] + currentBound[0]) / 2;
const end = (currentBound[1] + nextBound[0]) / 2;
const rectWidth = Math.max(width, height) * 5;

return {
id,
name: layerName[id],
x: (rectWidth - width) / -2,
y: start,
width: rectWidth,
height: Math.max(end - start, 0),
};
const rectSize = Math.max(width, height) * 5; // Adjust size calculation

// Return positions based on boolean orientation
if (orientation) { // Vertical layout when orientation is true
return {
id,
name: layerName[id],
x: start, // Horizontal layout moves along the x-axis
y: (rectSize - height) / -2, // Centered along y-axis
width: Math.max(end - start, 0),
height: rectSize,
};
} else { // Horizontal layout when orientation is false
return {
id,
name: layerName[id],
y: start, // Vertical layout moves along the y-axis
x: (rectSize - width) / -2, // Centered along x-axis
height: Math.max(end - start, 0),
width: rectSize,
};
}
});
}
);
6 changes: 4 additions & 2 deletions src/selectors/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const getVisibleCode = (state) => state.visible.code;
const getIgnoreLargeWarning = (state) => state.ignoreLargeWarning;
const getGraphHasNodes = (state) => Boolean(state.graph?.nodes?.length);
const getChartSizeState = (state) => state.chartSize;
const getOrient = (state) => state.textLabels;

/**
* Show the large graph warning only if there are sufficient nodes + edges,
Expand Down Expand Up @@ -51,13 +52,14 @@ export const getGraphInput = createSelector(
getVisibleEdges,
getVisibleLayerIDs,
getTriggerLargeGraphWarning,
getOrient,
],
(nodes, edges, layers, triggerLargeGraphWarning) => {
(nodes, edges, layers, triggerLargeGraphWarning, orient) => {
if (triggerLargeGraphWarning) {
return null;
}

return { nodes, edges, layers };
return { nodes, edges, layers, orient };
}
);

Expand Down
2 changes: 1 addition & 1 deletion src/selectors/nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const getHoveredNode = (state) => state.node.hovered;
const getIsPrettyName = (state) => state.isPrettyName;
const getTagActive = (state) => state.tag.active;
const getModularPipelineActive = (state) => state.modularPipeline.active;
const getTextLabels = (state) => state.textLabels;
const getTextLabels = (state) => true;
const getNodeTypeDisabled = (state) => state.nodeType.disabled;
const getClickedNode = (state) => state.node.clicked;
const getEdgeIDs = (state) => state.edge.ids;
Expand Down
39 changes: 29 additions & 10 deletions src/utils/graph/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,24 @@ export const snap = (value, unit) => Math.round(value / unit) * unit;
export const distance1d = (a, b) => Math.abs(a - b);

/**
* Returns the angle in radians between the points a and b relative to the X-axis about the origin
* Returns the angle in radians between the points a and b based on the given orientation
* @param {Object} a The first point
* @param {Object} b The second point
* @param {String} orientation The layout orientation ('top-to-bottom' or 'left-to-right')
* @returns {Number} The angle
*/
export const angle = (a, b) => Math.atan2(a.y - b.y, a.x - b.x);
export const angle = (a, b, orientation) => {
if (orientation === 'top-to-bottom') {
// Top-to-bottom orientation
return Math.atan2(a.y - b.y, a.x - b.x);
} else if (orientation === 'left-to-right') {
// Left-to-right orientation
return Math.atan2(a.x - b.x, a.y - b.y);
} else {
throw new Error(`Unsupported orientation: ${orientation}`);
}
};


/**
* Returns the left edge x-position of the node
Expand Down Expand Up @@ -75,24 +87,30 @@ export const nodeBottom = (node) => node.y + node.height * 0.5;
* @param {Array} nodes The input nodes
* @returns {Array} The sorted rows of nodes
*/
export const groupByRow = (nodes) => {
export const groupByRow = (nodes, orientation = 'top-to-bottom') => {
const rows = {};

// Create rows using node Y values
// Define the coordinate keys based on the orientation
const primaryCoord = orientation === 'left-to-right' ? 'x' : 'y';
const secondaryCoord = orientation === 'left-to-right' ? 'y' : 'x';

// Create rows using the primary coordinate (Y for top-to-bottom, X for left-to-right)
for (const node of nodes) {
rows[node.y] = rows[node.y] || [];
rows[node.y].push(node);
const key = snap(node[primaryCoord], 10);
rows[key] = rows[key] || [];
rows[key].push(node);
}

// Sort the set of rows accounting for keys being strings
// Sort the set of rows by the primary coordinate
const rowNumbers = Object.keys(rows).map((row) => parseFloat(row));
rowNumbers.sort((a, b) => a - b);

// Sort rows in order of X position if set. Break ties with ids for stability
// Sort rows in order of the secondary coordinate, then by ids for stability
const sortedRows = rowNumbers.map((row) => rows[row]);
for (let i = 0; i < sortedRows.length; i += 1) {
sortedRows[i].sort((a, b) => compare(a.x, b.x, a.id, b.id));

sortedRows[i].sort((a, b) => compare(a[secondaryCoord], b[secondaryCoord], a.id, b.id));

// Assign row index to each node in the row
for (const node of sortedRows[i]) {
node.row = i;
}
Expand All @@ -101,6 +119,7 @@ export const groupByRow = (nodes) => {
return sortedRows;
};


/**
* Generalised comparator function for sorting
* If values are strings then `localeCompare` is used, otherwise values are subtracted
Expand Down
33 changes: 13 additions & 20 deletions src/utils/graph/constraints.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,19 @@ import { Constraint, Operator, Strength } from 'kiwi.js';
* Layout constraint in Y for separating rows
*/
export const rowConstraint = {
property: 'y',

strict: (constraint, constants, variableA, variableB) =>
new Constraint(
variableA.minus(variableB),
Operator.Ge,
constants.spaceY,
constraint.separation,
Strength.required
),
};

/**
* Layout constraint in Y for separating layers
* Layout constraint for separating layers
*/
export const layerConstraint = {
property: 'y',

strict: (constraint, constants, variableA, variableB) =>
new Constraint(
variableA.minus(variableB),
Expand All @@ -42,13 +38,12 @@ export const layerConstraint = {
* Layout constraint in X for minimising distance from source to target for straight edges
*/
export const parallelConstraint = {
property: 'x',

solve: (constraint) => {
solve: (constraint, constants) => {
const { a, b, strength } = constraint;
const resolve = strength * (a.x - b.x);
a.x -= resolve;
b.x += resolve;
const resolve = strength * (a[constants.coordPrimary] - b[constants.coordPrimary]);
a[constants.coordPrimary] -= resolve;
b[constants.coordPrimary] += resolve;
},

strict: (constraint, constants, variableA, variableB) =>
Expand All @@ -64,33 +59,31 @@ export const parallelConstraint = {
* Crossing constraint in X for minimising edge crossings
*/
export const crossingConstraint = {
property: 'x',

solve: (constraint) => {
solve: (constraint, constants) => {
const { edgeA, edgeB, separationA, separationB, strength } = constraint;

// Amount to move each node towards required separation
const resolveSource =
strength *
((edgeA.sourceNode.x - edgeB.sourceNode.x - separationA) / separationA);
((edgeA.sourceNode[constants.coordPrimary] - edgeB.sourceNode[constants.coordPrimary] - separationA) / separationA);

const resolveTarget =
strength *
((edgeA.targetNode.x - edgeB.targetNode.x - separationB) / separationB);
((edgeA.targetNode[constants.coordPrimary] - edgeB.targetNode[constants.coordPrimary] - separationB) / separationB);

// Apply the resolve each node
edgeA.sourceNode.x -= resolveSource;
edgeB.sourceNode.x += resolveSource;
edgeA.targetNode.x -= resolveTarget;
edgeB.targetNode.x += resolveTarget;
edgeA.sourceNode[constants.coordPrimary] -= resolveSource;
edgeB.sourceNode[constants.coordPrimary] += resolveSource;
edgeA.targetNode[constants.coordPrimary] -= resolveTarget;
edgeB.targetNode[constants.coordPrimary] += resolveTarget;
},
};

/**
* Layout constraint in X for minimum node separation
*/
export const separationConstraint = {
property: 'x',

strict: (constraint, constants, variableA, variableB) =>
new Constraint(
Expand Down
Loading
Loading