@cosmos.gl/graph 2.4.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 +73 -1
- package/dist/index.d.ts +34 -6
- package/dist/index.js +4087 -3837
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +124 -44
- package/dist/index.min.js.map +1 -1
- package/dist/modules/GraphData/index.d.ts +1 -0
- package/dist/modules/Lines/index.d.ts +8 -0
- package/dist/modules/Points/index.d.ts +3 -0
- package/dist/modules/Store/index.d.ts +14 -2
- package/dist/modules/core-module.d.ts +1 -0
- package/dist/stories/beginners/link-hovering/data-generator.d.ts +19 -0
- package/dist/stories/beginners/link-hovering/index.d.ts +5 -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 +2 -0
- package/dist/variables.d.ts +5 -2
- package/package.json +1 -1
- package/src/config.ts +95 -3
- package/src/index.ts +179 -32
- package/src/modules/GraphData/index.ts +2 -1
- package/src/modules/Lines/draw-curve-line.frag +12 -1
- package/src/modules/Lines/draw-curve-line.vert +29 -2
- package/src/modules/Lines/hovered-line-index.frag +27 -0
- package/src/modules/Lines/hovered-line-index.vert +8 -0
- package/src/modules/Lines/index.ts +112 -2
- package/src/modules/Points/index.ts +34 -0
- package/src/modules/Points/update-position.frag +12 -0
- package/src/modules/Store/index.ts +33 -2
- package/src/modules/core-module.ts +1 -0
- package/src/stories/1. welcome.mdx +11 -4
- package/src/stories/2. configuration.mdx +13 -3
- package/src/stories/3. api-reference.mdx +13 -4
- package/src/stories/beginners/basic-set-up/index.ts +21 -11
- package/src/stories/beginners/link-hovering/data-generator.ts +198 -0
- package/src/stories/beginners/link-hovering/index.ts +61 -0
- package/src/stories/beginners/link-hovering/style.css +73 -0
- 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 +3 -2
- package/src/stories/beginners/remove-points/config.ts +1 -1
- package/src/stories/beginners/remove-points/index.ts +28 -30
- package/src/stories/beginners.stories.ts +31 -0
- package/src/stories/clusters/polygon-selection/index.ts +2 -4
- package/src/stories/create-cosmos.ts +1 -1
- package/src/stories/geospatial/moscow-metro-stations/index.ts +1 -1
- package/src/stories/shapes/image-example/index.ts +7 -8
- package/src/variables.ts +5 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
interface Point {
|
|
2
|
+
id: number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
interface Link {
|
|
6
|
+
source: number;
|
|
7
|
+
target: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface NetworkData {
|
|
11
|
+
pointPositions: Float32Array;
|
|
12
|
+
pointColors: Float32Array;
|
|
13
|
+
pointSizes: Float32Array;
|
|
14
|
+
links: Float32Array;
|
|
15
|
+
linkColors: Float32Array;
|
|
16
|
+
linkWidths: Float32Array;
|
|
17
|
+
points: Point[];
|
|
18
|
+
connections: Link[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hslToRgb (hue: number, saturation: number, lightness: number): [number, number, number] {
|
|
22
|
+
const c = (1 - Math.abs(2 * lightness - 1)) * saturation
|
|
23
|
+
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1))
|
|
24
|
+
const m = lightness - c / 2
|
|
25
|
+
|
|
26
|
+
let r, g, b
|
|
27
|
+
if (hue >= 0 && hue < 60) {
|
|
28
|
+
r = c; g = x; b = 0
|
|
29
|
+
} else if (hue >= 60 && hue < 120) {
|
|
30
|
+
r = x; g = c; b = 0
|
|
31
|
+
} else if (hue >= 120 && hue < 180) {
|
|
32
|
+
r = 0; g = c; b = x
|
|
33
|
+
} else if (hue >= 180 && hue < 240) {
|
|
34
|
+
r = 0; g = x; b = c
|
|
35
|
+
} else if (hue >= 240 && hue < 300) {
|
|
36
|
+
r = x; g = 0; b = c
|
|
37
|
+
} else {
|
|
38
|
+
r = c; g = 0; b = x
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return [r + m, g + m, b + m]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function generatePoints (count: number): Point[] {
|
|
45
|
+
const points: Point[] = []
|
|
46
|
+
for (let i = 0; i < count; i++) {
|
|
47
|
+
points.push({ id: i })
|
|
48
|
+
}
|
|
49
|
+
return points
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function generateConnections (points: Point[]): Link[] {
|
|
53
|
+
const connections: Link[] = []
|
|
54
|
+
const pointCount = points.length
|
|
55
|
+
|
|
56
|
+
// Sequential connections
|
|
57
|
+
for (let i = 0; i < pointCount; i++) {
|
|
58
|
+
const nextId1 = (i + 1) % pointCount
|
|
59
|
+
const nextId2 = (i + 2) % pointCount
|
|
60
|
+
connections.push({ source: i, target: nextId1 })
|
|
61
|
+
if (i % 2 === 0) {
|
|
62
|
+
connections.push({ source: i, target: nextId2 })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Hub connections
|
|
67
|
+
const hubPoints = [0, 10, 20, 30, 40, 50, 60, 70]
|
|
68
|
+
hubPoints.forEach(hub => {
|
|
69
|
+
for (let i = 1; i <= 5; i++) {
|
|
70
|
+
const targetId = (hub + i * 3) % pointCount
|
|
71
|
+
if (targetId !== hub) {
|
|
72
|
+
connections.push({ source: hub, target: targetId })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Cross connections
|
|
78
|
+
for (let i = 0; i < pointCount / 2; i++) {
|
|
79
|
+
const oppositeId = i + Math.floor(pointCount / 2)
|
|
80
|
+
if (i % 3 === 0) {
|
|
81
|
+
connections.push({ source: i, target: oppositeId })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Random connections
|
|
86
|
+
for (let i = 0; i < 30; i++) {
|
|
87
|
+
const source = Math.floor(Math.random() * pointCount)
|
|
88
|
+
const target = Math.floor(Math.random() * pointCount)
|
|
89
|
+
if (source !== target) {
|
|
90
|
+
const exists = connections.some(conn =>
|
|
91
|
+
(conn.source === source && conn.target === target) ||
|
|
92
|
+
(conn.source === target && conn.target === source)
|
|
93
|
+
)
|
|
94
|
+
if (!exists) {
|
|
95
|
+
connections.push({ source, target })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return connections
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function generatePointPositions (points: Point[]): Float32Array {
|
|
104
|
+
const radius = 100
|
|
105
|
+
const positions = new Float32Array(points.length * 2)
|
|
106
|
+
|
|
107
|
+
points.forEach((point, i) => {
|
|
108
|
+
const angle = (i / points.length) * Math.PI * 2
|
|
109
|
+
const pointRadius = radius + (Math.random() - 0.5) * 20
|
|
110
|
+
|
|
111
|
+
positions[i * 2] = Math.cos(angle) * pointRadius
|
|
112
|
+
positions[i * 2 + 1] = Math.sin(angle) * pointRadius
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return positions
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function generatePointColors (points: Point[]): Float32Array {
|
|
119
|
+
const colors = new Float32Array(points.length * 4)
|
|
120
|
+
|
|
121
|
+
points.forEach((point, i) => {
|
|
122
|
+
const hue = (point.id / points.length) * 360
|
|
123
|
+
const [r, g, b] = hslToRgb(hue, 0.8, 0.6)
|
|
124
|
+
|
|
125
|
+
colors[i * 4] = r
|
|
126
|
+
colors[i * 4 + 1] = g
|
|
127
|
+
colors[i * 4 + 2] = b
|
|
128
|
+
colors[i * 4 + 3] = 1.0
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return colors
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function generatePointSizes (points: Point[]): Float32Array {
|
|
135
|
+
const sizes = new Float32Array(points.length)
|
|
136
|
+
sizes.fill(10)
|
|
137
|
+
return sizes
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function generateLinkData (connections: Link[], points: Point[]): {
|
|
141
|
+
links: Float32Array;
|
|
142
|
+
linkColors: Float32Array;
|
|
143
|
+
linkWidths: Float32Array;
|
|
144
|
+
} {
|
|
145
|
+
const links = new Float32Array(connections.length * 2)
|
|
146
|
+
const linkColors = new Float32Array(connections.length * 4)
|
|
147
|
+
const linkWidths = new Float32Array(connections.length)
|
|
148
|
+
|
|
149
|
+
connections.forEach((connection, i) => {
|
|
150
|
+
links[i * 2] = connection.source
|
|
151
|
+
links[i * 2 + 1] = connection.target
|
|
152
|
+
|
|
153
|
+
// Color links based on average hue of connected points
|
|
154
|
+
const sourceHue = (connection.source / points.length) * 360
|
|
155
|
+
const targetHue = (connection.target / points.length) * 360
|
|
156
|
+
|
|
157
|
+
let avgHue
|
|
158
|
+
const hueDiff = Math.abs(targetHue - sourceHue)
|
|
159
|
+
if (hueDiff > 180) {
|
|
160
|
+
avgHue = ((sourceHue + targetHue + 360) / 2) % 360
|
|
161
|
+
} else {
|
|
162
|
+
avgHue = (sourceHue + targetHue) / 2
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const [r, g, b] = hslToRgb(avgHue, 0.7, 0.5)
|
|
166
|
+
|
|
167
|
+
linkColors[i * 4] = r
|
|
168
|
+
linkColors[i * 4 + 1] = g
|
|
169
|
+
linkColors[i * 4 + 2] = b
|
|
170
|
+
linkColors[i * 4 + 3] = 0.9
|
|
171
|
+
|
|
172
|
+
linkWidths[i] = 2
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
return { links, linkColors, linkWidths }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function generateData (pointCount: number = 500): NetworkData {
|
|
179
|
+
const points = generatePoints(pointCount)
|
|
180
|
+
const connections = generateConnections(points)
|
|
181
|
+
|
|
182
|
+
const pointPositions = generatePointPositions(points)
|
|
183
|
+
const pointColors = generatePointColors(points)
|
|
184
|
+
const pointSizes = generatePointSizes(points)
|
|
185
|
+
|
|
186
|
+
const { links, linkColors, linkWidths } = generateLinkData(connections, points)
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
pointPositions,
|
|
190
|
+
pointColors,
|
|
191
|
+
pointSizes,
|
|
192
|
+
links,
|
|
193
|
+
linkColors,
|
|
194
|
+
linkWidths,
|
|
195
|
+
points,
|
|
196
|
+
connections,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Graph, GraphConfigInterface } from '@cosmos.gl/graph'
|
|
2
|
+
import { generateData } from './data-generator'
|
|
3
|
+
import './style.css'
|
|
4
|
+
|
|
5
|
+
export const linkHovering = (): { div: HTMLDivElement; graph: Graph } => {
|
|
6
|
+
const data = generateData()
|
|
7
|
+
const infoPanel = document.createElement('div')
|
|
8
|
+
|
|
9
|
+
// Create div container
|
|
10
|
+
const div = document.createElement('div')
|
|
11
|
+
div.style.height = '100vh'
|
|
12
|
+
div.style.width = '100%'
|
|
13
|
+
div.style.position = 'relative'
|
|
14
|
+
|
|
15
|
+
// Configure graph
|
|
16
|
+
const config: GraphConfigInterface = {
|
|
17
|
+
backgroundColor: '#2d313a',
|
|
18
|
+
scalePointsOnZoom: true,
|
|
19
|
+
linkArrows: false,
|
|
20
|
+
curvedLinks: true,
|
|
21
|
+
enableSimulation: false,
|
|
22
|
+
hoveredLinkWidthIncrease: 4,
|
|
23
|
+
attribution: 'visualized with <a href="https://cosmograph.app/" style="color: var(--cosmosgl-attribution-color);" target="_blank">Cosmograph</a>',
|
|
24
|
+
|
|
25
|
+
onLinkMouseOver: (linkIndex: number) => {
|
|
26
|
+
infoPanel.style.display = 'block'
|
|
27
|
+
infoPanel.textContent = `Link ${linkIndex}`
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
onLinkMouseOut: () => {
|
|
31
|
+
infoPanel.style.display = 'none'
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Create graph instance
|
|
36
|
+
const graph = new Graph(div, config)
|
|
37
|
+
|
|
38
|
+
// Set data
|
|
39
|
+
graph.setPointPositions(data.pointPositions)
|
|
40
|
+
graph.setPointColors(data.pointColors)
|
|
41
|
+
graph.setPointSizes(data.pointSizes)
|
|
42
|
+
graph.setLinks(data.links)
|
|
43
|
+
graph.setLinkColors(data.linkColors)
|
|
44
|
+
graph.setLinkWidths(data.linkWidths)
|
|
45
|
+
|
|
46
|
+
// Render graph
|
|
47
|
+
graph.zoom(0.9)
|
|
48
|
+
graph.render()
|
|
49
|
+
|
|
50
|
+
infoPanel.style.cssText = `
|
|
51
|
+
position: absolute;
|
|
52
|
+
top: 20px;
|
|
53
|
+
left: 20px;
|
|
54
|
+
color: white;
|
|
55
|
+
font-size: 14px;
|
|
56
|
+
display: none;
|
|
57
|
+
`
|
|
58
|
+
div.appendChild(infoPanel)
|
|
59
|
+
|
|
60
|
+
return { div, graph }
|
|
61
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* Enhanced styling for the link hovering demo */
|
|
2
|
+
|
|
3
|
+
.company-network {
|
|
4
|
+
background: radial-gradient(circle at 50% 50%, #1a1a2e 0%, #0a0a0a 100%);
|
|
5
|
+
position: relative;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.info-panel {
|
|
10
|
+
backdrop-filter: blur(12px);
|
|
11
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.info-panel:hover {
|
|
15
|
+
transform: translateY(-2px);
|
|
16
|
+
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.6), 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.relationship-type-indicator {
|
|
20
|
+
display: inline-block;
|
|
21
|
+
width: 12px;
|
|
22
|
+
height: 12px;
|
|
23
|
+
border-radius: 50%;
|
|
24
|
+
margin-right: 8px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.relationship-type-indicator.management {
|
|
28
|
+
background: linear-gradient(45deg, #FFAD6B, #FF8A65);
|
|
29
|
+
box-shadow: 0 2px 8px rgba(255, 173, 107, 0.4);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.relationship-type-indicator.collaboration {
|
|
33
|
+
background: linear-gradient(45deg, #59C0FF, #42A5F5);
|
|
34
|
+
box-shadow: 0 2px 8px rgba(89, 192, 255, 0.4);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.relationship-type-indicator.mentorship {
|
|
38
|
+
background: linear-gradient(45deg, #6BD17B, #66BB6A);
|
|
39
|
+
box-shadow: 0 2px 8px rgba(107, 209, 123, 0.4);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.relationship-type-indicator.friendship {
|
|
43
|
+
background: linear-gradient(45deg, #FF61AD, #EC407A);
|
|
44
|
+
box-shadow: 0 2px 8px rgba(255, 97, 173, 0.4);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.person-card {
|
|
48
|
+
background: rgba(255, 255, 255, 0.03);
|
|
49
|
+
border-radius: 8px;
|
|
50
|
+
padding: 12px;
|
|
51
|
+
margin: 6px 0;
|
|
52
|
+
border-left: 3px solid transparent;
|
|
53
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
54
|
+
transition: all 0.2s ease;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.person-card.highlighted {
|
|
58
|
+
border-left-color: #59C0FF;
|
|
59
|
+
background: rgba(89, 192, 255, 0.08);
|
|
60
|
+
box-shadow: 0 4px 16px rgba(89, 192, 255, 0.15);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Modern color scheme variables */
|
|
64
|
+
:root {
|
|
65
|
+
--cosmos-orange: #FFAD6B;
|
|
66
|
+
--cosmos-blue: #59C0FF;
|
|
67
|
+
--cosmos-green: #6BD17B;
|
|
68
|
+
--cosmos-pink: #FF61AD;
|
|
69
|
+
--cosmos-gray: #C7CDD1;
|
|
70
|
+
--cosmos-dark-bg: rgba(16, 20, 40, 0.95);
|
|
71
|
+
--cosmos-card-bg: rgba(255, 255, 255, 0.03);
|
|
72
|
+
--cosmos-border: rgba(255, 255, 255, 0.08);
|
|
73
|
+
}
|
|
@@ -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
|
|
@@ -18,7 +18,8 @@ export const quickStart = (): { graph: Graph; div: HTMLDivElement} => {
|
|
|
18
18
|
fitViewPadding: 0.3, // centers the graph width padding of ~30% of screen
|
|
19
19
|
rescalePositions: true, // rescale positions
|
|
20
20
|
enableDrag: true, // enable dragging points
|
|
21
|
-
|
|
21
|
+
onPointClick: pointIndex => { console.log('Clicked point index: ', pointIndex) },
|
|
22
|
+
onBackgroundClick: () => { console.log('Clicked background') },
|
|
22
23
|
attribution: 'visualized with <a href="https://cosmograph.app/" style="color: var(--cosmosgl-attribution-color);" target="_blank">Cosmograph</a>',
|
|
23
24
|
/* ... */
|
|
24
25
|
}
|
|
@@ -22,40 +22,38 @@ export const removePoints = (): { graph: Graph; div: HTMLDivElement} => {
|
|
|
22
22
|
|
|
23
23
|
const graph = new Graph(graphDiv, {
|
|
24
24
|
...config,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
(posIndex % 2 === 0 && posIndex !== i * 2) ||
|
|
25
|
+
onPointClick: (i): void => {
|
|
26
|
+
// Filter out the clicked point from positions array
|
|
27
|
+
const currentPositions = graph.getPointPositions()
|
|
28
|
+
const newPointPositions = currentPositions
|
|
29
|
+
.filter((pos, posIndex) => {
|
|
30
|
+
return (
|
|
31
|
+
(posIndex % 2 === 0 && posIndex !== i * 2) ||
|
|
33
32
|
(posIndex % 2 === 1 && posIndex !== i * 2 + 1)
|
|
34
|
-
)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
// Convert links array to source-target pairs for easier filtering
|
|
38
|
-
const pairedNumbers = []
|
|
39
|
-
for (let j = 0; j < graphLinks.length; j += 2) {
|
|
40
|
-
const pair = [graphLinks[j], graphLinks[j + 1]]
|
|
41
|
-
pairedNumbers.push(pair)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Remove links connected to deleted point and adjust indices of remaining links
|
|
45
|
-
graphLinks = (pairedNumbers
|
|
46
|
-
.filter(
|
|
47
|
-
([sourceIndex, targetIndex]) => sourceIndex !== i && targetIndex !== i
|
|
48
33
|
)
|
|
49
|
-
|
|
50
|
-
.map((p) => {
|
|
51
|
-
if (p > i) return p - 1
|
|
52
|
-
else return p
|
|
53
|
-
})
|
|
34
|
+
})
|
|
54
35
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
// Convert links array to source-target pairs for easier filtering
|
|
37
|
+
const pairedNumbers = []
|
|
38
|
+
for (let j = 0; j < graphLinks.length; j += 2) {
|
|
39
|
+
const pair = [graphLinks[j], graphLinks[j + 1]]
|
|
40
|
+
pairedNumbers.push(pair)
|
|
58
41
|
}
|
|
42
|
+
|
|
43
|
+
// Remove links connected to deleted point and adjust indices of remaining links
|
|
44
|
+
graphLinks = (pairedNumbers
|
|
45
|
+
.filter(
|
|
46
|
+
([sourceIndex, targetIndex]) => sourceIndex !== i && targetIndex !== i
|
|
47
|
+
)
|
|
48
|
+
.flat() as number[])
|
|
49
|
+
.map((p) => {
|
|
50
|
+
if (p > i) return p - 1
|
|
51
|
+
else return p
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
graph.setPointPositions(new Float32Array(newPointPositions))
|
|
55
|
+
graph.setLinks(new Float32Array(graphLinks))
|
|
56
|
+
graph.render(isPaused ? 0 : undefined)
|
|
59
57
|
console.log('Clicked node: ', i)
|
|
60
58
|
},
|
|
61
59
|
})
|
|
@@ -6,6 +6,8 @@ import { quickStart } from './beginners/quick-start'
|
|
|
6
6
|
import { basicSetUp } from './beginners/basic-set-up'
|
|
7
7
|
import { pointLabels } from './beginners/point-labels'
|
|
8
8
|
import { removePoints } from './beginners/remove-points'
|
|
9
|
+
import { linkHovering } from './beginners/link-hovering'
|
|
10
|
+
import { pinnedPoints } from './beginners/pinned-points'
|
|
9
11
|
|
|
10
12
|
import quickStartStoryRaw from './beginners/quick-start?raw'
|
|
11
13
|
import basicSetUpStoryRaw from './beginners/basic-set-up/index?raw'
|
|
@@ -19,6 +21,11 @@ import removePointsStoryRaw from './beginners/remove-points/index?raw'
|
|
|
19
21
|
import removePointsStoryCssRaw from './beginners/remove-points/style.css?raw'
|
|
20
22
|
import removePointsStoryConfigRaw from './beginners/remove-points/config.ts?raw'
|
|
21
23
|
import removePointsStoryDataGenRaw from './beginners/remove-points/data-gen.ts?raw'
|
|
24
|
+
import linkHoveringStoryRaw from './beginners/link-hovering/index?raw'
|
|
25
|
+
import linkHoveringStoryDataGenRaw from './beginners/link-hovering/data-generator.ts?raw'
|
|
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'
|
|
22
29
|
|
|
23
30
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
24
31
|
const meta: Meta<CosmosStoryProps> = {
|
|
@@ -96,5 +103,29 @@ export const RemovePoints: Story = {
|
|
|
96
103
|
],
|
|
97
104
|
},
|
|
98
105
|
}
|
|
106
|
+
|
|
107
|
+
export const LinkHovering: Story = {
|
|
108
|
+
...createStory(linkHovering),
|
|
109
|
+
name: 'Link Hovering',
|
|
110
|
+
parameters: {
|
|
111
|
+
sourceCode: [
|
|
112
|
+
{ name: 'Story', code: linkHoveringStoryRaw },
|
|
113
|
+
{ name: 'data-generator.ts', code: linkHoveringStoryDataGenRaw },
|
|
114
|
+
{ name: 'style.css', code: linkHoveringStoryCssRaw },
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
}
|
|
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
|
+
|
|
99
130
|
// eslint-disable-next-line import/no-default-export
|
|
100
131
|
export default meta
|