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: Add state setters with options #95

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
49 changes: 44 additions & 5 deletions src/models/edge.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { INodeBase, INode } from './node';
import { GraphObjectState } from './state';
import { GraphObjectState, IGraphObjectStateOptions, IGraphObjectStateParameters } from './state';
import { Color, IPosition, ICircle, getDistanceToLine } from '../common';
import { isArrayOfNumbers, isFunction } from '../utils/type.utils';
import { isArrayOfNumbers, isFunction, isNumber, isPlainObject } from '../utils/type.utils';
import { IObserver, ISubject, Subject } from '../utils/observer.utils';
import { patchProperties } from '../utils/object.utils';

Expand Down Expand Up @@ -120,7 +120,9 @@ export interface IEdge<N extends INodeBase, E extends IEdgeBase> extends ISubjec
patchStyle(style: IEdgeStyle): void;
patchStyle(callback: (edge: IEdge<N, E>) => IEdgeStyle): void;
setState(state: number): void;
setState(state: IGraphObjectStateParameters): void;
setState(callback: (edge: IEdge<N, E>) => number): void;
setState(callback: (edge: IEdge<N, E>) => IGraphObjectStateParameters): void;
}

export interface IEdgeSettings {
Expand Down Expand Up @@ -400,15 +402,52 @@ abstract class Edge<N extends INodeBase, E extends IEdgeBase> extends Subject im
}

setState(state: number): void;
setState(state: IGraphObjectStateParameters): void;
setState(callback: (edge: IEdge<N, E>) => number): void;
setState(arg: number | ((edge: IEdge<N, E>) => number)): void {
setState(callback: (edge: IEdge<N, E>) => IGraphObjectStateParameters): void;
setState(
arg:
| number
| IGraphObjectStateParameters
| ((edge: IEdge<N, E>) => number)
| ((edge: IEdge<N, E>) => IGraphObjectStateParameters),
): void {
let result: number | IGraphObjectStateParameters;

if (isFunction(arg)) {
this._state = (arg as (edge: IEdge<N, E>) => number)(this);
result = (arg as (edge: IEdge<N, E>) => number | IGraphObjectStateParameters)(this);
} else {
this._state = arg as number;
result = arg;
}

if (isNumber(result)) {
this._state = result;
} else if (isPlainObject(result)) {
const options = result.options;

this._state = this._handleState(result.state, options);

if (options) {
this.notifyListeners({
id: this.id,
type: 'edge',
options: options,
});

return;
}
}

this.notifyListeners();
}

private _handleState(state: number, options?: Partial<IGraphObjectStateOptions>): number {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is nitpicking really, but maybe getNextState would be a better name.

if (options?.isToggle && this._state === state) {
return GraphObjectState.NONE;
} else {
return state;
}
}
}

const getEdgeType = <N extends INodeBase, E extends IEdgeBase>(data: IEdgeData<N, E>): EdgeType => {
Expand Down
21 changes: 21 additions & 0 deletions src/models/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,27 @@ export class Graph<N extends INodeBase, E extends IEdgeBase> extends Subject imp
}

update(data?: IObserverDataPayload): void {
if (data && 'type' in data && 'options' in data && 'isSingle' in data.options) {
if (data.type === 'node' && data.options.isSingle) {
Comment on lines +384 to +385
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you maybe simplify this to if (data && data.options?.isSingle && data.type === 'node')?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it could be possible, but only after reworking IObserverDataPayload into a more generic solution as you mentioned here. Currently, I am using some sort of custom type guard because of the interfaces used in that payload

const nodes = this._nodes.getAll();

for (let i = 0; i < nodes.length; i++) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like these iterations through all nodes/edges can be slow. Any ideas? And do we want to clear the state of all nodes/edges or only the adjusting ones?

Copy link
Contributor

@cizl cizl Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using hash maps? Obviously, this would require a lot of code change. Just starting a discussion, I don't think it's doable right now.

For the second question, I would say only the ones that are being adjusted.

Copy link
Contributor

@cizl cizl Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe to supplement this nodes array we can keep an index where nodeIndex[node.id] === nodes.findIndex((el) => el.id === node.id)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting storing nodes in a hashmap? As far as I know, retrieving elements from an array by a known index is O(1) complexity, so iterating through an array vs a hashmap (which also has O(1) complexity for retrieving element by key) may not make a big difference here. I might have misunderstood your point, or perhaps I missed something, so feel free to clarify it further :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all nodes are in state X, and you want to clear them up if they are not node with ID 1, you still need to to N - 1 updates. So hash maps or any other structure won't help there, you already have the fastest structure, an array.

But, you can do this and increase the memory footprint: I am not sure if it makes sense to go into this right now, but this would be the plan:

  • Graph keeps a structure of nodes/edges by state, so each state has a structure of nodes and edges (probably just id) - a set of a map.
  • States in nodes and edges are just views, readonly
  • When state is updated in node, edge it is just updated in the graph

This way, if you have isSingle: true, graph, as an owner of the data can then literally take all selected nodes from a set or a map, clear those states and set the new node as single selected. This would improve the speed, but it would worse the memory footprint and code simplitiy because owner of the state becomes the graph, not graph objects (node/edge).

I would keep this as is and fix only if we see that it hurts performance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I misunderstood the functionality. I saw === instead of !==. I thought you were trying to clear the state of anode with a specific id.

if (nodes[i].id !== data.id) {
nodes[i].clearState();
}
}
}

if (data.type === 'edge' && data.options.isSingle) {
const edges = this._edges.getAll();

for (let i = 0; i < edges.length; i++) {
if (edges[i].id !== data.id) {
edges[i].clearState();
}
}
}
}
this.notifyListeners(data);
}

