@cosmos.gl/graph 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/SECURITY.md +7 -1
- package/dist/config.d.ts +4 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1098 -1047
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +28 -16
- package/dist/index.min.js.map +1 -1
- package/dist/modules/GraphData/index.d.ts +1 -0
- package/dist/modules/Points/index.d.ts +3 -0
- package/dist/stories/beginners/pinned-points/data-gen.d.ts +5 -0
- package/dist/stories/beginners/pinned-points/index.d.ts +5 -0
- package/dist/stories/beginners.stories.d.ts +1 -0
- package/package.json +1 -1
- package/src/config.ts +9 -1
- package/src/index.ts +28 -1
- package/src/modules/GraphData/index.ts +2 -1
- package/src/modules/Points/index.ts +34 -0
- package/src/modules/Points/update-position.frag +12 -0
- package/src/stories/1. welcome.mdx +9 -3
- package/src/stories/2. configuration.mdx +3 -2
- package/src/stories/beginners/basic-set-up/index.ts +1 -1
- package/src/stories/beginners/pinned-points/data-gen.ts +153 -0
- package/src/stories/beginners/pinned-points/index.ts +61 -0
- package/src/stories/beginners/quick-start.ts +1 -1
- package/src/stories/beginners/remove-points/config.ts +1 -1
- package/src/stories/beginners.stories.ts +14 -0
- package/src/stories/create-cosmos.ts +1 -1
- package/src/stories/geospatial/moscow-metro-stations/index.ts +1 -1
|
@@ -24,6 +24,7 @@ export declare class GraphData {
|
|
|
24
24
|
inputPointClusters: (number | undefined)[] | undefined;
|
|
25
25
|
inputClusterPositions: (number | undefined)[] | undefined;
|
|
26
26
|
inputClusterStrength: Float32Array | undefined;
|
|
27
|
+
inputPinnedPoints: number[] | undefined;
|
|
27
28
|
pointPositions: Float32Array | undefined;
|
|
28
29
|
pointColors: Float32Array | undefined;
|
|
29
30
|
pointSizes: Float32Array | undefined;
|
|
@@ -39,6 +39,8 @@ export declare class Points extends CoreModule {
|
|
|
39
39
|
private trackedIndices;
|
|
40
40
|
private selectedTexture;
|
|
41
41
|
private greyoutStatusTexture;
|
|
42
|
+
private pinnedStatusTexture;
|
|
43
|
+
private pinnedStatusFbo;
|
|
42
44
|
private sizeTexture;
|
|
43
45
|
private trackedIndicesTexture;
|
|
44
46
|
private polygonPathTexture;
|
|
@@ -51,6 +53,7 @@ export declare class Points extends CoreModule {
|
|
|
51
53
|
initPrograms(): void;
|
|
52
54
|
updateColor(): void;
|
|
53
55
|
updateGreyoutStatus(): void;
|
|
56
|
+
updatePinnedStatus(): void;
|
|
54
57
|
updateSize(): void;
|
|
55
58
|
updateShape(): void;
|
|
56
59
|
updateImageIndices(): void;
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -44,6 +44,9 @@ export interface GraphConfigInterface {
|
|
|
44
44
|
* in the format `[red, green, blue, alpha]` where each value is a number between 0 and 255.
|
|
45
45
|
* Default value: '#b3b3b3'
|
|
46
46
|
*/
|
|
47
|
+
pointDefaultColor?: string | [number, number, number, number];
|
|
48
|
+
|
|
49
|
+
/** @deprecated Use `pointDefaultColor` instead */
|
|
47
50
|
pointColor?: string | [number, number, number, number];
|
|
48
51
|
|
|
49
52
|
/**
|
|
@@ -51,7 +54,7 @@ export interface GraphConfigInterface {
|
|
|
51
54
|
* This can be either a hex color string (e.g., '#b3b3b3') or an array of RGBA values
|
|
52
55
|
* in the format `[red, green, blue, alpha]` where each value is a number between 0 and 255.
|
|
53
56
|
*
|
|
54
|
-
* If not provided, the color will be the same as the
|
|
57
|
+
* If not provided, the color will be the same as the point's original color,
|
|
55
58
|
* but darkened or lightened depending on the background color.
|
|
56
59
|
*
|
|
57
60
|
* If `pointGreyoutOpacity` is also defined, it will override the alpha/opacity component
|
|
@@ -615,6 +618,11 @@ export class GraphConfig implements GraphConfigInterface {
|
|
|
615
618
|
public backgroundColor = defaultBackgroundColor
|
|
616
619
|
public spaceSize = defaultConfigValues.spaceSize
|
|
617
620
|
public pointColor = defaultPointColor
|
|
621
|
+
// TODO: When pointColor is removed, change this to:
|
|
622
|
+
// public pointDefaultColor = defaultPointColor
|
|
623
|
+
// Currently undefined to allow fallback to deprecated pointColor via nullish coalescing
|
|
624
|
+
// in GraphData.updatePointColor() (see: this._config.pointDefaultColor ?? this._config.pointColor)
|
|
625
|
+
public pointDefaultColor = undefined
|
|
618
626
|
public pointGreyoutOpacity = defaultGreyoutPointOpacity
|
|
619
627
|
public pointGreyoutColor = defaultGreyoutPointColor
|
|
620
628
|
public pointSize = defaultPointSize
|
package/src/index.ts
CHANGED
|
@@ -247,7 +247,8 @@ export class Graph {
|
|
|
247
247
|
if (this._isDestroyed || !this.reglInstance || !this.points || !this.lines || !this.clusters) return
|
|
248
248
|
const prevConfig = { ...this.config }
|
|
249
249
|
this.config.init(config)
|
|
250
|
-
if (prevConfig.
|
|
250
|
+
if ((prevConfig.pointDefaultColor !== this.config.pointDefaultColor) ||
|
|
251
|
+
(prevConfig.pointColor !== this.config.pointColor)) {
|
|
251
252
|
this.graph.updatePointColor()
|
|
252
253
|
this.points.updateColor()
|
|
253
254
|
}
|
|
@@ -340,6 +341,7 @@ export class Graph {
|
|
|
340
341
|
this.isPointSizeUpdateNeeded = true
|
|
341
342
|
this.isPointShapeUpdateNeeded = true
|
|
342
343
|
this.isPointImageIndicesUpdateNeeded = true
|
|
344
|
+
this.isPointImageSizesUpdateNeeded = true
|
|
343
345
|
this.isPointClusterUpdateNeeded = true
|
|
344
346
|
this.isForceManyBodyUpdateNeeded = true
|
|
345
347
|
this.isForceLinkUpdateNeeded = true
|
|
@@ -592,6 +594,29 @@ export class Graph {
|
|
|
592
594
|
this.isPointClusterUpdateNeeded = true
|
|
593
595
|
}
|
|
594
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Sets which points are pinned (fixed) in position.
|
|
599
|
+
*
|
|
600
|
+
* Pinned points:
|
|
601
|
+
* - Do not move due to physics forces (gravity, repulsion, link forces, etc.)
|
|
602
|
+
* - Still participate in force calculations (other nodes are attracted to/repelled by them)
|
|
603
|
+
* - Can still be dragged by the user if `enableDrag` is true
|
|
604
|
+
*
|
|
605
|
+
* @param {number[] | null} pinnedIndices - Array of point indices to pin. Set to `[]` or `null` to unpin all points.
|
|
606
|
+
* @example
|
|
607
|
+
* // Pin points 0 and 5
|
|
608
|
+
* graph.setPinnedPoints([0, 5])
|
|
609
|
+
*
|
|
610
|
+
* // Unpin all points
|
|
611
|
+
* graph.setPinnedPoints([])
|
|
612
|
+
* graph.setPinnedPoints(null)
|
|
613
|
+
*/
|
|
614
|
+
public setPinnedPoints (pinnedIndices: number[] | null): void {
|
|
615
|
+
if (this._isDestroyed || !this.points) return
|
|
616
|
+
this.graph.inputPinnedPoints = pinnedIndices && pinnedIndices.length > 0 ? pinnedIndices : undefined
|
|
617
|
+
this.points.updatePinnedStatus()
|
|
618
|
+
}
|
|
619
|
+
|
|
595
620
|
/**
|
|
596
621
|
* Renders the graph.
|
|
597
622
|
*
|
|
@@ -1361,6 +1386,8 @@ export class Graph {
|
|
|
1361
1386
|
// To prevent the dragged point from suddenly jumping, run the drag function twice
|
|
1362
1387
|
this.points?.drag()
|
|
1363
1388
|
this.points?.drag()
|
|
1389
|
+
// Update tracked positions after drag, even when simulation is disabled
|
|
1390
|
+
this.points?.trackPoints()
|
|
1364
1391
|
}
|
|
1365
1392
|
this.fpsMonitor?.end(now)
|
|
1366
1393
|
|
|
@@ -27,6 +27,7 @@ export class GraphData {
|
|
|
27
27
|
public inputPointClusters: (number | undefined)[] | undefined
|
|
28
28
|
public inputClusterPositions: (number | undefined)[] | undefined
|
|
29
29
|
public inputClusterStrength: Float32Array | undefined
|
|
30
|
+
public inputPinnedPoints: number[] | undefined
|
|
30
31
|
|
|
31
32
|
public pointPositions: Float32Array | undefined
|
|
32
33
|
public pointColors: Float32Array | undefined
|
|
@@ -87,7 +88,7 @@ export class GraphData {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
// Sets point colors to default values from config if the input is missing or does not match input points number.
|
|
90
|
-
const defaultRgba = getRgbaColor(this._config.pointColor)
|
|
91
|
+
const defaultRgba = getRgbaColor(this._config.pointDefaultColor ?? this._config.pointColor)
|
|
91
92
|
if (this.inputPointColors === undefined || this.inputPointColors.length / 4 !== this.pointsNumber) {
|
|
92
93
|
this.pointColors = new Float32Array(this.pointsNumber * 4)
|
|
93
94
|
for (let i = 0; i < this.pointColors.length / 4; i++) {
|
|
@@ -61,6 +61,8 @@ export class Points extends CoreModule {
|
|
|
61
61
|
private trackedIndices: number[] | undefined
|
|
62
62
|
private selectedTexture: regl.Texture2D | undefined
|
|
63
63
|
private greyoutStatusTexture: regl.Texture2D | undefined
|
|
64
|
+
private pinnedStatusTexture: regl.Texture2D | undefined
|
|
65
|
+
private pinnedStatusFbo: regl.Framebuffer2D | undefined
|
|
64
66
|
private sizeTexture: regl.Texture2D | undefined
|
|
65
67
|
private trackedIndicesTexture: regl.Texture2D | undefined
|
|
66
68
|
private polygonPathTexture: regl.Texture2D | undefined
|
|
@@ -172,6 +174,7 @@ export class Points extends CoreModule {
|
|
|
172
174
|
this.sampledPointIndices(createIndexesForBuffer(store.pointsTextureSize))
|
|
173
175
|
|
|
174
176
|
this.updateGreyoutStatus()
|
|
177
|
+
this.updatePinnedStatus()
|
|
175
178
|
this.updateSampledPointsGrid()
|
|
176
179
|
|
|
177
180
|
this.trackPointsByIndices()
|
|
@@ -193,6 +196,7 @@ export class Points extends CoreModule {
|
|
|
193
196
|
velocity: () => this.velocityFbo,
|
|
194
197
|
friction: () => config.simulationFriction,
|
|
195
198
|
spaceSize: () => store.adjustedSpaceSize,
|
|
199
|
+
pinnedStatusTexture: () => this.pinnedStatusFbo,
|
|
196
200
|
},
|
|
197
201
|
})
|
|
198
202
|
}
|
|
@@ -520,6 +524,36 @@ export class Points extends CoreModule {
|
|
|
520
524
|
})
|
|
521
525
|
}
|
|
522
526
|
|
|
527
|
+
public updatePinnedStatus (): void {
|
|
528
|
+
const { reglInstance, store: { pointsTextureSize }, data } = this
|
|
529
|
+
if (!pointsTextureSize) return
|
|
530
|
+
|
|
531
|
+
// Pinned status: 0 - not pinned, 1 - pinned
|
|
532
|
+
const initialState = new Float32Array(pointsTextureSize * pointsTextureSize * 4).fill(0)
|
|
533
|
+
|
|
534
|
+
if (data.inputPinnedPoints && data.pointsNumber !== undefined) {
|
|
535
|
+
for (const pinnedIndex of data.inputPinnedPoints) {
|
|
536
|
+
if (pinnedIndex >= 0 && pinnedIndex < data.pointsNumber) {
|
|
537
|
+
initialState[pinnedIndex * 4] = 1
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!this.pinnedStatusTexture) this.pinnedStatusTexture = reglInstance.texture()
|
|
543
|
+
this.pinnedStatusTexture({
|
|
544
|
+
data: initialState,
|
|
545
|
+
width: pointsTextureSize,
|
|
546
|
+
height: pointsTextureSize,
|
|
547
|
+
type: 'float',
|
|
548
|
+
})
|
|
549
|
+
if (!this.pinnedStatusFbo) this.pinnedStatusFbo = reglInstance.framebuffer()
|
|
550
|
+
this.pinnedStatusFbo({
|
|
551
|
+
color: this.pinnedStatusTexture,
|
|
552
|
+
depth: false,
|
|
553
|
+
stencil: false,
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
|
|
523
557
|
public updateSize (): void {
|
|
524
558
|
const { reglInstance, store: { pointsTextureSize }, data } = this
|
|
525
559
|
if (!pointsTextureSize || data.pointsNumber === undefined || data.pointSizes === undefined) return
|
|
@@ -4,6 +4,7 @@ precision highp float;
|
|
|
4
4
|
|
|
5
5
|
uniform sampler2D positionsTexture;
|
|
6
6
|
uniform sampler2D velocity;
|
|
7
|
+
uniform sampler2D pinnedStatusTexture;
|
|
7
8
|
uniform float friction;
|
|
8
9
|
uniform float spaceSize;
|
|
9
10
|
|
|
@@ -13,6 +14,17 @@ void main() {
|
|
|
13
14
|
vec4 pointPosition = texture2D(positionsTexture, textureCoords);
|
|
14
15
|
vec4 pointVelocity = texture2D(velocity, textureCoords);
|
|
15
16
|
|
|
17
|
+
// Check if point is pinned
|
|
18
|
+
// pinnedStatusTexture has the same size and layout as positionsTexture
|
|
19
|
+
// Each pixel corresponds to a point: red channel > 0.5 means the point is pinned
|
|
20
|
+
vec4 pinnedStatus = texture2D(pinnedStatusTexture, textureCoords);
|
|
21
|
+
|
|
22
|
+
// If pinned, don't update position
|
|
23
|
+
if (pinnedStatus.r > 0.5) {
|
|
24
|
+
gl_FragColor = pointPosition;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
// Friction
|
|
17
29
|
pointVelocity.rg *= friction;
|
|
18
30
|
|
|
@@ -2,13 +2,19 @@ import { Meta } from "@storybook/blocks";
|
|
|
2
2
|
|
|
3
3
|
<Meta title="Welcome to cosmos.gl" />
|
|
4
4
|
|
|
5
|
-
<
|
|
5
|
+
<div style={{ fontSize: '1.0rem', float: 'right' }}>
|
|
6
|
+
<a href="https://github.com/cosmosgl/graph" target="_blank" rel="noopener noreferrer" style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', color: '#fff' }}>
|
|
7
|
+
<svg height="30" viewBox="0 0 16 16" width="30" aria-hidden="true">
|
|
8
|
+
<path fill="currentColor" d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
|
|
9
|
+
</svg>
|
|
10
|
+
</a>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<p style={{ fontSize: '1.75rem', lineHeight: '1.25em', marginTop: 0 }}>Welcome to <b>cosmos.gl</b> – a high-performance WebGL library for visualizing network graphs and machine learning embeddings.</p>
|
|
6
14
|
|
|
7
15
|
<video style={{ width: '100%' }} src="https://user-images.githubusercontent.com/755708/173392407-9b05cbb6-d39e-4c2c-ab41-50900cfda823.mp4" loop autoPlay muted playsInline>
|
|
8
16
|
</video>
|
|
9
17
|
|
|
10
|
-
<p style={{ fontSize: '1.0rem' }}>Here you can find documentaion and examples of how to use cosmos.gl</p>
|
|
11
|
-
|
|
12
18
|
---
|
|
13
19
|
|
|
14
20
|
### Quick Start
|
|
@@ -9,9 +9,10 @@ import { Meta } from "@storybook/blocks";
|
|
|
9
9
|
| enableSimulation | If set to `false`, the simulation will not run. This property will be applied only on component initialization and it can't be changed using the `setConfig` method | `true` |
|
|
10
10
|
| backgroundColor | Canvas background color | `#222222` |
|
|
11
11
|
| spaceSize | Simulation space size (max 8192) | `8192` |
|
|
12
|
-
|
|
|
12
|
+
| pointDefaultColor | The default color to use for points when no point colors are provided, or if the color value in the array is `undefined` or `null`. This can be either a hex color string (e.g., '#b3b3b3') or an array of RGBA values in the format `[red, green, blue, alpha]` where each value is a number between 0 and 255 | `#b3b3b3` |
|
|
13
|
+
| pointColor | **[DEPRECATED]** Use `pointDefaultColor` instead. The default color to use for points when no point colors are provided... | `#b3b3b3` |
|
|
13
14
|
| pointGreyoutOpacity | Greyed out point opacity value when the selection is active | `undefined` |
|
|
14
|
-
| pointGreyoutColor |
|
|
15
|
+
| pointGreyoutColor | The color to use for points when they are greyed out (when selection is active). This can be either a hex color string (e.g., '#b3b3b3') or an array of RGBA values in the format `[red, green, blue, alpha]` where each value is a number between 0 and 255. If not provided, the color will be the same as the point's original color, but darkened or lightened depending on the background color. If `pointGreyoutOpacity` is also defined, it will override the alpha/opacity component of this color. | `undefined` |
|
|
15
16
|
| pointSize | The default size value to use for points when no point sizes are provided or if the size value in the array is `undefined` or `null` | `4` |
|
|
16
17
|
| pointOpacity | Universal opacity value applied to all points. This value multiplies with individual point alpha values (if set via setPointColors). Useful for dynamically controlling opacity of all points without updating individual RGBA arrays. | `1.0` |
|
|
17
18
|
| pointSizeScale | Scale factor for the point size | `1` |
|
|
@@ -23,7 +23,7 @@ export const basicSetUp = (): { graph: Graph; div: HTMLDivElement} => {
|
|
|
23
23
|
spaceSize: 4096,
|
|
24
24
|
backgroundColor: '#2d313a',
|
|
25
25
|
pointSize: 4,
|
|
26
|
-
|
|
26
|
+
pointDefaultColor: '#4B5BBF',
|
|
27
27
|
linkWidth: 0.6,
|
|
28
28
|
scalePointsOnZoom: true,
|
|
29
29
|
linkColor: '#5F74C2',
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Note: This is vibe coding only - quick prototype code for demonstration purposes
|
|
2
|
+
|
|
3
|
+
function getRandom (min: number, max: number): number {
|
|
4
|
+
return Math.random() * (max - min) + min
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function hslToRgb (hue: number, saturation: number, lightness: number): [number, number, number] {
|
|
8
|
+
const c = (1 - Math.abs(2 * lightness - 1)) * saturation
|
|
9
|
+
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1))
|
|
10
|
+
const m = lightness - c / 2
|
|
11
|
+
|
|
12
|
+
let r, g, b
|
|
13
|
+
if (hue >= 0 && hue < 60) {
|
|
14
|
+
r = c; g = x; b = 0
|
|
15
|
+
} else if (hue >= 60 && hue < 120) {
|
|
16
|
+
r = x; g = c; b = 0
|
|
17
|
+
} else if (hue >= 120 && hue < 180) {
|
|
18
|
+
r = 0; g = c; b = x
|
|
19
|
+
} else if (hue >= 180 && hue < 240) {
|
|
20
|
+
r = 0; g = x; b = c
|
|
21
|
+
} else if (hue >= 240 && hue < 300) {
|
|
22
|
+
r = x; g = 0; b = c
|
|
23
|
+
} else {
|
|
24
|
+
r = c; g = 0; b = x
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [r + m, g + m, b + m]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function generateData (numNodes = 60): { pointPositions: Float32Array; links: Float32Array; pointColors: Float32Array } {
|
|
31
|
+
const pointPositions = new Float32Array(numNodes * 2)
|
|
32
|
+
const pointColors = new Float32Array(numNodes * 4)
|
|
33
|
+
const linksArray: number[] = []
|
|
34
|
+
|
|
35
|
+
const centerX = 2048
|
|
36
|
+
const centerY = 2048
|
|
37
|
+
const circleRadius = 900
|
|
38
|
+
|
|
39
|
+
// First, place 6 nodes in a perfect circle with equal spacing
|
|
40
|
+
const numCircleNodes = 6
|
|
41
|
+
for (let i = 0; i < numCircleNodes; i++) {
|
|
42
|
+
const angle = (i / numCircleNodes) * Math.PI * 2
|
|
43
|
+
const x = centerX + Math.cos(angle) * circleRadius
|
|
44
|
+
const y = centerY + Math.sin(angle) * circleRadius
|
|
45
|
+
|
|
46
|
+
pointPositions[i * 2] = x
|
|
47
|
+
pointPositions[i * 2 + 1] = y
|
|
48
|
+
|
|
49
|
+
// Color based on position - rainbow gradient
|
|
50
|
+
const hue = (i / numNodes) * 360
|
|
51
|
+
const [r, g, b] = hslToRgb(hue, 0.7, 0.6)
|
|
52
|
+
|
|
53
|
+
pointColors[i * 4] = r
|
|
54
|
+
pointColors[i * 4 + 1] = g
|
|
55
|
+
pointColors[i * 4 + 2] = b
|
|
56
|
+
pointColors[i * 4 + 3] = 1.0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Create remaining nodes in clusters around the space
|
|
60
|
+
const numClusters = 4
|
|
61
|
+
const remainingNodes = numNodes - numCircleNodes
|
|
62
|
+
const nodesPerCluster = Math.floor(remainingNodes / numClusters)
|
|
63
|
+
|
|
64
|
+
for (let cluster = 0; cluster < numClusters; cluster++) {
|
|
65
|
+
const clusterAngle = (cluster / numClusters) * Math.PI * 2
|
|
66
|
+
const clusterRadius = 1200
|
|
67
|
+
const clusterX = centerX + Math.cos(clusterAngle) * clusterRadius
|
|
68
|
+
const clusterY = centerY + Math.sin(clusterAngle) * clusterRadius
|
|
69
|
+
|
|
70
|
+
const startIndex = numCircleNodes + cluster * nodesPerCluster
|
|
71
|
+
const endIndex = cluster === numClusters - 1 ? numNodes : startIndex + nodesPerCluster
|
|
72
|
+
|
|
73
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
74
|
+
// Position nodes in a small cluster
|
|
75
|
+
const angle = (i - startIndex) / (endIndex - startIndex) * Math.PI * 2
|
|
76
|
+
const radius = 300 + getRandom(-50, 50)
|
|
77
|
+
const x = clusterX + Math.cos(angle) * radius * getRandom(0.7, 1.3)
|
|
78
|
+
const y = clusterY + Math.sin(angle) * radius * getRandom(0.7, 1.3)
|
|
79
|
+
|
|
80
|
+
pointPositions[i * 2] = x
|
|
81
|
+
pointPositions[i * 2 + 1] = y
|
|
82
|
+
|
|
83
|
+
// Color based on position - rainbow gradient
|
|
84
|
+
const hue = (i / numNodes) * 360
|
|
85
|
+
const [r, g, b] = hslToRgb(hue, 0.7, 0.6)
|
|
86
|
+
|
|
87
|
+
pointColors[i * 4] = r
|
|
88
|
+
pointColors[i * 4 + 1] = g
|
|
89
|
+
pointColors[i * 4 + 2] = b
|
|
90
|
+
pointColors[i * 4 + 3] = 1.0
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create links: connect the 6 circle nodes to form a ring
|
|
95
|
+
for (let i = 0; i < numCircleNodes; i++) {
|
|
96
|
+
const nextIndex = (i + 1) % numCircleNodes
|
|
97
|
+
linksArray.push(i)
|
|
98
|
+
linksArray.push(nextIndex)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Connect circle nodes to nearby cluster nodes - more connections
|
|
102
|
+
for (let i = 0; i < numCircleNodes; i++) {
|
|
103
|
+
const circleAngle = (i / numCircleNodes) * Math.PI * 2
|
|
104
|
+
// Find nearest cluster and connect to many nodes in it
|
|
105
|
+
const nearestCluster = Math.floor((circleAngle / (Math.PI * 2)) * numClusters) % numClusters
|
|
106
|
+
const clusterStart = numCircleNodes + nearestCluster * nodesPerCluster
|
|
107
|
+
const clusterEnd = nearestCluster === numClusters - 1 ? numNodes : clusterStart + nodesPerCluster
|
|
108
|
+
// Connect to many nodes in the nearest cluster
|
|
109
|
+
for (let j = clusterStart; j < Math.min(clusterStart + Math.floor(nodesPerCluster * 0.6), clusterEnd); j++) {
|
|
110
|
+
linksArray.push(i)
|
|
111
|
+
linksArray.push(j)
|
|
112
|
+
}
|
|
113
|
+
// Also connect to some nodes in adjacent clusters
|
|
114
|
+
const nextCluster = (nearestCluster + 1) % numClusters
|
|
115
|
+
const nextClusterStart = numCircleNodes + nextCluster * nodesPerCluster
|
|
116
|
+
const nextClusterEnd = nextCluster === numClusters - 1 ? numNodes : nextClusterStart + nodesPerCluster
|
|
117
|
+
for (let j = nextClusterStart; j < Math.min(nextClusterStart + Math.floor(nodesPerCluster * 0.3), nextClusterEnd); j++) {
|
|
118
|
+
linksArray.push(i)
|
|
119
|
+
linksArray.push(j)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Connect nodes within clusters and some cross-cluster links
|
|
124
|
+
for (let i = numCircleNodes; i < numNodes; i++) {
|
|
125
|
+
const cluster = Math.floor((i - numCircleNodes) / nodesPerCluster)
|
|
126
|
+
const clusterStart = numCircleNodes + cluster * nodesPerCluster
|
|
127
|
+
const clusterEnd = cluster === numClusters - 1 ? numNodes : clusterStart + nodesPerCluster
|
|
128
|
+
|
|
129
|
+
// Connect to nearby nodes in the same cluster
|
|
130
|
+
for (let j = clusterStart; j < clusterEnd; j++) {
|
|
131
|
+
if (i !== j && Math.abs(i - j) <= 3) {
|
|
132
|
+
linksArray.push(i)
|
|
133
|
+
linksArray.push(j)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Connect to nodes in adjacent clusters (sparse connections)
|
|
138
|
+
if (i % 3 === 0) {
|
|
139
|
+
const nextCluster = (cluster + 1) % numClusters
|
|
140
|
+
const nextClusterStart = numCircleNodes + nextCluster * nodesPerCluster
|
|
141
|
+
const nextClusterEnd = nextCluster === numClusters - 1 ? numNodes : nextClusterStart + nodesPerCluster
|
|
142
|
+
const targetIndex = nextClusterStart + Math.floor(((i - clusterStart) % nodesPerCluster) * (nextClusterEnd - nextClusterStart) / nodesPerCluster)
|
|
143
|
+
if (targetIndex < numNodes && targetIndex >= numCircleNodes) {
|
|
144
|
+
linksArray.push(i)
|
|
145
|
+
linksArray.push(targetIndex)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const links = new Float32Array(linksArray)
|
|
151
|
+
|
|
152
|
+
return { pointPositions, links, pointColors }
|
|
153
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Graph } from '@cosmos.gl/graph'
|
|
2
|
+
import { generateData } from './data-gen'
|
|
3
|
+
|
|
4
|
+
export const pinnedPoints = (): { graph: Graph; div: HTMLDivElement} => {
|
|
5
|
+
const div = document.createElement('div')
|
|
6
|
+
div.style.height = '100vh'
|
|
7
|
+
div.style.width = '100%'
|
|
8
|
+
div.style.position = 'relative'
|
|
9
|
+
|
|
10
|
+
const infoPanel = document.createElement('div')
|
|
11
|
+
infoPanel.textContent = 'White points are pinned. Try to move them.'
|
|
12
|
+
Object.assign(infoPanel.style, {
|
|
13
|
+
position: 'absolute',
|
|
14
|
+
top: '20px',
|
|
15
|
+
left: '20px',
|
|
16
|
+
color: 'white',
|
|
17
|
+
fontSize: '14px',
|
|
18
|
+
})
|
|
19
|
+
div.appendChild(infoPanel)
|
|
20
|
+
|
|
21
|
+
const graph = new Graph(div, {
|
|
22
|
+
spaceSize: 4096,
|
|
23
|
+
backgroundColor: '#2d313a',
|
|
24
|
+
curvedLinks: true,
|
|
25
|
+
enableDrag: true,
|
|
26
|
+
simulationLinkSpring: 3.1,
|
|
27
|
+
simulationRepulsion: 150,
|
|
28
|
+
simulationGravity: 0.05,
|
|
29
|
+
simulationDecay: 10000000,
|
|
30
|
+
attribution: 'visualized with <a href="https://cosmograph.app/" style="color: var(--cosmosgl-attribution-color);" target="_blank">Cosmograph</a>',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const { pointPositions, links, pointColors } = generateData(100)
|
|
34
|
+
|
|
35
|
+
const pinnedIndices = [0, 1, 2, 3, 4, 5]
|
|
36
|
+
const numPoints = pointPositions.length / 2
|
|
37
|
+
|
|
38
|
+
const colors = new Float32Array(pointColors)
|
|
39
|
+
for (const pinnedIndex of pinnedIndices) {
|
|
40
|
+
colors[pinnedIndex * 4] = 1.0
|
|
41
|
+
colors[pinnedIndex * 4 + 1] = 1.0
|
|
42
|
+
colors[pinnedIndex * 4 + 2] = 1.0
|
|
43
|
+
colors[pinnedIndex * 4 + 3] = 1.0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const pointSizes = new Float32Array(numPoints).fill(12)
|
|
47
|
+
for (const pinnedIndex of pinnedIndices) {
|
|
48
|
+
pointSizes[pinnedIndex] = 30
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
graph.setPointPositions(pointPositions)
|
|
52
|
+
graph.setPointColors(colors)
|
|
53
|
+
graph.setPointSizes(pointSizes)
|
|
54
|
+
graph.setLinks(links)
|
|
55
|
+
graph.setPinnedPoints(pinnedIndices)
|
|
56
|
+
|
|
57
|
+
graph.zoom(0.8)
|
|
58
|
+
graph.render()
|
|
59
|
+
|
|
60
|
+
return { div, graph }
|
|
61
|
+
}
|
|
@@ -8,7 +8,7 @@ export const quickStart = (): { graph: Graph; div: HTMLDivElement} => {
|
|
|
8
8
|
const config: GraphConfigInterface = {
|
|
9
9
|
spaceSize: 4096,
|
|
10
10
|
backgroundColor: '#2d313a',
|
|
11
|
-
|
|
11
|
+
pointDefaultColor: '#F069B4',
|
|
12
12
|
scalePointsOnZoom: true,
|
|
13
13
|
simulationFriction: 0.1, // keeps the graph inert
|
|
14
14
|
simulationGravity: 0, // disables gravity
|
|
@@ -7,6 +7,7 @@ import { basicSetUp } from './beginners/basic-set-up'
|
|
|
7
7
|
import { pointLabels } from './beginners/point-labels'
|
|
8
8
|
import { removePoints } from './beginners/remove-points'
|
|
9
9
|
import { linkHovering } from './beginners/link-hovering'
|
|
10
|
+
import { pinnedPoints } from './beginners/pinned-points'
|
|
10
11
|
|
|
11
12
|
import quickStartStoryRaw from './beginners/quick-start?raw'
|
|
12
13
|
import basicSetUpStoryRaw from './beginners/basic-set-up/index?raw'
|
|
@@ -23,6 +24,8 @@ import removePointsStoryDataGenRaw from './beginners/remove-points/data-gen.ts?r
|
|
|
23
24
|
import linkHoveringStoryRaw from './beginners/link-hovering/index?raw'
|
|
24
25
|
import linkHoveringStoryDataGenRaw from './beginners/link-hovering/data-generator.ts?raw'
|
|
25
26
|
import linkHoveringStoryCssRaw from './beginners/link-hovering/style.css?raw'
|
|
27
|
+
import pinnedPointsStoryRaw from './beginners/pinned-points/index?raw'
|
|
28
|
+
import pinnedPointsStoryDataGenRaw from './beginners/pinned-points/data-gen.ts?raw'
|
|
26
29
|
|
|
27
30
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
28
31
|
const meta: Meta<CosmosStoryProps> = {
|
|
@@ -113,5 +116,16 @@ export const LinkHovering: Story = {
|
|
|
113
116
|
},
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
export const PinnedPoints: Story = {
|
|
120
|
+
...createStory(pinnedPoints),
|
|
121
|
+
name: 'Pinned Points',
|
|
122
|
+
parameters: {
|
|
123
|
+
sourceCode: [
|
|
124
|
+
{ name: 'Story', code: pinnedPointsStoryRaw },
|
|
125
|
+
{ name: 'data-gen.ts', code: pinnedPointsStoryDataGenRaw },
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
116
130
|
// eslint-disable-next-line import/no-default-export
|
|
117
131
|
export default meta
|
|
@@ -23,7 +23,7 @@ export const createCosmos = (props: CosmosStoryProps): { div: HTMLDivElement; gr
|
|
|
23
23
|
const config: GraphConfigInterface = {
|
|
24
24
|
backgroundColor: '#2d313a',
|
|
25
25
|
pointSize: 3,
|
|
26
|
-
|
|
26
|
+
pointDefaultColor: '#4B5BBF',
|
|
27
27
|
pointGreyoutOpacity: 0.1,
|
|
28
28
|
scalePointsOnZoom: true,
|
|
29
29
|
linkWidth: 0.8,
|
|
@@ -31,7 +31,7 @@ export const moscowMetroStations = (): {graph: Graph; div: HTMLDivElement} => {
|
|
|
31
31
|
backgroundColor: '#2d313a',
|
|
32
32
|
scalePointsOnZoom: false,
|
|
33
33
|
rescalePositions,
|
|
34
|
-
|
|
34
|
+
pointDefaultColor: '#FEE08B',
|
|
35
35
|
enableSimulation: false,
|
|
36
36
|
enableDrag: false,
|
|
37
37
|
fitViewOnInit: true,
|