@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 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 */