@reventlessdev/rescript-cytoscape 0.1.0-alpha.1
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/README.md +57 -0
- package/package.json +34 -0
- package/rescript.json +9 -0
- package/src/CytoscapeBindings.res +227 -0
- package/src/CytoscapeBindings.res.mjs +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @reventlessdev/rescript-cytoscape
|
|
2
|
+
|
|
3
|
+
ReScript bindings for [Cytoscape.js](https://js.cytoscape.org/) — a narrow
|
|
4
|
+
surface covering what a node-link view needs:
|
|
5
|
+
|
|
6
|
+
- `Cytoscape.CytoscapeBindings.make({container, elements, style, layout, …})`
|
|
7
|
+
— an instance rendering into a container element.
|
|
8
|
+
- Element descriptors: `nodeElement({data: {id, label, parent}})`,
|
|
9
|
+
`edgeElement({data: {id, source, target, label}})` (`parent` is cytoscape's
|
|
10
|
+
compound-node mechanism — clustering for free).
|
|
11
|
+
- Stylesheets: `rule(~selector, [("background-color", "#333"), …])`, applied at
|
|
12
|
+
init or swapped wholesale with `style(cy, sheet)` (how a theme change lands).
|
|
13
|
+
- Layouts: `layout(cy, {name: "dagre", rankDir: "BT"})->run`. Built-in names
|
|
14
|
+
are `grid` / `circle` / `concentric` / `breadthfirst` / `cose` / `preset`;
|
|
15
|
+
layered `dagre` and compound-aware `fcose` are extensions and need
|
|
16
|
+
`use(dagre)` / `use(fcose)` once, first.
|
|
17
|
+
- Camera: `fit(cy, padding)`, `center`, `resize`, `zoom`/`setZoom`,
|
|
18
|
+
`pan`/`setPan` — cytoscape ships pan/zoom, nothing to build.
|
|
19
|
+
- Events: `onSelector(cy, "tap", "node", e => Event.targetId(e))`.
|
|
20
|
+
- Algorithms: `dijkstra`, `aStar`, `neighborhood`, `components`, `degree`.
|
|
21
|
+
- `png(cy, {full: true})` → base64 data URI; `destroy(cy)` on unmount.
|
|
22
|
+
|
|
23
|
+
The package carries **no Reventless and no React dependency** — the AutoUI
|
|
24
|
+
graph view mode built on it lives in
|
|
25
|
+
[`@reventlessdev/reventless-graph`](../../reventless/graph).
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm add @reventlessdev/rescript-cytoscape cytoscape cytoscape-dagre cytoscape-fcose
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`rescript.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
"dependencies": ["@reventlessdev/rescript-cytoscape"]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```rescript
|
|
40
|
+
open Cytoscape.CytoscapeBindings
|
|
41
|
+
|
|
42
|
+
use(dagre) // once, before the first instance that asks for these layouts
|
|
43
|
+
use(fcose)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Cytoscape draws to a canvas and needs no stylesheet import; the container
|
|
47
|
+
element must have a non-zero height.
|
|
48
|
+
|
|
49
|
+
## Building in this repo
|
|
50
|
+
|
|
51
|
+
`pnpm run build` here uses `rescript-legacy`: the ReScript v12 build
|
|
52
|
+
orchestrator still panics (UTF-8 underline bug) when invoked from a downstream
|
|
53
|
+
package that walks into reventless-ui's bs-dependencies. This package has no
|
|
54
|
+
dependencies, so build it **first**, then reventless-graph, then
|
|
55
|
+
reventless-ui **last** — building the downstream package walks into ui and
|
|
56
|
+
deletes its dev-source `.res.mjs`, which only a ui `clean && build` restores.
|
|
57
|
+
Never use `-with-deps` from a downstream package.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reventlessdev/rescript-cytoscape",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Cytoscape.js bindings for ReScript. The minimal surface (graph init, element builders, stylesheets, layouts, camera, events, graph algorithms, PNG export). Framework-agnostic — the Reventless AutoUI graph view mode lives in @reventlessdev/reventless-graph.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"files": [
|
|
7
|
+
"src/**/*.res",
|
|
8
|
+
"src/**/*.res.mjs",
|
|
9
|
+
"rescript.json"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "rescript-legacy build",
|
|
13
|
+
"start": "rescript-legacy build -w",
|
|
14
|
+
"clean": "rescript-legacy clean",
|
|
15
|
+
"test": "echo \"no test specified\""
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"cytoscape": "^3.34.0",
|
|
19
|
+
"cytoscape-dagre": "^4.0.0",
|
|
20
|
+
"cytoscape-fcose": "^2.2.0",
|
|
21
|
+
"rescript": "^12.3.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"cytoscape": ">=3.30.0",
|
|
25
|
+
"cytoscape-dagre": ">=2.5.0",
|
|
26
|
+
"cytoscape-fcose": ">=2.2.0",
|
|
27
|
+
"rescript": "^12.3.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"registry": "https://registry.npmjs.org",
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"gitHead": "ee9a72b1a1f76e7fdeab58922e4fb653a1b20cec"
|
|
34
|
+
}
|
package/rescript.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reventlessdev/rescript-cytoscape",
|
|
3
|
+
"namespace": "Cytoscape",
|
|
4
|
+
"sources": [{ "dir": "src", "subdirs": true }],
|
|
5
|
+
"package-specs": { "module": "esmodule", "in-source": true },
|
|
6
|
+
"suffix": ".res.mjs",
|
|
7
|
+
"warnings": { "number": "-3" },
|
|
8
|
+
"dependencies": []
|
|
9
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Minimal cytoscape bindings — only what the AutoUI graph view mode
|
|
2
|
+
// (@reventlessdev/reventless-graph) and its stories need: instance init,
|
|
3
|
+
// element descriptors, stylesheets, layouts, the built-in camera, tap events,
|
|
4
|
+
// the handful of graph algorithms we surface, and PNG export.
|
|
5
|
+
//
|
|
6
|
+
// Cytoscape is imperative and framework-agnostic (it owns a canvas inside a
|
|
7
|
+
// container element), so this binds like the maplibre package: no React, no
|
|
8
|
+
// Reventless. Style values travel as strings — cytoscape parses numeric
|
|
9
|
+
// properties out of strings, and every property we set is expressible that
|
|
10
|
+
// way, which keeps the stylesheet a plain (selector, declarations) pair
|
|
11
|
+
// instead of a typed pyramid.
|
|
12
|
+
|
|
13
|
+
type t // a cytoscape instance
|
|
14
|
+
type collection // one or more elements (nodes and/or edges)
|
|
15
|
+
type element // an element descriptor handed to cytoscape
|
|
16
|
+
type extension // a registrable extension (cytoscape-dagre, …)
|
|
17
|
+
|
|
18
|
+
// ─── Elements ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
type nodeData = {
|
|
21
|
+
id: string,
|
|
22
|
+
label?: string,
|
|
23
|
+
parent?: string, // compound parent node id — the clustering mechanism
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type edgeData = {
|
|
27
|
+
id: string,
|
|
28
|
+
source: string,
|
|
29
|
+
target: string,
|
|
30
|
+
label?: string,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type nodeSpec = {data: nodeData, classes?: string}
|
|
34
|
+
type edgeSpec = {data: edgeData, classes?: string}
|
|
35
|
+
|
|
36
|
+
// Descriptors are plain objects; the casts exist so `elements` can hold both.
|
|
37
|
+
external nodeElement: nodeSpec => element = "%identity"
|
|
38
|
+
external edgeElement: edgeSpec => element = "%identity"
|
|
39
|
+
|
|
40
|
+
// ─── Stylesheet ──────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
type styleRule = {
|
|
43
|
+
selector: string,
|
|
44
|
+
style: Js.Dict.t<string>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let rule = (~selector: string, declarations: array<(string, string)>): styleRule => {
|
|
48
|
+
selector,
|
|
49
|
+
style: Js.Dict.fromArray(declarations),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Layout ──────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
// One record for every layout: cytoscape ignores options a layout doesn't
|
|
55
|
+
// know, so `{name: "dagre", rankDir: "TB"}` and `{name: "concentric"}` share
|
|
56
|
+
// a type. Built-in names: grid, circle, concentric, breadthfirst, cose,
|
|
57
|
+
// random, preset. `dagre` needs `use(dagre)` first.
|
|
58
|
+
type layoutOptions = {
|
|
59
|
+
name: string,
|
|
60
|
+
fit?: bool,
|
|
61
|
+
padding?: int,
|
|
62
|
+
animate?: bool,
|
|
63
|
+
animationDuration?: int,
|
|
64
|
+
spacingFactor?: float,
|
|
65
|
+
nodeDimensionsIncludeLabels?: bool,
|
|
66
|
+
// dagre
|
|
67
|
+
rankDir?: string, // "TB" | "BT" | "LR" | "RL"
|
|
68
|
+
nodeSep?: int,
|
|
69
|
+
rankSep?: int,
|
|
70
|
+
edgeSep?: int,
|
|
71
|
+
// breadthfirst
|
|
72
|
+
directed?: bool,
|
|
73
|
+
roots?: string,
|
|
74
|
+
circle?: bool,
|
|
75
|
+
// concentric
|
|
76
|
+
minNodeSpacing?: int,
|
|
77
|
+
// cose (the built-in force layout) / fcose
|
|
78
|
+
idealEdgeLength?: int,
|
|
79
|
+
nodeRepulsion?: int,
|
|
80
|
+
gravity?: float,
|
|
81
|
+
// Multiplies the ideal edge length for edges that cross a compound
|
|
82
|
+
// boundary — how far apart cluster boxes are pushed.
|
|
83
|
+
nestingFactor?: float,
|
|
84
|
+
numIter?: int,
|
|
85
|
+
randomize?: bool,
|
|
86
|
+
componentSpacing?: int,
|
|
87
|
+
// fcose only
|
|
88
|
+
quality?: string, // "draft" | "default" | "proof"
|
|
89
|
+
nodeSeparation?: int,
|
|
90
|
+
gravityCompound?: float,
|
|
91
|
+
packComponents?: bool,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type layout
|
|
95
|
+
|
|
96
|
+
// ─── Instance ────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
type options = {
|
|
99
|
+
container: Dom.element,
|
|
100
|
+
elements: array<element>,
|
|
101
|
+
style?: array<styleRule>,
|
|
102
|
+
layout?: layoutOptions,
|
|
103
|
+
minZoom?: float,
|
|
104
|
+
maxZoom?: float,
|
|
105
|
+
wheelSensitivity?: float,
|
|
106
|
+
autoungrabify?: bool,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@module("cytoscape") external make: options => t = "default"
|
|
110
|
+
|
|
111
|
+
// `cytoscape.use(ext)` registers an extension globally — call once, before
|
|
112
|
+
// the first `make` that names its layout.
|
|
113
|
+
type lib
|
|
114
|
+
@module("cytoscape") external lib: lib = "default"
|
|
115
|
+
@send external useExtension: (lib, extension) => unit = "use"
|
|
116
|
+
let use = (ext: extension): unit => lib->useExtension(ext)
|
|
117
|
+
|
|
118
|
+
@module("cytoscape-dagre") external dagre: extension = "default"
|
|
119
|
+
// fcose is the compound-aware force layout — the built-in `cose` piles
|
|
120
|
+
// cluster boxes on top of each other.
|
|
121
|
+
@module("cytoscape-fcose") external fcose: extension = "default"
|
|
122
|
+
|
|
123
|
+
@send external destroy: t => unit = "destroy"
|
|
124
|
+
|
|
125
|
+
// ─── Collections ─────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
@send external elements: t => collection = "elements"
|
|
128
|
+
@send external nodes: t => collection = "nodes"
|
|
129
|
+
@send external edges: t => collection = "edges"
|
|
130
|
+
@send external getElementById: (t, string) => collection = "getElementById"
|
|
131
|
+
// Selector query, e.g. cy->query("node[kind = 'Order']").
|
|
132
|
+
@send external query: (t, string) => collection = "$"
|
|
133
|
+
|
|
134
|
+
@send external id: collection => string = "id"
|
|
135
|
+
@send external size: collection => int = "size"
|
|
136
|
+
@send external map: (collection, collection => 'a) => array<'a> = "map"
|
|
137
|
+
@send external forEach: (collection, collection => unit) => unit = "forEach"
|
|
138
|
+
@send external addClass: (collection, string) => collection = "addClass"
|
|
139
|
+
@send external removeClass: (collection, string) => collection = "removeClass"
|
|
140
|
+
@send external dataAt: (collection, string) => Js.Json.t = "data"
|
|
141
|
+
// Writing a computed value back into element data is how a derived metric
|
|
142
|
+
// (degree centrality, a score) becomes available to `mapData(…)` in a style.
|
|
143
|
+
@send external setDataAt: (collection, string, Js.Json.t) => collection = "data"
|
|
144
|
+
// Neighbours reached in either direction; `closed` includes the node itself.
|
|
145
|
+
@send external neighborhood: collection => collection = "neighborhood"
|
|
146
|
+
@send external closedNeighborhood: collection => collection = "closedNeighborhood"
|
|
147
|
+
@send external union: (collection, collection) => collection = "union"
|
|
148
|
+
@send external difference: (collection, collection) => collection = "difference"
|
|
149
|
+
// Degree centrality of the collection's first element.
|
|
150
|
+
@send external degree: (collection, bool) => int = "degree"
|
|
151
|
+
// Connected components, one collection each.
|
|
152
|
+
@send external components: collection => array<collection> = "components"
|
|
153
|
+
|
|
154
|
+
@send external add: (t, array<element>) => collection = "add"
|
|
155
|
+
@send external remove: (t, collection) => collection = "remove"
|
|
156
|
+
@send external removeSelector: (t, string) => collection = "remove"
|
|
157
|
+
|
|
158
|
+
// Replaces the whole stylesheet — how a theme change is applied.
|
|
159
|
+
@send external style: (t, array<styleRule>) => unit = "style"
|
|
160
|
+
|
|
161
|
+
@send external layout: (t, layoutOptions) => layout = "layout"
|
|
162
|
+
@send external run: layout => unit = "run"
|
|
163
|
+
@send external stop: layout => unit = "stop"
|
|
164
|
+
|
|
165
|
+
// ─── Camera ──────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
type position = {x: float, y: float}
|
|
168
|
+
|
|
169
|
+
// cy.fit(padding) — the number arity fits every element with that padding.
|
|
170
|
+
@send external fit: (t, int) => unit = "fit"
|
|
171
|
+
@send external fitTo: (t, collection, int) => unit = "fit"
|
|
172
|
+
@send external center: t => unit = "center"
|
|
173
|
+
// Re-reads the container size; call after the container box changes.
|
|
174
|
+
@send external resize: t => unit = "resize"
|
|
175
|
+
@send external zoom: t => float = "zoom"
|
|
176
|
+
@send external setZoom: (t, float) => unit = "zoom"
|
|
177
|
+
@send external pan: t => position = "pan"
|
|
178
|
+
@send external setPan: (t, position) => unit = "pan"
|
|
179
|
+
|
|
180
|
+
// ─── Events ──────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
type event
|
|
183
|
+
|
|
184
|
+
// Graph-wide ("tap" on the background included).
|
|
185
|
+
@send external on: (t, string, event => unit) => unit = "on"
|
|
186
|
+
// Delegated to elements matching a selector, e.g. on(cy, "tap", "node", …).
|
|
187
|
+
@send external onSelector: (t, string, string, event => unit) => unit = "on"
|
|
188
|
+
|
|
189
|
+
module Event = {
|
|
190
|
+
@get external target: event => collection = "target"
|
|
191
|
+
// The tapped element's id — the row key the graph mode drills through.
|
|
192
|
+
let targetId = (e: event): string => e->target->id
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Algorithms ──────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
type dijkstraOptions = {
|
|
198
|
+
root: string, // selector, e.g. "#e1"
|
|
199
|
+
directed?: bool,
|
|
200
|
+
}
|
|
201
|
+
type dijkstraResult
|
|
202
|
+
@send external dijkstra: (collection, dijkstraOptions) => dijkstraResult = "dijkstra"
|
|
203
|
+
@send external distanceTo: (dijkstraResult, collection) => float = "distanceTo"
|
|
204
|
+
// The nodes and edges of the shortest path, in order.
|
|
205
|
+
@send external pathTo: (dijkstraResult, collection) => collection = "pathTo"
|
|
206
|
+
|
|
207
|
+
type aStarOptions = {
|
|
208
|
+
root: string,
|
|
209
|
+
goal: string,
|
|
210
|
+
directed?: bool,
|
|
211
|
+
}
|
|
212
|
+
type aStarResult = {
|
|
213
|
+
found: bool,
|
|
214
|
+
distance: float,
|
|
215
|
+
path: collection,
|
|
216
|
+
}
|
|
217
|
+
@send external aStar: (collection, aStarOptions) => aStarResult = "aStar"
|
|
218
|
+
|
|
219
|
+
// ─── Export ──────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
type pngOptions = {
|
|
222
|
+
full?: bool, // whole graph rather than the current viewport
|
|
223
|
+
scale?: float,
|
|
224
|
+
bg?: string,
|
|
225
|
+
}
|
|
226
|
+
// Returns a base64 data URI.
|
|
227
|
+
@send external png: (t, pngOptions) => string = "png"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Js_dict from "@rescript/runtime/lib/es6/Js_dict.js";
|
|
4
|
+
import Cytoscape from "cytoscape";
|
|
5
|
+
|
|
6
|
+
function rule(selector, declarations) {
|
|
7
|
+
return {
|
|
8
|
+
selector: selector,
|
|
9
|
+
style: Js_dict.fromArray(declarations)
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function use(ext) {
|
|
14
|
+
Cytoscape.use(ext);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function targetId(e) {
|
|
18
|
+
return e.target.id();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let Event = {
|
|
22
|
+
targetId: targetId
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
rule,
|
|
27
|
+
use,
|
|
28
|
+
Event,
|
|
29
|
+
}
|
|
30
|
+
/* cytoscape Not a pure module */
|