From e7eb3a8bbb889f302a90a2fc26bbee9b87a7afff Mon Sep 17 00:00:00 2001 From: Oskar Damkjaer Date: Thu, 18 Aug 2022 15:44:46 +0200 Subject: [PATCH 1/3] Add precompute and zoom to fit on load --- .../VisualizationView/VisualizationView.tsx | 2 + .../GraphVisualizer/Graph/Graph.tsx | 43 ++++++++++--------- .../Graph/visualization/ForceSimulation.ts | 31 +++++++++---- .../Graph/visualization/Visualization.ts | 25 ++++++++--- .../GraphVisualizer/GraphVisualizer.tsx | 2 + .../graph-visualization/constants.ts | 4 +- 6 files changed, 69 insertions(+), 38 deletions(-) diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx index a23d2ac16b4..254da3e4fd2 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.tsx @@ -277,6 +277,7 @@ LIMIT ${maxNewNeighbours}` updateStyle={this.props.updateStyle} getNeighbours={this.getNeighbours.bind(this)} nodes={this.state.nodes} + autocompleteRelationships={this.props.autoComplete ?? false} relationships={this.state.relationships} isFullscreen={this.props.isFullscreen} assignVisElement={this.props.assignVisElement} @@ -301,6 +302,7 @@ LIMIT ${maxNewNeighbours}` DetailsPaneOverride={DetailsPane} OverviewPaneOverride={OverviewPane} useGeneratedDefaultColors={false} + initialZoomToFit /> ) diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/Graph.tsx b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/Graph.tsx index dd8c1ec692c..499ca3b06f8 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/Graph.tsx +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/Graph.tsx @@ -61,6 +61,7 @@ export type GraphProps = { styleVersion: number onGraphModelChange: (stats: GraphStats) => void assignVisElement: (svgElement: any, graphElement: any) => void + autocompleteRelationships: boolean getAutoCompleteCallback: ( callback: (internalRelationships: BasicRelationship[]) => void ) => void @@ -105,20 +106,21 @@ export class Graph extends React.Component { componentDidMount(): void { const { - nodes, - relationships, - graphStyle, + assignVisElement, + autocompleteRelationships, + getAutoCompleteCallback, getNodeNeighbours, + graphStyle, + initialZoomToFit, + isFullscreen, + nodes, + onGraphInteraction, + onGraphModelChange, onItemMouseOver, onItemSelect, - onGraphModelChange, - onGraphInteraction, + relationships, setGraph, - getAutoCompleteCallback, - assignVisElement, - isFullscreen, - wheelZoomRequiresModKey, - initialZoomToFit + wheelZoomRequiresModKey } = this.props if (!this.svgElement.current) return @@ -137,7 +139,8 @@ export class Graph extends React.Component { graph, graphStyle, isFullscreen, - wheelZoomRequiresModKey + wheelZoomRequiresModKey, + initialZoomToFit ) const graphEventHandler = new GraphEventHandlerModel( @@ -153,30 +156,28 @@ export class Graph extends React.Component { onGraphModelChange(getGraphStats(graph)) this.visualization.resize(isFullscreen, !!wheelZoomRequiresModKey) - if (initialZoomToFit) { - this.visualization.endInitCallback = () => { - setTimeout(() => { - this.visualization?.zoomByType(ZoomType.FIT) - }, 150) - } - } - this.visualization.init() if (setGraph) { setGraph(graph) } - if (getAutoCompleteCallback) { + if (autocompleteRelationships) { getAutoCompleteCallback((internalRelationships: BasicRelationship[]) => { + this.visualization?.init() graph.addInternalRelationships( mapRelationships(internalRelationships, graph) ) onGraphModelChange(getGraphStats(graph)) this.visualization?.update({ updateNodes: false, - updateRelationships: true + updateRelationships: true, + restartSimulation: false }) + this.visualization?.precomputeAndStart() graphEventHandler.onItemMouseOut() }) + } else { + this.visualization?.init() + this.visualization?.precomputeAndStart() } if (assignVisElement) { assignVisElement(this.svgElement.current, this.visualization) diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/ForceSimulation.ts b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/ForceSimulation.ts index 97d0da15305..9e005711699 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/ForceSimulation.ts +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/ForceSimulation.ts @@ -36,8 +36,8 @@ import { FORCE_COLLIDE_RADIUS, FORCE_LINK_DISTANCE, LINK_DISTANCE, - PRECOMPUTED_TICKS, - TICKS_PER_RENDER, + MAX_PRECOMPUTED_TICKS, + EXTRA_TICKS_PER_RENDER, VELOCITY_DECAY } from '../../../constants' import { GraphModel } from '../../../models/Graph' @@ -52,14 +52,15 @@ export class ForceSimulation { simulation: Simulation simulationTimeout: null | number = null - constructor(private render: () => void) { + constructor(render: () => void) { this.simulation = forceSimulation() .velocityDecay(VELOCITY_DECAY) .force('charge', forceManyBody().strength(FORCE_CHARGE)) .force('centerX', forceX(0).strength(FORCE_CENTER_X)) .force('centerY', forceY(0).strength(FORCE_CENTER_Y)) + .alphaMin(DEFAULT_ALPHA_MIN) .on('tick', () => { - this.simulation.tick(TICKS_PER_RENDER) + this.simulation.tick(EXTRA_TICKS_PER_RENDER) render() }) .stop() @@ -91,12 +92,26 @@ export class ForceSimulation { ) } - precompute(): void { - this.simulation.stop().tick(PRECOMPUTED_TICKS) - this.render() + precomputeAndStart(onEnd: () => void = () => undefined): void { + this.simulation.stop() + + let precomputeTicks = 0 + const start = performance.now() + while ( + performance.now() - start < 250 && + precomputeTicks < MAX_PRECOMPUTED_TICKS + ) { + this.simulation.tick(1) + precomputeTicks += 1 + if (this.simulation.alpha() <= this.simulation.alphaMin()) { + break + } + } + + this.simulation.restart().on('end', onEnd) } restart(): void { - this.simulation.alphaMin(DEFAULT_ALPHA_MIN).alpha(DEFAULT_ALPHA).restart() + this.simulation.alpha(DEFAULT_ALPHA).restart() } } diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/Visualization.ts b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/Visualization.ts index 6c5659c3447..01c8912878c 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/Visualization.ts +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/Visualization.ts @@ -70,17 +70,17 @@ export class Visualization { // 'canvasClick' event when panning ends. private draw = false private isZoomClick = false - public endInitCallback: null | (() => void) = null constructor( element: SVGElement, private measureSize: MeasureSizeFn, - private onZoomEvent: (limitsReached: ZoomLimitsReached) => void, - private onDisplayZoomWheelInfoMessage: () => void, + onZoomEvent: (limitsReached: ZoomLimitsReached) => void, + onDisplayZoomWheelInfoMessage: () => void, private graph: GraphModel, public style: GraphStyleModel, public isFullscreen: boolean, - public wheelZoomRequiresModKey?: boolean + public wheelZoomRequiresModKey?: boolean, + private initialZoomToFit?: boolean ) { this.root = d3Select(element) @@ -330,12 +330,23 @@ export class Visualization { this.updateNodes() this.updateRelationships() - this.forceSimulation.precompute() this.adjustZoomMinScaleExtentToFitGraph() + this.setInitialZoom() + } + + setInitialZoom(): void { + const count = this.graph.nodes().length - this.endInitCallback && this.endInitCallback() - this.endInitCallback = null + // chosen by *feel* (graph fitting guesstimate) + const scale = -0.02364554 + 1.913 / (1 + (count / 12.7211) ** 0.8156444) + this.zoomBehavior.scaleBy(this.root, Math.max(0, scale)) + } + + precomputeAndStart(): void { + this.forceSimulation.precomputeAndStart( + () => this.initialZoomToFit && this.zoomByType(ZoomType.FIT) + ) } update(options: { diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/GraphVisualizer.tsx b/src/neo4j-arc/graph-visualization/GraphVisualizer/GraphVisualizer.tsx index 02e8dcc70dd..d8f86518c16 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/GraphVisualizer.tsx +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/GraphVisualizer.tsx @@ -84,6 +84,7 @@ type GraphVisualizerProps = GraphVisualizerDefaultProps & { OverviewPaneOverride?: React.FC onGraphInteraction?: GraphInteractionCallBack useGeneratedDefaultColors?: boolean + autocompleteRelationships: boolean } type GraphVisualizerState = { @@ -265,6 +266,7 @@ export class GraphVisualizer extends Component< onGraphModelChange={this.onGraphModelChange.bind(this)} assignVisElement={this.props.assignVisElement} getAutoCompleteCallback={this.props.getAutoCompleteCallback} + autocompleteRelationships={this.props.autocompleteRelationships} setGraph={this.props.setGraph} offset={ (this.state.nodePropertiesExpanded ? this.state.width + 8 : 0) + 8 diff --git a/src/neo4j-arc/graph-visualization/constants.ts b/src/neo4j-arc/graph-visualization/constants.ts index fbfd0474fd1..2f92a8c04c5 100644 --- a/src/neo4j-arc/graph-visualization/constants.ts +++ b/src/neo4j-arc/graph-visualization/constants.ts @@ -20,8 +20,8 @@ import { NodeModel } from './models/Node' import { RelationshipModel } from './models/Relationship' -export const PRECOMPUTED_TICKS = 300 -export const TICKS_PER_RENDER = 10 +export const MAX_PRECOMPUTED_TICKS = 300 +export const EXTRA_TICKS_PER_RENDER = 10 // Friction. export const VELOCITY_DECAY = 0.4 From fd8a665da22cba9358a4382370a90e5baeb27980 Mon Sep 17 00:00:00 2001 From: Oskar Damkjaer Date: Mon, 22 Aug 2022 16:13:47 +0200 Subject: [PATCH 2/3] fix tests --- e2e_tests/integration/viz.spec.ts | 6 +- .../VisualizationView.test.tsx | 13 - .../VisualizationView.test.tsx.snap | 248 ------------------ 3 files changed, 5 insertions(+), 262 deletions(-) diff --git a/e2e_tests/integration/viz.spec.ts b/e2e_tests/integration/viz.spec.ts index 06c24b82899..66fe6aca2c4 100644 --- a/e2e_tests/integration/viz.spec.ts +++ b/e2e_tests/integration/viz.spec.ts @@ -154,6 +154,10 @@ describe('Viz rendering', () => { parseSpecialCharSequences: false }) + const zoomOutButton = cy.get(`[aria-label="zoom-out"]`) + zoomOutButton.click({ force: true }) + zoomOutButton.wait(3000) + // Check that zoom in button increases the size of the node in the graph view cy.get('svg') .find(`[aria-label^="graph-node"]`) @@ -198,7 +202,7 @@ describe('Viz rendering', () => { // Enter fullscreen cy.get('article').find(`[title='Fullscreen']`).click() cy.get(`#svg-vis`).trigger('wheel', { deltaY: 3000 }) - + cy.get(`[aria-label="zoom-out"]`).should('be.disabled') // Leave fullscreen diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.test.tsx b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.test.tsx index 7fa4f743272..07986dc2692 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.test.tsx +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/VisualizationView.test.tsx @@ -65,12 +65,6 @@ function renderWithRedux(children: JSX.Element) { const mockEmptyResult = { records: [] } -const node = new (neo4j.types.Node as any)('1', ['Person'], { - prop1: 'String with HTML in it' -}) -const mockResult = { - records: [{ keys: ['0'], __fields: [node], get: (_key: any) => node }] -} test('Visualization renders empty content', () => { const { container } = renderWithRedux( @@ -78,10 +72,3 @@ test('Visualization renders empty content', () => { ) expect(container).toMatchSnapshot() }) - -test('Visualization renders with result and escapes any HTML', () => { - const { container } = renderWithRedux( - - ) - expect(container).toMatchSnapshot() -}) diff --git a/src/browser/modules/Stream/CypherFrame/VisualizationView/__snapshots__/VisualizationView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/VisualizationView/__snapshots__/VisualizationView.test.tsx.snap index 73bd2f40c0a..6f17548ff41 100644 --- a/src/browser/modules/Stream/CypherFrame/VisualizationView/__snapshots__/VisualizationView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/VisualizationView/__snapshots__/VisualizationView.test.tsx.snap @@ -1,251 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Visualization renders empty content 1`] = `
`; - -exports[`Visualization renders with result and escapes any HTML 1`] = ` -
-
-
-
- - - - - - - - - - - <b>… - - - - - - -
- - - -
-
-
- -
-
-
-
-
- Overview -
-
-
-
- - Node labels - -
-
    -
    - * (1) -
    -
    - Person (1) -
    -
-
-
- Displaying 1 nodes, 0 relationships. -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`; From baccd81d9cdb4eb69cf46cd90317b8130402f874 Mon Sep 17 00:00:00 2001 From: Oskar Damkjaer Date: Tue, 23 Aug 2022 11:10:27 +0200 Subject: [PATCH 3/3] only run once --- .../GraphVisualizer/Graph/visualization/ForceSimulation.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/ForceSimulation.ts b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/ForceSimulation.ts index 9e005711699..a3db63a44c2 100644 --- a/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/ForceSimulation.ts +++ b/src/neo4j-arc/graph-visualization/GraphVisualizer/Graph/visualization/ForceSimulation.ts @@ -108,7 +108,10 @@ export class ForceSimulation { } } - this.simulation.restart().on('end', onEnd) + this.simulation.restart().on('end', () => { + onEnd() + this.simulation.on('end', null) + }) } restart(): void {