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/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/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`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Overview
-
-
-
-
-
- Node labels
-
-
-
-
- * (1)
-
-
- Person (1)
-
-
-
-
- Displaying 1 nodes, 0 relationships.
-
-
-
-
-
-
-
-
-
-`;
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..a3db63a44c2 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,29 @@ 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()
+ this.simulation.on('end', null)
+ })
}
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