Skip to content

Commit

Permalink
Node Sampling
Browse files Browse the repository at this point in the history
  • Loading branch information
Stukova authored and rokotyan committed Oct 3, 2023
1 parent dc00ad5 commit 214d06b
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,12 @@ export interface GraphConfigInterface<N extends CosmosInputNode, L extends Cosmo
* Default value: undefined
*/
randomSeed?: number | string;
/**
* Node sampling distance in pixels between neighboring nodes when calling the `getSampledNodePositionsMap` method.
* This parameter determines how many nodes will be included in the sample.
* Default value: `150`
*/
nodeSamplingDistance?: number;
}

export class GraphConfig<N extends CosmosInputNode, L extends CosmosInputLink> implements GraphConfigInterface<N, L> {
Expand Down Expand Up @@ -440,6 +446,7 @@ export class GraphConfig<N extends CosmosInputNode, L extends CosmosInputLink> i
public disableZoom = defaultConfigValues.disableZoom

public randomSeed = undefined
public nodeSamplingDistance = defaultConfigValues.nodeSamplingDistance

public init (config: GraphConfigInterface<N, L>): void {
(Object.keys(config) as (keyof GraphConfigInterface<N, L>)[])
Expand Down
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,16 @@ export class Graph<N extends CosmosInputNode, L extends CosmosInputLink> {
return this.points.getTrackedPositions()
}

/**
* For the nodes that are currently visible on the screen, get a sample of node ids with their coordinates.
* The resulting number of nodes will depend on the `nodeSamplingDistance` configuration property,
* and the sampled nodes will be evenly distributed.
* @returns A Map object where keys are the ids of the nodes and values are their corresponding X and Y coordinates in the [number, number] format.
*/
public getSampledNodePositionsMap (): Map<string, [number, number]> {
return this.points.getSampledNodePositionsMap()
}

/**
* Start the simulation.
* @param alpha Value from 0 to 1. The higher the value, the more initial energy the simulation will get.
Expand Down Expand Up @@ -789,6 +799,7 @@ export class Graph<N extends CosmosInputNode, L extends CosmosInputLink> {
this.reglInstance.poll()
this.canvasD3Selection
.call(this.zoomInstance.behavior.transform, this.zoomInstance.eventTransform)
this.points.updateSampledNodesGrid()
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/modules/Points/fill-sampled-nodes.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#ifdef GL_ES
precision highp float;
#endif

varying vec4 rgba;

void main() {
gl_FragColor = rgba;
}
29 changes: 29 additions & 0 deletions src/modules/Points/fill-sampled-nodes.vert
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#ifdef GL_ES
precision highp float;
#endif

uniform sampler2D position;
uniform float pointsTextureSize;
uniform float spaceSize;
uniform vec2 screenSize;
uniform mat3 transform;

attribute vec2 indexes;

varying vec4 rgba;

void main() {
vec4 pointPosition = texture2D(position, (indexes + 0.5) / pointsTextureSize);
vec2 p = 2.0 * pointPosition.rg / spaceSize - 1.0;
p *= spaceSize / screenSize;
vec3 final = transform * vec3(p, 1);

vec2 pointScreenPosition = (final.xy + 1.0) * screenSize / 2.0;
float index = indexes.g * pointsTextureSize + indexes.r;
rgba = vec4(index, 1.0, pointPosition.xy);
float i = (pointScreenPosition.x + 0.5) / screenSize.x;
float j = (pointScreenPosition.y + 0.5) / screenSize.y;
gl_Position = vec4(2.0 * vec2(i, j) - 1.0, 0.0, 1.0);

gl_PointSize = 1.0;
}
70 changes: 70 additions & 0 deletions src/modules/Points/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import drawHighlightedFrag from '@/graph/modules/Points/draw-highlighted.frag'
import drawHighlightedVert from '@/graph/modules/Points/draw-highlighted.vert'
import findHoveredPointFrag from '@/graph/modules/Points/find-hovered-point.frag'
import findHoveredPointVert from '@/graph/modules/Points/find-hovered-point.vert'
import fillGridWithSampledNodesFrag from '@/graph/modules/Points/fill-sampled-nodes.frag'
import fillGridWithSampledNodesVert from '@/graph/modules/Points/fill-sampled-nodes.vert'
import { createSizeBuffer, getNodeSize } from '@/graph/modules/Points/size-buffer'
import updatePositionFrag from '@/graph/modules/Points/update-position.frag'
import { createIndexesBuffer, createQuadBuffer, destroyFramebuffer } from '@/graph/modules/Shared/buffer'
Expand All @@ -29,12 +31,15 @@ export class Points<N extends CosmosInputNode, L extends CosmosInputLink> extend
public sizeFbo: regl.Framebuffer2D | undefined
public trackedIndicesFbo: regl.Framebuffer2D | undefined
public trackedPositionsFbo: regl.Framebuffer2D | undefined
public sampledNodesFbo: regl.Framebuffer2D | undefined
private drawCommand: regl.DrawCommand | undefined
private drawHighlightedCommand: regl.DrawCommand | undefined
private updatePositionCommand: regl.DrawCommand | undefined
private findPointsOnAreaSelectionCommand: regl.DrawCommand | undefined
private findHoveredPointCommand: regl.DrawCommand | undefined
private clearHoveredFboCommand: regl.DrawCommand | undefined
private clearSampledNodesFboCommand: regl.DrawCommand | undefined
private fillSampledNodesFboCommand: regl.DrawCommand | undefined
private trackPointsCommand: regl.DrawCommand | undefined
private trackedIds: string[] | undefined
private trackedPositionsById: Map<string, [number, number]> = new Map()
Expand Down Expand Up @@ -109,6 +114,7 @@ export class Points<N extends CosmosInputNode, L extends CosmosInputLink> extend
this.updateSize()
this.updateColor()
this.updateGreyoutStatus()
this.updateSampledNodesGrid()
}

public initPrograms (): void {
Expand Down Expand Up @@ -222,6 +228,33 @@ export class Points<N extends CosmosInputNode, L extends CosmosInputLink> extend
mask: false,
},
})
this.clearSampledNodesFboCommand = reglInstance({
frag: clearFrag,
vert: updateVert,
framebuffer: () => this.sampledNodesFbo as regl.Framebuffer2D,
primitive: 'triangle strip',
count: 4,
attributes: { quad: createQuadBuffer(reglInstance) },
})
this.fillSampledNodesFboCommand = reglInstance({
frag: fillGridWithSampledNodesFrag,
vert: fillGridWithSampledNodesVert,
primitive: 'points',
count: () => data.nodes.length,
framebuffer: () => this.sampledNodesFbo as regl.Framebuffer2D,
attributes: { indexes: createIndexesBuffer(reglInstance, store.pointsTextureSize) },
uniforms: {
position: () => this.currentPositionFbo,
pointsTextureSize: () => store.pointsTextureSize,
transform: () => store.transform,
spaceSize: () => store.adjustedSpaceSize,
screenSize: () => store.screenSize,
},
depth: {
enable: false,
mask: false,
},
})
this.drawHighlightedCommand = reglInstance({
frag: drawHighlightedFrag,
vert: drawHighlightedVert,
Expand Down Expand Up @@ -295,6 +328,20 @@ export class Points<N extends CosmosInputNode, L extends CosmosInputLink> extend
this.sizeFbo = createSizeBuffer(data, reglInstance, pointsTextureSize, config.nodeSize)
}

public updateSampledNodesGrid (): void {
const { store: { screenSize }, config: { nodeSamplingDistance }, reglInstance } = this
const dist = nodeSamplingDistance ?? Math.min(...screenSize) / 2
const w = Math.ceil(screenSize[0] / dist)
const h = Math.ceil(screenSize[1] / dist)
destroyFramebuffer(this.sampledNodesFbo)
this.sampledNodesFbo = reglInstance.framebuffer({
shape: [w, h],
depth: false,
stencil: false,
colorType: 'float',
})
}

public trackPoints (): void {
if (!this.trackedIndicesFbo || !this.trackedPositionsFbo) return
this.trackPointsCommand?.()
Expand Down Expand Up @@ -364,6 +411,29 @@ export class Points<N extends CosmosInputNode, L extends CosmosInputLink> extend
return this.trackedPositionsById
}

public getSampledNodePositionsMap (): Map<string, [number, number]> {
const positions = new Map<string, [number, number]>()
if (!this.sampledNodesFbo) return positions
this.clearSampledNodesFboCommand?.()
this.fillSampledNodesFboCommand?.()
const pixels = readPixels(this.reglInstance, this.sampledNodesFbo as regl.Framebuffer2D)
for (let i = 0; i < pixels.length / 4; i++) {
const index = pixels[i * 4]
const isNotEmpty = !!pixels[i * 4 + 1]
const x = pixels[i * 4 + 2]
const y = pixels[i * 4 + 3]

if (isNotEmpty && index !== undefined && x !== undefined && y !== undefined) {
const inputIndex = this.data.getInputIndexBySortedIndex(index)
if (inputIndex !== undefined) {
const id = this.data.getNodeByIndex(inputIndex)?.id
if (id !== undefined) positions.set(id, [x, y])
}
}
}
return positions
}

public destroy (): void {
destroyFramebuffer(this.currentPositionFbo)
destroyFramebuffer(this.previousPositionFbo)
Expand Down
1 change: 1 addition & 0 deletions src/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const defaultConfigValues = {
scaleNodesOnZoom: true,
initialZoomLevel: 1,
disableZoom: false,
nodeSamplingDistance: 150,
}

export const hoveredNodeRingOpacity = 0.7
Expand Down

0 comments on commit 214d06b

Please sign in to comment.