Expand Down
49 changes: 44 additions & 5 deletions src/models/node.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { IEdge, IEdgeBase } from './edge';
import { Color, IPosition, IRectangle, isPointInRectangle } from '../common';
import { ImageHandler } from '../services/images';
import { GraphObjectState } from './state';
import { GraphObjectState, IGraphObjectStateOptions, IGraphObjectStateParameters } from './state';
import { IObserver, ISubject, Subject } from '../utils/observer.utils';
import { patchProperties } from '../utils/object.utils';
import { isFunction } from '../utils/type.utils';
import { isFunction, isNumber, isPlainObject } from '../utils/type.utils';

/**
* Node baseline object with required fields
Expand Down Expand Up @@ -123,7 +123,9 @@ export interface INode<N extends INodeBase, E extends IEdgeBase> extends ISubjec
patchStyle(style: INodeStyle): void;
patchStyle(callback: (node: INode<N, E>) => INodeStyle): void;
setState(state: number): void;
setState(state: IGraphObjectStateParameters): void;
setState(callback: (node: INode<N, E>) => number): void;
setState(callback: (node: INode<N, E>) => IGraphObjectStateParameters): void;
}

// TODO: Dirty solution: Find another way to listen for global images, maybe through
Expand Down Expand Up @@ -526,17 +528,54 @@ export class Node<N extends INodeBase, E extends IEdgeBase> extends Subject impl
}

setState(state: number): void;
setState(state: IGraphObjectStateParameters): void;
setState(callback: (node: INode<N, E>) => number): void;
setState(arg: number | ((node: INode<N, E>) => number)): void {
setState(callback: (node: INode<N, E>) => IGraphObjectStateParameters): void;
setState(
arg:
| number
| IGraphObjectStateParameters
| ((node: INode<N, E>) => number)
| ((node: INode<N, E>) => IGraphObjectStateParameters),
): void {
let result: number | IGraphObjectStateParameters;

if (isFunction(arg)) {
this._state = (arg as (node: INode<N, E>) => number)(this);
result = (arg as (node: INode<N, E>) => number | IGraphObjectStateParameters)(this);
} else {
this._state = arg as number;
result = arg;
}

if (isNumber(result)) {
this._state = result;
} else if (isPlainObject(result)) {
const options = result.options;

this._state = this._handleState(result.state, options);

if (options) {
this.notifyListeners({
id: this.id,
type: 'node',
options: options,
});

return;
}
}

this.notifyListeners();
}

protected _isPointInBoundingBox(point: IPosition): boolean {
return isPointInRectangle(this.getBoundingBox(), point);
}

private _handleState(state: number, options?: Partial<IGraphObjectStateOptions>): number {
if (options?.isToggle && this._state === state) {
return GraphObjectState.NONE;
} else {
return state;
}
}
}
18 changes: 18 additions & 0 deletions src/models/state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import { GraphObject } from '../utils/observer.utils';

// Enum is dismissed so user can define custom additional events (numbers)
export const GraphObjectState = {
NONE: 0,
SELECTED: 1,
HOVERED: 2,
};

export interface IGraphObjectStateOptions {
isToggle: boolean;
isSingle: boolean;
}

export interface IGraphObjectStateParameters {
state: number;
options?: Partial<IGraphObjectStateOptions>;
}

export interface ISetStateDataPayload {
id: any;
type: GraphObject;
options: Partial<IGraphObjectStateOptions>;
}
5 changes: 4 additions & 1 deletion src/utils/observer.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { INodeCoordinates, INodeMapCoordinates, INodePosition } from '../models/node';
import { ISetStateDataPayload } from '../models/state';

export type IObserverDataPayload = INodePosition | INodeCoordinates | INodeMapCoordinates;
export type GraphObject = 'node' | 'edge';

export type IObserverDataPayload = INodePosition | INodeCoordinates | INodeMapCoordinates | ISetStateDataPayload;

export interface IObserver {
update(data?: IObserverDataPayload): void;
Expand Down
Loading