import _ from "underscore"; import { SimulationNodeDatum, SimulationLinkDatum } from "d3"; export interface PaginationInfo { total: number; // Note: end is the upper-bound outside of the page range. currentPage: { start: number; end: number }; } export interface GremlinVertex { id: string; label: string; inE?: { [label: string]: GremlinShortInEdge[] }; outE?: { [label: string]: GremlinShortOutEdge[] }; properties?: { [propName: string]: GremlinProperty[] }; // Private use. Not part of Gremlin's specs _isRoot?: boolean; _isFixedPosition?: boolean; _pagination?: PaginationInfo; _ancestorsId?: string[]; _inEAllLoaded?: boolean; _outEAllLoaded?: boolean; _outEdgeIds?: string[]; _inEdgeIds?: string[]; } export interface GremlinShortInEdge { id: string; outV: string; } export interface GremlinShortOutEdge { id: string; inV: string; } export interface GremlinEdge { id: string; inV: string; outV: string; label: string; } export interface GremlinProperty { id: string; value: string | number | boolean; } export interface MapArray { [id: string]: string[]; } /* * D3 adds fields (such as x,y, ...) to the original vertices and edges. * For D3 purposes, we extend GremlinVertex and GremlinEdge to support d3's functionality. * By keeping GremlinVertex, GremlinEdge different from D3Node and D3Link, we can decouple * the Gremlin code/cosmosdb code (in GraphExplorerComponents.ts) from the * D3 visualization implementation (in D3ForceGraph.ts). * * GraphData's logic works with both type pairs. */ export interface D3Node extends GremlinVertex, SimulationNodeDatum {} export interface D3Link extends GremlinEdge, SimulationLinkDatum {} /** * Functionality related to graph manipulation */ export class GraphData { private _vertices: V[]; private _edges: E[]; // Keep track of neighbors' id private _targetsMap: MapArray; private _sourcesMap: MapArray; // Lookup vertex name by id private _id2VertexMap: { [id: string]: V }; constructor() { this._vertices = []; this._edges = []; this._targetsMap = {}; this._sourcesMap = {}; this._id2VertexMap = {}; } /** * Copy from plain object * @param data */ public setData(data: any) { if (!data) { return; } const props = ["_vertices", "_edges", "_targetsMap", "_sourcesMap", "_id2VertexMap"]; for (let i = 0; i < props.length; i++) { if (!data.hasOwnProperty(props[i])) { return; } } this._vertices = data._vertices; this._edges = data._edges; this._targetsMap = data._targetsMap; this._sourcesMap = data._sourcesMap; this._id2VertexMap = data._id2VertexMap; } public get vertices(): V[] { return this._vertices; } public get edges(): E[] { return this._edges; } public getVertexById(id: string): V { return this._id2VertexMap[id]; } public hasVertexId(id: string): boolean { return this._id2VertexMap.hasOwnProperty(id); } public get ids(): string[] { return Object.keys(this._id2VertexMap); } public addVertex(vertex: V) { if (this.ids.indexOf(vertex.id) !== -1) { // Make sure vertex is not already in return; } this._vertices.push(vertex); this._id2VertexMap[vertex.id] = vertex; this.addNeighborInfo(vertex); } /** * Look in this inE and outE and update nodes already in the graph * @param vertex */ public addNeighborInfo(vertex: V) { // Add edge if the other end is in the graph if (vertex.hasOwnProperty("inE")) { for (let p in vertex["inE"]) { vertex["inE"][p].forEach((e: GremlinShortInEdge) => { if (this.hasVertexId(e.outV)) { const v = this.getVertexById(e.outV); GraphData.addOutE(v, p, { id: e.id, inV: e.outV, }); } }); } } if (vertex.hasOwnProperty("outE")) { for (let p in vertex["outE"]) { vertex["outE"][p].forEach((e: GremlinShortOutEdge) => { if (this.hasVertexId(e.inV)) { const v = this.getVertexById(e.inV); GraphData.addInE(v, p, { id: e.id, outV: e.inV, }); } }); } } } public getSourcesForId(id: string): string[] { return this._sourcesMap[id]; } public getTargetsForId(id: string): string[] { return this._targetsMap[id]; } public addEdge(edge: E): void { // Check if edge is not already in for (let i = 0; i < this._edges.length; i++) { if (this._edges[i].inV === edge.inV && this._edges[i].outV === edge.outV) { return; } } // Add edge only if both ends of the edge are in the graph if (this.hasVertexId(edge.inV) && this.hasVertexId(edge.outV)) { this._edges.push(edge); } GraphData.addToMap(this._targetsMap, edge.outV, edge.inV); GraphData.addToMap(this._sourcesMap, edge.inV, edge.outV); // Add edge info to vertex if (this.hasVertexId(edge.inV)) { GraphData.addInE(this.getVertexById(edge.inV), edge.label, edge); } if (this.hasVertexId(edge.outV)) { GraphData.addOutE(this.getVertexById(edge.outV), edge.label, edge); } } /** * Add inE edge to vertex * @param v * @param edge */ public static addInE(v: GremlinVertex, label: string, edge: GremlinShortInEdge) { v["inE"] = v["inE"] || {}; v["inE"][label] = v["inE"][label] || []; GraphData.addToEdgeArray(edge, v["inE"][label]); } /** * Add outE edge to vertex * @param v * @param edge */ public static addOutE(v: GremlinVertex, label: string, edge: GremlinShortOutEdge) { v["outE"] = v["outE"] || {}; v["outE"][label] = v["outE"][label] || []; GraphData.addToEdgeArray(edge, v["outE"][label]); } /** * Make sure to not add the same edge if already exists * TODO Unit test this! * @param edge * @param edgeArray */ public static addToEdgeArray( edge: GremlinShortInEdge | GremlinShortOutEdge, edgeArray: (GremlinShortInEdge | GremlinShortOutEdge)[] ) { for (let i = 0; i < edgeArray.length; i++) { if (edgeArray[i].id === edge.id) { return; } } edgeArray.push(edge); } /** * Merge all inE and outE from source to target * @param target * @param source */ public static addEdgeInfoToVertex(target: GremlinVertex, source: GremlinVertex): void { if (source.hasOwnProperty("outE")) { for (let p in source.outE) { source.outE[p].forEach((e: GremlinShortOutEdge) => { GraphData.addOutE(target, p, e); }); } } if (source.hasOwnProperty("inE")) { for (let p in source.inE) { source.inE[p].forEach((e: GremlinShortInEdge) => { GraphData.addInE(target, p, e); }); } } } public unloadAllVertices(excludedIds: string[]): void { this.ids.forEach((id: string) => { if (excludedIds.indexOf(id) !== -1) { return; } this.removeVertex(id, true); }); } /** * IE doesn't support Object.values(object) * @param object */ public static getValues(object: any) { if (!object) { return []; } return Object.keys(object).map((p: string) => { return object[p]; }); } /** * Erase all references to vertex in graph * @param id * @param unloadOnly true: unload from cache. Delete edge and vertices, but not the references to it in the vertices */ public removeVertex(id: string, unloadOnly: boolean): void { if (!this.hasVertexId(id)) { console.error("No vertex to delete found with id", id); return; } // Find all edges that touches this vertex and remove them let edgeIds: string[] = []; this._edges.forEach((edge: E) => { if (edge.inV === id || edge.outV === id) { edgeIds.push(edge.id); } }); edgeIds.forEach((id: string) => { this.removeEdge(id, unloadOnly); }); GraphData.removeFromMap(this._sourcesMap, id); GraphData.removeFromMap(this._targetsMap, id); // Delete from the map this.deleteVertex(id); } /** * Remove edge from graph data * @param edgeId * @param unloadOnly remove edge, but not references in the nodes */ public removeEdge(edgeId: string, unloadOnly: boolean) { // Remove from edges array for (let i = 0; i < this._edges.length; i++) { if (this._edges[i].id === edgeId) { const edge = this._edges[i]; this._edges.splice(i, 1); GraphData.removeEltFromMap(this._sourcesMap, edge.inV, edge.outV); GraphData.removeEltFromMap(this._targetsMap, edge.outV, edge.inV); break; } } if (!unloadOnly) { // Cleanup vertices this._vertices.forEach((vertex: GremlinVertex) => { GraphData.getValues(vertex.inE).forEach((edges: GremlinShortInEdge[]) => { for (let i = 0; i < edges.length; i++) { if (edges[i].id === edgeId) { edges.splice(i, 1); return; } } }); GraphData.getValues(vertex.outE).forEach((edges: GremlinShortOutEdge[]) => { for (let i = 0; i < edges.length; i++) { if (edges[i].id === edgeId) { edges.splice(i, 1); return; } } }); }); } } /** * Set this node as root, clear root tag for other nodes * @param id */ public setAsRoot(id: string) { this._vertices.forEach((v: V) => { delete v._isRoot; delete v._isFixedPosition; }); if (this.hasVertexId(id)) { const v = this.getVertexById(id); v._isRoot = true; v._isFixedPosition = true; } } /** * Find root node id * @return root node id if found, undefined otherwise */ public findRootNodeId(): string { return ( _.find(this._vertices, (v: GremlinVertex) => { return !!v._isRoot; }) || ({} as any) ).id; } /** * * @param v * @return list of edge ids for this vertex */ public static getEdgesId(v: GremlinVertex): string[] { const ids: string[] = []; if (v.inE) { for (var l in v.inE) { v.inE[l].forEach((e: GremlinShortInEdge) => ids.push(e.id)); } } if (v.outE) { for (var l in v.outE) { v.outE[l].forEach((e: GremlinShortOutEdge) => ids.push(e.id)); } } return ids; } /** * Retrieve node's property value * @param node * @param prop */ public static getNodePropValue(node: D3Node, prop: string): undefined | string | number | boolean { if (node.hasOwnProperty(prop)) { return (node as any)[prop]; } // This is DocDB specific if (node.properties && node.properties.hasOwnProperty(prop)) { return node.properties[prop][0]["value"]; } return undefined; } /** * Add a new value to the key-values map * key <--> [ value1, value2, ... ] * @param kvmap * @param key * @param value */ private static addToMap(kvmap: { [id: string]: string[] }, key: string, value: string): void { var values: string[] = []; if (kvmap.hasOwnProperty(key)) { values = kvmap[key]; } else { kvmap[key] = values; } if (values.indexOf(value) === -1) { values.push(value); } } private deleteVertex(id: string): void { const v = this.getVertexById(id); const n = this.vertices.indexOf(v); this._vertices.splice(n, 1); delete this._id2VertexMap[id]; } private static removeIdFromArray(idArray: string[], id2remove: string) { const n = idArray.indexOf(id2remove); if (n !== -1) { idArray.splice(n, 1); } } /** * Remove id from map * Note: map may end up with empty arrays * @param map * @param id2remove */ private static removeFromMap(map: MapArray, id2remove: string) { // First remove entry if it exists if (map.hasOwnProperty(id2remove)) { delete map[id2remove]; } // Then remove element if it's in any array GraphData.getValues(map).forEach((idArray: string[]) => { GraphData.removeIdFromArray(idArray, id2remove); }); } /** * Remove value2remove for entryId in map * @param map * @param entryId * @param value2Remove */ private static removeEltFromMap(map: MapArray, entryId: string, id2remove: string) { const idArray = map[entryId]; if (!idArray || idArray.length < 1) { return; } GraphData.removeIdFromArray(idArray, id2remove); } /** * Get list of children ids of a given vertex * @param vertex */ public static getChildrenId(vertex: GremlinVertex): string[] { const ids = {}; // HashSet if (vertex.hasOwnProperty("outE")) { let outE = vertex.outE; for (var label in outE) { for (let i = 0; i < outE[label].length; i++) { let edge = outE[label][i]; ids[edge.inV] = true; } } } if (vertex.hasOwnProperty("inE")) { let inE = vertex.inE; for (var label in inE) { for (let i = 0; i < inE[label].length; i++) { let edge = inE[label][i]; ids[edge.outV] = true; } } } return Object.keys(ids); } }