Skip to content
This repository has been archived by the owner on Jul 8, 2024. It is now read-only.

Commit

Permalink
Feat: DO-2244 support tiers in marketing spring layouts (#61)
Browse files Browse the repository at this point in the history
* merge node dragging bug

* merge soft edge fix

* Fix: DOS-237 - Added props.onchange to the callback dependency of numeric inputs and added onKeyDown property so we can bind to it (#58)

* Added props.onchange to the callback dependency as the onChange does not change in sync with the parent

* added tests and changelog

* fixed changelog

* removed if needed

* update version to 1.4.4

* updating changelogs

* support for tiers in spring layout

* support in marketing layout

* added missing changelog

* adjusting ordering force

* PR feedback

---------

Co-authored-by: Krzysztof Bielikowicz <[email protected]>
Co-authored-by: Jonathan Sumiguin <[email protected]>
  • Loading branch information
3 people authored Jan 9, 2024
1 parent c594ade commit 84d4e73
Show file tree
Hide file tree
Showing 26 changed files with 440 additions and 222 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ test:

# Version all the main packages in lockstep as a patch - run pnpm i and lock to update the lockfiles accordingly
version:
@lerna version minor --no-git-tag-version --force-publish --yes && pnpm i --lockfile-only && borg version minor && borg lock
@lerna version patch --no-git-tag-version --force-publish --yes && pnpm i --lockfile-only && borg version patch && borg lock

# Publish all the packages to the appropriate repositories
publish:
Expand Down
2 changes: 1 addition & 1 deletion borg.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version = "1.4.3"
version = "1.4.4"
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"packages": [
"packages/**"
],
"version": "1.4.3",
"version": "1.4.4",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
4 changes: 2 additions & 2 deletions packages/eslint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@darajs/eslint-config",
"version": "1.4.3",
"version": "1.4.4",
"description": "Dara ESLint configuration",
"author": "Krzysztof Bielikowicz <[email protected]>",
"license": "Apache-2.0",
Expand All @@ -10,7 +10,7 @@
],
"prettier": "@darajs/prettier-config",
"dependencies": {
"@darajs/prettier-config": "^1.4.3",
"@darajs/prettier-config": "^1.4.4",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^7.5.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/prettier-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@darajs/prettier-config",
"version": "1.4.3",
"version": "1.4.4",
"description": "Dara Prettier configuration",
"author": "Krzysztof Bielikowicz <[email protected]>",
"license": "Apache-2.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/styled-components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@darajs/styled-components",
"version": "1.4.3",
"version": "1.4.4",
"description": "CL wrapper around styled components so everything gets the same instance",
"main": "dist/index.js",
"license": "Apache-2.0",
Expand All @@ -12,9 +12,9 @@
},
"prettier": "@darajs/prettier-config",
"devDependencies": {
"@darajs/eslint-config": "^1.4.3",
"@darajs/prettier-config": "^1.4.3",
"@darajs/stylelint-config": "^1.4.3",
"@darajs/eslint-config": "^1.4.4",
"@darajs/prettier-config": "^1.4.4",
"@darajs/stylelint-config": "^1.4.4",
"@types/styled-components": "^5.1.26",
"eslint": "^7.32.0",
"prettier": "2.4.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/stylelint-config/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@darajs/stylelint-config",
"version": "1.4.3",
"version": "1.4.4",
"description": "Dara Stylelint configuration",
"author": "Krzysztof Bielikowicz <[email protected]>",
"main": "index.json",
Expand Down
5 changes: 4 additions & 1 deletion packages/ui-causal-graph-editor/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ title: Changelog

## NEXT

- Added support for tiered layout in `Fcose`, `Spring` and `Marketing` layouts. It allows for nodes to be placed on tiers following some hierarchy and to further define requirements of nodes positions within that tier.

## 1.4.4

- Fix an issue where dragging nodes too quickly would cause the node drag to stop working
- Added support for tiered layout in `Fcose`. It allows for nodes to be placed on tiers following some hierarchy and to further define requirements of nodes positions within that tier.
- Added `Soft Directed` edge to legend of `EdgeEncoder`

## 1.4.0
Expand Down
20 changes: 10 additions & 10 deletions packages/ui-causal-graph-editor/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@darajs/ui-causal-graph-editor",
"version": "1.4.3",
"version": "1.4.4",
"description": "CausalGraph editor for the Dara UI framework",
"main": "dist/index.js",
"module": "dist/index.js",
Expand All @@ -23,9 +23,9 @@
"@babel/preset-env": "^7.15.8",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@darajs/eslint-config": "^1.4.3",
"@darajs/prettier-config": "^1.4.3",
"@darajs/stylelint-config": "^1.4.3",
"@darajs/eslint-config": "^1.4.4",
"@darajs/prettier-config": "^1.4.4",
"@darajs/stylelint-config": "^1.4.4",
"@storybook/addon-a11y": "^6.5.16",
"@storybook/addon-actions": "^6.5.16",
"@storybook/addon-essentials": "^6.5.16",
Expand Down Expand Up @@ -65,12 +65,12 @@
"typescript": "^5.0.4"
},
"dependencies": {
"@darajs/styled-components": "^1.4.3",
"@darajs/ui-components": "^1.4.3",
"@darajs/ui-icons": "^1.4.3",
"@darajs/ui-notifications": "^1.4.3",
"@darajs/ui-utils": "^1.4.3",
"@darajs/ui-widgets": "^1.4.3",
"@darajs/styled-components": "^1.4.4",
"@darajs/ui-components": "^1.4.4",
"@darajs/ui-icons": "^1.4.4",
"@darajs/ui-notifications": "^1.4.4",
"@darajs/ui-utils": "^1.4.4",
"@darajs/ui-widgets": "^1.4.4",
"@pixi-essentials/cull": "~2.0.0",
"@pixi/app": "~7.2.0",
"@pixi/constants": "~7.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
} from '../shared/graph-layout';
import { CausalGraph, EdgeConstraintType, EdgeType, EditorMode, VariableType } from '../types';
import { CausalGraphEditorProps, default as CausalGraphViewerComponent } from './causal-graph-editor';
import { Template, causalGraph, pagCausalGraph } from './utils/stories-utils';
import { Template, causalGraph, nodeTiersCausalGraph, nodeTiersList, pagCausalGraph } from './utils/stories-utils';

export default {
component: CausalGraphViewerComponent,
Expand Down Expand Up @@ -144,6 +144,16 @@ MarketingCenter.args = {
graphLayout: MarketingLayout.Builder.targetLocation('center').build(),
};

export const MarketingTiers = Template.bind({});
const marketingLayout = SpringLayout.Builder.build();
marketingLayout.tiers = nodeTiersList;

MarketingTiers.args = {
editable: true,
graphData: nodeTiersCausalGraph,
graphLayout: marketingLayout,
};

export const PlanarVertical = Template.bind({});
PlanarVertical.args = {
editable: true,
Expand All @@ -165,6 +175,27 @@ Spring.args = {
graphLayout: SpringLayout.Builder.build(),
};

export const SpringTiersArray = Template.bind({});
const springArrayLayout = SpringLayout.Builder.build();
springArrayLayout.tiers = nodeTiersList;

SpringTiersArray.args = {
editable: true,
graphData: nodeTiersCausalGraph,
graphLayout: springArrayLayout,
};

export const SpringTiers = Template.bind({});
const springLayout = SpringLayout.Builder.build();
springLayout.tiers = { group: 'meta.group', order_nodes_by: 'meta.order', rank: ['a', 'b', 'c', 'd', 'e'] };
springLayout.tierSeparation = 300;

SpringTiers.args = {
editable: true,
graphData: FRAUD,
graphLayout: springLayout,
};

export const Circular = Template.bind({});
Circular.args = {
editable: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,4 @@ export const nodeTiersCausalGraph = {
},
};

export const nodeTiersList = [['target'], ['input1', 'input2'], ['input3', 'input4']];
export const nodeTiersList = [['input1', 'input2'], ['input3', 'input4'], ['target']];
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import fcose, { FcoseLayoutOptions, FcoseRelativePlacementConstraint } from 'cyt
import { LayoutMapping, XYPosition } from 'graphology-layout/utils';

import { SimulationGraph } from '../../types';
import { getNodeOrder, getTiersArray } from '../utils';
import { DirectionType, GraphLayout, GraphLayoutBuilder, GraphTiers, TieredGraphLayoutBuilder } from './common';

cytoscape.use(fcose);
Expand Down Expand Up @@ -154,33 +155,6 @@ class FcoseLayoutBuilder extends GraphLayoutBuilder<FcoseLayout> implements Tier
}
}

/**
* Based on a node attribute checks if the path is in the attribute or in extras, if not found returns undefined
* @param attributes node object or a sub attribute of the node object
* @param path path to the attribute
* */
function getPathInNodeAttribute(attributes: Record<string, any>, path: string): any {
let searchablePath = path;
// If attribute becomes undefined we have a non valid path within the node
if (attributes === undefined) {
throw new Error('Could not find path for rank or group within Node');
}
// If path is in meta change it to originalMeta
if (searchablePath === 'meta') {
searchablePath = 'originalMeta';
}
// Check if path is in node attributes
if (Object.prototype.hasOwnProperty.call(attributes, searchablePath)) {
return attributes[searchablePath];
}
// If not check if it has been moved to extras
if (attributes?.extras && searchablePath in attributes.extras) {
return attributes.extras[searchablePath];
}
// If not found the node does not have that attribute
return undefined;
}

/**
* Creates an array of relative placements for a given tier given a certain order of nodes and orientation
* @param tier tier to be placed
Expand Down Expand Up @@ -268,59 +242,6 @@ function getRelativeTieredArrayPlacement(
return relativePlacements;
}

/**
* Gets nodes grouped by a given attribute
* @param nodes nodes to be grouped
* @param group the attribute to group by
* @param graph the graph
* */
function getNodeGroups(nodes: string[], group: string, graph: SimulationGraph): Record<string, string[]> {
const attributePathArray = group.split('.');

return nodes.reduce((groupAccumulator: Record<string, string[]>, node) => {
const nodeAttributes = graph.getNodeAttributes(node);
// The node attribute containing the group can be deep within the node, e.g. meta.rendering_properties.group
// or anywhere else defined by the user. Here we tranverse the path checking what the group value is.
const nodeGroup = attributePathArray.reduce(getPathInNodeAttribute, nodeAttributes);

// If it is not undefined at this point i.e. node group was found
if (nodeGroup !== undefined) {
const groupKey = String(nodeGroup);
// if group is not in tieredNodes add it, if it is add node to that tier
if (groupKey in groupAccumulator) {
groupAccumulator[groupKey].push(node);
} else {
groupAccumulator[groupKey] = [node];
}
}
return groupAccumulator;
}, {});
}

/**
* Gets nodes grouped by a given attribute
* @param nodes nodes to be grouped
* @param group the attribute to group by
* @param graph the graph
* */
function getNodeOrder(nodes: string[], orderPath: string, graph: SimulationGraph): Record<string, string> {
const attributePathArray = orderPath.split('.');

return nodes.reduce((groupAccumulator: Record<string, string>, node) => {
const nodeAttributes = graph.getNodeAttributes(node);
// The node attribute containing the group can be deep within the node, e.g. meta.rendering_properties.group
// or anywhere else defined by the user. Here we tranverse the path checking what the group value is.
const nodeOrder = attributePathArray.reduce(getPathInNodeAttribute, nodeAttributes);

// If it is not undefined at this point i.e. node order was found
if (nodeOrder !== undefined) {
const order = String(nodeOrder);
groupAccumulator[node] = order;
}
return groupAccumulator;
}, {});
}

interface TiersProperties {
alignmentConstraint?: string[][];
relativePlacementConstraint?: FcoseRelativePlacementConstraint[];
Expand All @@ -338,33 +259,14 @@ export function getTieredLayoutProperties(
orientation: DirectionType,
tierSeparation: number
): TiersProperties {
let tiersArray: string[][] = Array.isArray(tiers) ? tiers : [];
const tiersArray = getTiersArray(tiers, graph);
let nodesOrder: Record<string, string>;

if (!Array.isArray(tiers)) {
// must be of type TiersConfig
const { group, order_nodes_by, rank } = tiers;
const { order_nodes_by } = tiers;
const nodes = graph.nodes();
const tieredNodes = getNodeGroups(nodes, group, graph);
nodesOrder = order_nodes_by ? getNodeOrder(nodes, order_nodes_by, graph) : undefined;

// if rank is defined use it to order the tiers
if (rank) {
const missingGroups: string[] = [];
tiersArray = rank.map((key) => {
if (!(key in tieredNodes)) {
missingGroups.push(key);
return [];
}
return tieredNodes[key];
});

if (missingGroups.length > 0) {
throw new Error(`Group(s) ${missingGroups.join(', ')} defined in rank not found within any Nodes`);
}
} else {
tiersArray = Object.values(tieredNodes);
}
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,20 @@ import { LayoutMapping, XYPosition } from 'graphology-layout/utils';

import { SimulationGraph, SimulationNodeWithGroup } from '../../types';
import { getD3Data, nodesToLayout } from '../parsers';
import { GraphLayout, GraphLayoutBuilder } from './common';
import { DirectionType, GraphLayout, GraphLayoutBuilder, GraphTiers, TieredGraphLayoutBuilder } from './common';
import { applyTierForces } from './spring-layout';

export type TargetLocation = 'center' | 'bottom';

class MarketingLayoutBuilder extends GraphLayoutBuilder<MarketingLayout> {
class MarketingLayoutBuilder extends GraphLayoutBuilder<MarketingLayout> implements TieredGraphLayoutBuilder {
_targetLocation: TargetLocation = 'bottom';

_tierSeparation = 300;

orientation: DirectionType = 'horizontal';

tiers: GraphTiers;

/**
* Sets the target location and returns the builder
*
Expand All @@ -37,6 +44,16 @@ class MarketingLayoutBuilder extends GraphLayoutBuilder<MarketingLayout> {
return this;
}

/**
* Set tier separation
*
* @param separation separation
*/
tierSeparation(separation: number): this {
this._tierSeparation = separation;
return this;
}

build(): MarketingLayout {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return new MarketingLayout(this);
Expand All @@ -52,9 +69,18 @@ class MarketingLayoutBuilder extends GraphLayoutBuilder<MarketingLayout> {
export default class MarketingLayout extends GraphLayout {
public targetLocation: TargetLocation = 'bottom';

public tierSeparation: number;

public orientation: DirectionType;

public tiers: GraphTiers;

constructor(builder: MarketingLayoutBuilder) {
super(builder);
this.targetLocation = builder._targetLocation;
this.tierSeparation = builder._tierSeparation;
this.orientation = builder.orientation;
this.tiers = builder.tiers;
}

applyLayout(graph: SimulationGraph): Promise<{
Expand Down Expand Up @@ -123,6 +149,10 @@ export default class MarketingLayout extends GraphLayout {
)
.stop();

if (this.tiers) {
applyTierForces(simulation, graph, nodes, this.tiers, this.tierSeparation, this.orientation);
}

simulation.tick(1000);

return Promise.resolve({ layout: nodesToLayout(simulation.nodes()) });
Expand Down
Loading

0 comments on commit 84d4e73

Please sign in to comment.