@slxu/graphsx 0.1.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/LICENSE +21 -0
- package/README.md +588 -0
- package/docs/assets/basic-graph.svg +15 -0
- package/docs/assets/plot-heart.svg +15 -0
- package/docs/assets/tensor-repeat.svg +15 -0
- package/package.json +56 -0
- package/src/codemirror.css +66 -0
- package/src/codemirror.js +225 -0
- package/src/document.js +71 -0
- package/src/errors.js +7 -0
- package/src/index.js +42 -0
- package/src/literals.js +436 -0
- package/src/markdown.css +24 -0
- package/src/markdown.js +111 -0
- package/src/markup.js +219 -0
- package/src/parser.js +1463 -0
- package/src/plot-math.js +459 -0
- package/src/plot-renderer.js +1025 -0
- package/src/plot.js +842 -0
- package/src/renderer.js +987 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shenglong Xu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
# GraphSX
|
|
2
|
+
|
|
3
|
+
GraphSX is a JSX-like DSL for drawing diagrams, plots, and Markdown-native figures as SVG.
|
|
4
|
+
|
|
5
|
+
It is designed for notes, papers, docs, and small scientific figures where the source should stay readable:
|
|
6
|
+
|
|
7
|
+
- JSX-like tags and props instead of a separate command language
|
|
8
|
+
- reusable shapes with public ports
|
|
9
|
+
- links, explicit paths, routing, arrowheads, and labels
|
|
10
|
+
- standalone plots with axes, data, ticks, legends, annotations, and KaTeX labels
|
|
11
|
+
- nested plots inside graphs for subplots and mixed plot/diagram figures
|
|
12
|
+
- Markdown fences and CodeMirror live-preview widgets
|
|
13
|
+
|
|
14
|
+
Try the playground: https://slxuphys.github.io/graphsx/
|
|
15
|
+
|
|
16
|
+
Current npm package name: `@slxu/graphsx`. Project and repo name: GraphSX.
|
|
17
|
+
|
|
18
|
+
Install from npm:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @slxu/graphsx
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What It Looks Like
|
|
25
|
+
|
|
26
|
+
### Port Diagram
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
<Graph>
|
|
30
|
+
<Style id="box" fill="#eef6ff" stroke="#1d4ed8" />
|
|
31
|
+
<Style id="wire" stroke="#7c3aed" strokeWidth={3} />
|
|
32
|
+
|
|
33
|
+
<Rect id="A" at={[70, 82]} size={[100, 60]} label="alpha" useStyle="box">
|
|
34
|
+
<Port id="out" right label="xy" />
|
|
35
|
+
</Rect>
|
|
36
|
+
<Circle id="B" at={[280, 112]} r={40} label="B">
|
|
37
|
+
<Port id="in" left />
|
|
38
|
+
</Circle>
|
|
39
|
+
|
|
40
|
+
<Link headArrow from="A.out" to="B.in" useStyle="wire" />
|
|
41
|
+
</Graph>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
### Parametric Plot
|
|
47
|
+
|
|
48
|
+
```jsx
|
|
49
|
+
<Plot width={430} height={330} xDomain={[-18, 18]} yDomain={[-18, 14]} frame box>
|
|
50
|
+
<Data
|
|
51
|
+
id="heart"
|
|
52
|
+
x="16 * pow(sin(t), 3)"
|
|
53
|
+
y="13*cos(t) - 5*cos(2*t) - 2*cos(3*t) - cos(4*t)"
|
|
54
|
+
domain={[0, 2*pi]}
|
|
55
|
+
samples={360}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<Axis x label="x" ticks grid />
|
|
59
|
+
<Axis y label="y" ticks grid />
|
|
60
|
+
<Line data="heart" stroke="#e11d48" strokeWidth={2.6} />
|
|
61
|
+
</Plot>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
### Reusable Shapes And Repeat
|
|
67
|
+
|
|
68
|
+
```jsx
|
|
69
|
+
<Graph route="straight">
|
|
70
|
+
<Shape id="Tensor" groupBox={false}>
|
|
71
|
+
<Rect id="box" size={[54, 54]} corner={8} label={tensorLabel}>
|
|
72
|
+
<Port id="left" left r={0} />
|
|
73
|
+
<Port id="right" right r={0} />
|
|
74
|
+
<Port id="phys" bottom r={0} />
|
|
75
|
+
</Rect>
|
|
76
|
+
<Port id="left" target="box.left" />
|
|
77
|
+
<Port id="right" target="box.right" />
|
|
78
|
+
<Port id="phys" target="box.phys" />
|
|
79
|
+
</Shape>
|
|
80
|
+
|
|
81
|
+
<Repeat count={4} as="i" step={[96, 0]}>
|
|
82
|
+
<Tensor id={`A${i}`} at={[70, 60]} tensorLabel={`A${i}`} />
|
|
83
|
+
</Repeat>
|
|
84
|
+
</Graph>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+

|
|
88
|
+
|
|
89
|
+
## Quick Example
|
|
90
|
+
|
|
91
|
+
```jsx
|
|
92
|
+
<Graph>
|
|
93
|
+
<Style id="box" fill="#eef6ff" stroke="#1d4ed8" strokeWidth={2} />
|
|
94
|
+
<Style id="wire" stroke="#7c3aed" strokeWidth={3} />
|
|
95
|
+
|
|
96
|
+
<Rect id="A" at={[100, 100]} size={[100, 60]} label="$\alpha$" useStyle="box">
|
|
97
|
+
<Port id="out" right label="xy" />
|
|
98
|
+
</Rect>
|
|
99
|
+
|
|
100
|
+
<Circle id="B" at={[300, 100]} r={40} label="B">
|
|
101
|
+
<Port id="in" left />
|
|
102
|
+
</Circle>
|
|
103
|
+
|
|
104
|
+
<Link headArrow from="A.out" to="B.in" useStyle="wire" />
|
|
105
|
+
</Graph>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Use it from JavaScript:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
import { parseGraphSXDocument, renderGraphSXDocument } from "@slxu/graphsx";
|
|
112
|
+
import katex from "katex";
|
|
113
|
+
|
|
114
|
+
const model = parseGraphSXDocument(source);
|
|
115
|
+
renderGraphSXDocument(document.querySelector("svg"), model, { katex });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Labels are opt-in. Use `label="xy"` for plain text and `label="$\alpha$"` for KaTeX math. If there is no `label` prop, no label is rendered.
|
|
119
|
+
|
|
120
|
+
## Syntax Model
|
|
121
|
+
|
|
122
|
+
GraphSX should feel familiar if you already understand JSX:
|
|
123
|
+
|
|
124
|
+
- tags define components: `<Rect />`, `<Plot />`, `<Shape />`
|
|
125
|
+
- props configure components: `at={[100, 80]}`, `label="$x$"`, `frame`
|
|
126
|
+
- children nest inside parents: ports inside shapes, ticks inside axes
|
|
127
|
+
- braces hold static values and simple arithmetic: `{[L / 2, 20]}`
|
|
128
|
+
- backtick strings support substitution: ``label={`$A^{[${site}]}$`}``
|
|
129
|
+
|
|
130
|
+
GraphSX is not a full JSX compiler. Values are parsed by a small safe parser, not by executing arbitrary JavaScript.
|
|
131
|
+
|
|
132
|
+
## Styling
|
|
133
|
+
|
|
134
|
+
Most visible components can be styled inline:
|
|
135
|
+
|
|
136
|
+
```jsx
|
|
137
|
+
<Rect id="A" style={{ fill: "#eef6ff", stroke: "#1d4ed8", strokeWidth: 2 }} />
|
|
138
|
+
<Link from="A.right" to="B.left" style={{ stroke: "#7c3aed", strokeWidth: 3 }} />
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Or with reusable style libraries:
|
|
142
|
+
|
|
143
|
+
```jsx
|
|
144
|
+
<Graph>
|
|
145
|
+
<Style id="tensor" fill="#6aa4d8" stroke="#111111" strokeWidth={3} />
|
|
146
|
+
<Style id="wire" stroke="#111111" strokeWidth={2.5} />
|
|
147
|
+
|
|
148
|
+
<Rect id="A" useStyle="tensor" />
|
|
149
|
+
<Link from="A.right" to="B.left" useStyle="wire" />
|
|
150
|
+
</Graph>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Inline `style` overrides `useStyle`. Style keys can use camelCase, such as `strokeWidth`; the renderer maps them to SVG attributes.
|
|
154
|
+
|
|
155
|
+
Plot-specific boxes also support dedicated style props, such as `frameStyle`, `boxStyle`, `textStyle`, and `labelStyle`.
|
|
156
|
+
|
|
157
|
+
## Graphs
|
|
158
|
+
|
|
159
|
+
`<Graph>` is the general figure canvas. It auto-sizes around its content and supports nodes, ports, links, paths, reusable shapes, repeats, and nested plots.
|
|
160
|
+
|
|
161
|
+
Canonical built-in graph nodes:
|
|
162
|
+
|
|
163
|
+
- `Rect`
|
|
164
|
+
- `Circle`
|
|
165
|
+
- `Point`, `Anchor`
|
|
166
|
+
- `Plot` when placed inside a graph
|
|
167
|
+
|
|
168
|
+
`Rect`, `Circle`, and nested `Plot` expose default ports:
|
|
169
|
+
|
|
170
|
+
```txt
|
|
171
|
+
A.left
|
|
172
|
+
A.right
|
|
173
|
+
A.top
|
|
174
|
+
A.bottom
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`Point` and `Anchor` expose:
|
|
178
|
+
|
|
179
|
+
```txt
|
|
180
|
+
J.center
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Ports
|
|
184
|
+
|
|
185
|
+
Ports can use side shorthand or custom local coordinates:
|
|
186
|
+
|
|
187
|
+
```jsx
|
|
188
|
+
<Rect id="A" at={[100, 100]} size={[120, 80]}>
|
|
189
|
+
<Port id="in" left />
|
|
190
|
+
<Port id="tap" at={[60, 20]} angle={35} label="$t$" />
|
|
191
|
+
</Rect>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`at` on a port is relative to its shape. `angle` controls the direction a routed link emits from or enters the port. `0` points right, `90` points down, `180` points left, and `-90` points up.
|
|
195
|
+
|
|
196
|
+
### Links
|
|
197
|
+
|
|
198
|
+
Links connect quoted port addresses:
|
|
199
|
+
|
|
200
|
+
```jsx
|
|
201
|
+
<Link headArrow from="A.right" to="B.left" />
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Routing options:
|
|
205
|
+
|
|
206
|
+
```jsx
|
|
207
|
+
<Graph route="auto" grid={20} padding={16} corner={8}>
|
|
208
|
+
<Rect id="A" at={[60, 100]} size={[90, 60]} />
|
|
209
|
+
<Rect id="Block" at={[210, 70]} size={[90, 110]} />
|
|
210
|
+
<Rect id="B" at={[380, 100]} size={[90, 60]} />
|
|
211
|
+
|
|
212
|
+
<Link headArrow from="A.right" to="B.left" />
|
|
213
|
+
<Link headArrow from="A.top" to="B.top" route="orthogonal" corner={0} />
|
|
214
|
+
</Graph>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Available routes:
|
|
218
|
+
|
|
219
|
+
- default curved route
|
|
220
|
+
- `route="straight"`
|
|
221
|
+
- `route="orthogonal"`
|
|
222
|
+
- `route="auto"` for first-pass obstacle avoidance
|
|
223
|
+
|
|
224
|
+
`route="auto"` avoids shape boxes. It does not yet optimize for edge crossings.
|
|
225
|
+
|
|
226
|
+
### Paths
|
|
227
|
+
|
|
228
|
+
Use `<Path>` for exact geometry rather than semantic port-to-port links:
|
|
229
|
+
|
|
230
|
+
```jsx
|
|
231
|
+
<Graph>
|
|
232
|
+
<Path points={[[90, 80], [90, 240], [180, 240]]} />
|
|
233
|
+
<Path points={[[120, 80], [170, 80], [170, 160]]} corner={6} headArrow />
|
|
234
|
+
<Path d="M 90 80 L 180 80" tailArrow arrowSize={8} />
|
|
235
|
+
</Graph>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`Path` accepts `points` or raw SVG `d`. Add `headArrow`, `tailArrow`, and `arrowSize` for arrows. `corner` rounds bends in point paths.
|
|
239
|
+
|
|
240
|
+
### Layout
|
|
241
|
+
|
|
242
|
+
Coordinates are optional when graph layout is enabled:
|
|
243
|
+
|
|
244
|
+
```jsx
|
|
245
|
+
<Graph layout="dag" direction="right" rankGap={200} nodeGap={90}>
|
|
246
|
+
<Rect id="A" size={[100, 60]} />
|
|
247
|
+
<Rect id="B" size={[100, 60]} />
|
|
248
|
+
<Rect id="C" size={[100, 60]} />
|
|
249
|
+
|
|
250
|
+
<Link headArrow from="A.right" to="B.left" />
|
|
251
|
+
<Link headArrow from="A.right" to="C.left" />
|
|
252
|
+
</Graph>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Supported layouts:
|
|
256
|
+
|
|
257
|
+
- `layout="row"`
|
|
258
|
+
- `layout="column"`
|
|
259
|
+
- `layout="dag"`
|
|
260
|
+
|
|
261
|
+
Explicit `at={[x, y]}` positions win over automatic layout.
|
|
262
|
+
|
|
263
|
+
## Reusable Shapes
|
|
264
|
+
|
|
265
|
+
Define custom shapes with `<Shape>`, then instantiate them by tag name:
|
|
266
|
+
|
|
267
|
+
```jsx
|
|
268
|
+
<Graph>
|
|
269
|
+
<Shape id="Tensor" groupBox={false}>
|
|
270
|
+
<Rect id="box" at={[0, 0]} size={[56, 56]} corner={8} label={`$A^{[${site}]}$`} />
|
|
271
|
+
<Port id="left" target="box.left" />
|
|
272
|
+
<Port id="right" target="box.right" />
|
|
273
|
+
<Port id="phys" target="box.bottom" />
|
|
274
|
+
</Shape>
|
|
275
|
+
|
|
276
|
+
<Repeat count={4} as="i" step={[100, 0]}>
|
|
277
|
+
<Tensor id={`A${i}`} at={[100, 100]} site={i} />
|
|
278
|
+
</Repeat>
|
|
279
|
+
|
|
280
|
+
<Repeat count={3} as="i">
|
|
281
|
+
<Link from={`A${i}.right`} to={`A${i+1}.left`} />
|
|
282
|
+
</Repeat>
|
|
283
|
+
</Graph>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Address paths use ids. `A0.left` is the public port on the `Tensor` instance. `A0.box.left` is the `left` port on the internal child rectangle.
|
|
287
|
+
|
|
288
|
+
Grouped shapes render a dashed group box by default. Use `groupBox={false}` on the shape definition or on an instance to hide it.
|
|
289
|
+
|
|
290
|
+
Shape variants can inherit from another shape:
|
|
291
|
+
|
|
292
|
+
```jsx
|
|
293
|
+
<Shape id="Gate" groupBox={false}>
|
|
294
|
+
<Rect id="box" size={[70, 40]} label={label} />
|
|
295
|
+
<Port id="in" target="box.left" />
|
|
296
|
+
<Port id="out" target="box.right" />
|
|
297
|
+
</Shape>
|
|
298
|
+
|
|
299
|
+
<Shape id="WideGate" from="Gate" w={110} label="$U$" />
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Derived shapes can override props, but not replace inherited children with the same id.
|
|
303
|
+
|
|
304
|
+
## Repeat
|
|
305
|
+
|
|
306
|
+
Use `<Repeat>` to expand repeated nodes, links, paths, or shape internals:
|
|
307
|
+
|
|
308
|
+
```jsx
|
|
309
|
+
<Graph>
|
|
310
|
+
<Repeat count={2} as="row" step={[0, 90]}>
|
|
311
|
+
<Repeat count={3} as="col" step={[100, 0]}>
|
|
312
|
+
<Rect id={`cell-${row}-${col}`} at={[100, 100]} size={[70, 50]} label={`cell ${row},${col}`} />
|
|
313
|
+
</Repeat>
|
|
314
|
+
</Repeat>
|
|
315
|
+
</Graph>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`step` offsets each repeated copy. Backtick strings can use `${i}`, `${i+1}`, `${row}`, `${col}`, and simple arithmetic.
|
|
319
|
+
|
|
320
|
+
## Plots
|
|
321
|
+
|
|
322
|
+
`<Plot>` can be used standalone:
|
|
323
|
+
|
|
324
|
+
```jsx
|
|
325
|
+
<Plot width={560} height={340} xDomain={[0, 2*pi]} yDomain={[-1.2, 1.2]} frame box>
|
|
326
|
+
<Data id="sin" y="sin(x)" domain={[0, 2*pi]} samples={160} />
|
|
327
|
+
|
|
328
|
+
<Axis x label="$x$">
|
|
329
|
+
<Ticks
|
|
330
|
+
values={[0, pi/2, pi, 3*pi/2, 2*pi]}
|
|
331
|
+
labels={["$0$", "$\pi/2$", "$\pi$", "$3\pi/2$", "$2\pi$"]}
|
|
332
|
+
grid
|
|
333
|
+
/>
|
|
334
|
+
</Axis>
|
|
335
|
+
<Axis y label="$\sin(x)$" ticks grid />
|
|
336
|
+
|
|
337
|
+
<Line data="sin" stroke="#2563eb" strokeWidth={2} label="$\sin(x)$" />
|
|
338
|
+
<Legend />
|
|
339
|
+
</Plot>
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Or inside a graph as a placed subplot:
|
|
343
|
+
|
|
344
|
+
```jsx
|
|
345
|
+
<Graph>
|
|
346
|
+
<Plot id="left" at={[0, 0]} width={320} height={220} frame box>
|
|
347
|
+
<Port id="out" right />
|
|
348
|
+
<Axis x ticks />
|
|
349
|
+
<Axis y ticks />
|
|
350
|
+
<Line points={[[0, 1], [1, 2], [2, 4]]} />
|
|
351
|
+
</Plot>
|
|
352
|
+
|
|
353
|
+
<Plot id="right" at={left.right + [90, 0]} width={320} height={220} frame box>
|
|
354
|
+
<Port id="in" left />
|
|
355
|
+
<Axis x ticks />
|
|
356
|
+
<Axis y ticks />
|
|
357
|
+
<Line points={[[0, 4], [1, 2], [2, 1]]} />
|
|
358
|
+
</Plot>
|
|
359
|
+
|
|
360
|
+
<Link from="left.out" to="right.in" headArrow />
|
|
361
|
+
</Graph>
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
In a plot:
|
|
365
|
+
|
|
366
|
+
- `frame` draws the outer plot panel
|
|
367
|
+
- `box` draws the inner axis/data box
|
|
368
|
+
- `padding` controls the gap between frame and axes
|
|
369
|
+
- `xDomain` and `yDomain` set data coordinates
|
|
370
|
+
- `Data` can hold points, x/y arrays, or generated math data
|
|
371
|
+
- generated `Data` always stores point fields named `x` and `y`
|
|
372
|
+
- parametric data uses `x="..."` and `y="..."`; the default sampling variable is `t`
|
|
373
|
+
- `Line`, `Curve`, `Mark`, `Scatter`, `Text`, `Legend`, and annotation shapes render on top
|
|
374
|
+
- generated data expressions may be complex; plotted coordinates use the real part by default
|
|
375
|
+
|
|
376
|
+
Parametric curves sample a variable and evaluate both coordinates:
|
|
377
|
+
|
|
378
|
+
```jsx
|
|
379
|
+
<Plot width={500} height={460} xDomain={[-18, 18]} yDomain={[-18, 14]} frame box>
|
|
380
|
+
<Data
|
|
381
|
+
id="heart"
|
|
382
|
+
x="16 * pow(sin(t), 3)"
|
|
383
|
+
y="13*cos(t) - 5*cos(2*t) - 2*cos(3*t) - cos(4*t)"
|
|
384
|
+
domain={[0, 2*pi]}
|
|
385
|
+
samples={420}
|
|
386
|
+
/>
|
|
387
|
+
<Line data="heart" stroke="#e11d48" strokeWidth={2.8} />
|
|
388
|
+
</Plot>
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
For `y="..."` alone, the default sampling variable is `x`, and the sampled domain value is stored as the point's `x` field. For `x="..."` plus `y="..."`, the default sampling variable is `t`; both expressions are evaluated from that variable and stored as point fields named `x` and `y`. Use `variable="theta"` to override either default.
|
|
392
|
+
|
|
393
|
+
```jsx
|
|
394
|
+
<Data id="phase" y="exp(1j*t)" variable="t" domain={[0, 2*pi]} />
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
This stores points like `{ x: t, y: exp(1j*t) }`.
|
|
398
|
+
|
|
399
|
+
Complex math uses Python-style imaginary literals such as `1j`, `2.5j`, and `x + 3j`. Bare `j` is just a normal variable name, so `exp(j*x)` only works if `j` is declared in `params`.
|
|
400
|
+
|
|
401
|
+
```jsx
|
|
402
|
+
<Plot width={560} height={340} xDomain={[-1, 1]} yDomain={[-1.1, 1.1]} frame box>
|
|
403
|
+
<Data id="root" y="sqrt(x)" domain={[-1, 1]} samples={200} />
|
|
404
|
+
|
|
405
|
+
<Axis x label="$x$" ticks grid />
|
|
406
|
+
<Axis y label="$\sqrt{x}$" ticks grid />
|
|
407
|
+
|
|
408
|
+
<Line data="root" label="real" />
|
|
409
|
+
<Line data="root" yMap="imag(y)" stroke="#dc2626" label="imag" />
|
|
410
|
+
<Line data="root" yMap="abs(y)" stroke="#16a34a" label="abs" />
|
|
411
|
+
<Legend />
|
|
412
|
+
</Plot>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Use `xMap` and `yMap` to transform stored points at plot time. The map scope contains the stored `x` and `y` values, which may be real or complex. Either map can use either stored field:
|
|
416
|
+
|
|
417
|
+
```jsx
|
|
418
|
+
<Plot width={420} height={420} xDomain={[-1.2, 1.2]} yDomain={[-1.2, 1.2]} frame box>
|
|
419
|
+
<Data id="phase" y="exp(1j*t)" variable="t" domain={[0, 2*pi]} samples={240} />
|
|
420
|
+
|
|
421
|
+
<Line data="phase" xMap="real(y)" yMap="imag(y)" label="unit circle" />
|
|
422
|
+
</Plot>
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Plot annotations can use `Rect`, `Circle`, `Anchor`, `Link`, and `Path`:
|
|
426
|
+
|
|
427
|
+
```jsx
|
|
428
|
+
<Plot width={500} height={300} xDomain={[0, 5]} yDomain={[0, 10]} frame box>
|
|
429
|
+
<Line points={[[0, 1], [2, 4], [5, 9]]} />
|
|
430
|
+
<Circle id="peak" at={[5, 9]} r={5} fill="#ef4444" />
|
|
431
|
+
<Rect id="note" at={[3.2, 8.2]} size={[90, 28]} label="peak">
|
|
432
|
+
<Port id="tip" left />
|
|
433
|
+
</Rect>
|
|
434
|
+
<Link from="note.tip" to="peak.top" headArrow />
|
|
435
|
+
</Plot>
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Annotation coordinates default to data coordinates. Use `atUnit="screen"` for screen/SVG coordinates.
|
|
439
|
+
|
|
440
|
+
## Supported Tags
|
|
441
|
+
|
|
442
|
+
Common top-level tags:
|
|
443
|
+
|
|
444
|
+
| Tag | Children | Key props |
|
|
445
|
+
| --- | --- | --- |
|
|
446
|
+
| `Graph` | `Style`, `Shape`, nodes, `Plot`, `Link`, `Path`, `Repeat` | `route`, `layout`, `direction`, `grid`, `padding`, `corner` |
|
|
447
|
+
| `Plot` | plot children, and `Port` when nested in `Graph` | `width`, `height`, `padding`, `xDomain`, `yDomain`, `frame`, `box` |
|
|
448
|
+
|
|
449
|
+
Graph tags:
|
|
450
|
+
|
|
451
|
+
| Tag | Children | Key props |
|
|
452
|
+
| --- | --- | --- |
|
|
453
|
+
| `Rect` | `Port` | `id`, `at`, `size`, `w`, `h`, `label`, `corner`, `anchor`, `origin`, `rotate`, `flipX`, `flipY`, `style`, `useStyle` |
|
|
454
|
+
| `Circle` | `Port` | `id`, `at`, `r`, `label`, `anchor`, `origin`, `rotate`, `flipX`, `flipY`, `style`, `useStyle` |
|
|
455
|
+
| `Point`, `Anchor` | `Port` | `id`, `at`, `label`, `r`, `style`, `useStyle` |
|
|
456
|
+
| `Port`, `Leg` | none | `id`, `left`, `right`, `top`, `bottom`, `side`, `at`, `angle`, `target`, `label`, `r`, `style`, `useStyle` |
|
|
457
|
+
| `Link` | none | `from`, `to`, `route`, `corner`, `stub`, `headArrow`, `tailArrow`, `arrowSize`, `style`, `useStyle` |
|
|
458
|
+
| `Path` | none | `points`, `d`, `at`, `corner`, `closed`, `headArrow`, `tailArrow`, `arrowSize`, `rotate`, `flipX`, `flipY`, `style`, `useStyle` |
|
|
459
|
+
| `Shape` | graph nodes, `Port`, `Link`, `Path`, `Repeat` | `id`, `from`, `groupBox`, user-defined props |
|
|
460
|
+
| `Repeat` | repeatable graph children | `count`, `as`, `step` |
|
|
461
|
+
| `Style` | none | `id`, SVG style props |
|
|
462
|
+
|
|
463
|
+
Plot tags:
|
|
464
|
+
|
|
465
|
+
| Tag | Children | Key props |
|
|
466
|
+
| --- | --- | --- |
|
|
467
|
+
| `Data` | none | `id`, `points`, `x`, `y`, `domain`, `samples`, `params`, `variable` |
|
|
468
|
+
| `Axis` | `Ticks` | `x`, `y`, `label`, `ticks`, `grid`, `labelGap`, `tickLabelGap`, `tickSize`, `style`, `labelStyle` |
|
|
469
|
+
| `Ticks` | none | `values`, `labels`, `grid`, `labelGap`, `tickLabelGap`, `tickSize`, `style`, `labelStyle` |
|
|
470
|
+
| `Line`, `Curve` | none | `data`, `points`, `x`, `y`, `xMap`, `yMap`, `fmt`, `label`, `animate`, `stroke`, `strokeWidth`, `fill`, `r`, `style`, `useStyle` |
|
|
471
|
+
| `Mark`, `Scatter` | none | `data`, `at`, `points`, `x`, `y`, `xMap`, `yMap`, `r`, `label`, `animate`, `fill`, `stroke`, `style`, `useStyle` |
|
|
472
|
+
| `Text`, `Label` | none | `at`, `label`, `fontSize`, `anchor`, `baseline`, `rotate`, `style`, `useStyle` |
|
|
473
|
+
| `Legend` | none | `position`, `box`, `padding`, `margin`, `fontSize`, `textStyle`, `boxStyle` |
|
|
474
|
+
|
|
475
|
+
Canonical tags use PascalCase. A few aliases are accepted for convenience, but new examples should prefer the canonical form:
|
|
476
|
+
|
|
477
|
+
| Canonical | Aliases |
|
|
478
|
+
| --- | --- |
|
|
479
|
+
| `Rect` | `rect`, `Rec`, `rec` |
|
|
480
|
+
| `Circle` | `circle`, `Circ`, `circ` |
|
|
481
|
+
| `Point` | `point` |
|
|
482
|
+
| `Anchor` | `anchor` |
|
|
483
|
+
| `Path` | `path` |
|
|
484
|
+
| `Data` | `Dataset` |
|
|
485
|
+
| `Axis` | `XAxis`, `YAxis` |
|
|
486
|
+
| `Ticks` | `ticks` |
|
|
487
|
+
| `Curve` | `Series` |
|
|
488
|
+
| `Legend` | `legend` |
|
|
489
|
+
|
|
490
|
+
## Markdown
|
|
491
|
+
|
|
492
|
+
GraphSX works in Markdown with a `markdown-it` plugin:
|
|
493
|
+
|
|
494
|
+
```js
|
|
495
|
+
import MarkdownIt from "markdown-it";
|
|
496
|
+
import katex from "katex";
|
|
497
|
+
import { graphsxMarkdownIt, renderGraphSXBlocks } from "@slxu/graphsx";
|
|
498
|
+
import "@slxu/graphsx/markdown.css";
|
|
499
|
+
|
|
500
|
+
const md = new MarkdownIt().use(graphsxMarkdownIt);
|
|
501
|
+
const preview = document.querySelector("#preview");
|
|
502
|
+
|
|
503
|
+
preview.innerHTML = md.render(markdownSource);
|
|
504
|
+
renderGraphSXBlocks(preview, { katex });
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Authors use `graphsx` fences:
|
|
508
|
+
|
|
509
|
+
````md
|
|
510
|
+
```graphsx
|
|
511
|
+
<Graph>
|
|
512
|
+
<Rect id="A" />
|
|
513
|
+
<Rect id="B" at={[220, 0]} />
|
|
514
|
+
<Link headArrow from="A.right" to="B.left" />
|
|
515
|
+
</Graph>
|
|
516
|
+
```
|
|
517
|
+
````
|
|
518
|
+
|
|
519
|
+
Reusable style/shape libraries can be declared with hidden `graphsx-defs` fences:
|
|
520
|
+
|
|
521
|
+
````md
|
|
522
|
+
```graphsx-defs theme
|
|
523
|
+
<Style id="tensor" fill="#6aa4d8" stroke="#111111" />
|
|
524
|
+
<Style id="wire" stroke="#111111" strokeWidth={2.5} />
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
```graphsx use="theme"
|
|
528
|
+
<Graph>
|
|
529
|
+
<Rect id="A" useStyle="tensor" />
|
|
530
|
+
<Rect id="B" at={[120, 0]} useStyle="tensor" />
|
|
531
|
+
<Link from="A.right" to="B.left" useStyle="wire" />
|
|
532
|
+
</Graph>
|
|
533
|
+
```
|
|
534
|
+
````
|
|
535
|
+
|
|
536
|
+
Multiple library names can be separated by spaces or commas.
|
|
537
|
+
|
|
538
|
+
## CodeMirror Live Preview
|
|
539
|
+
|
|
540
|
+
Use the CodeMirror extension to render `graphsx` fences as editable live widgets inside a Markdown editor:
|
|
541
|
+
|
|
542
|
+
```js
|
|
543
|
+
import { EditorView, basicSetup } from "codemirror";
|
|
544
|
+
import { markdown } from "@codemirror/lang-markdown";
|
|
545
|
+
import { jsxLanguage } from "@codemirror/lang-javascript";
|
|
546
|
+
import { graphsxCodeMirrorLivePreview } from "@slxu/graphsx/codemirror";
|
|
547
|
+
import "@slxu/graphsx/codemirror.css";
|
|
548
|
+
|
|
549
|
+
new EditorView({
|
|
550
|
+
doc,
|
|
551
|
+
extensions: [
|
|
552
|
+
basicSetup,
|
|
553
|
+
markdown({
|
|
554
|
+
codeLanguages: (info) => {
|
|
555
|
+
const name = info.trim().split(/\s+/)[0];
|
|
556
|
+
return name === "graphsx" || name === "graphsx-defs" ? jsxLanguage : null;
|
|
557
|
+
}
|
|
558
|
+
}),
|
|
559
|
+
graphsxCodeMirrorLivePreview({ katex })
|
|
560
|
+
],
|
|
561
|
+
parent: document.querySelector("#editor")
|
|
562
|
+
});
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
When the cursor is outside a `graphsx` fence, the fence renders as an SVG widget. Clicking the widget moves the cursor into the original fenced code. `graphsx-defs` fences render as compact library markers and feed reusable shapes/styles to later fences.
|
|
566
|
+
|
|
567
|
+
## Development
|
|
568
|
+
|
|
569
|
+
```bash
|
|
570
|
+
npm install
|
|
571
|
+
npm test
|
|
572
|
+
npm run playground
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
The playground runs at the Vite URL printed in your terminal, usually `http://127.0.0.1:5173/`.
|
|
576
|
+
|
|
577
|
+
Build the GitHub Pages version:
|
|
578
|
+
|
|
579
|
+
```bash
|
|
580
|
+
npm run build:pages
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## Notes
|
|
584
|
+
|
|
585
|
+
- Graph labels and plot labels support KaTeX when a `katex` instance is passed to the renderer.
|
|
586
|
+
- Graph `<Plot>` nodes behave like rectangles: they have `at`, `width`, `height`, and default side ports.
|
|
587
|
+
- Standalone `<Plot>` is best for one plot. Use `<Graph>` when you want subplots or a mixed figure.
|
|
588
|
+
- The parser intentionally supports a constrained JSX-like subset rather than arbitrary JavaScript.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg viewBox="62 62 286 116" width="286" height="116" xmlns="http://www.w3.org/2000/svg"><style>
|
|
2
|
+
.shape { fill: #ffffff; stroke: #26312d; stroke-width: 2; }
|
|
3
|
+
.node-label { font: 700 14px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1e2724; pointer-events: none; }
|
|
4
|
+
.leg-label { font: 11px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #52605a; pointer-events: none; }
|
|
5
|
+
.edge { fill: none; stroke: #2d6cdf; stroke-width: 2.5; }
|
|
6
|
+
.path { fill: none; stroke: #111111; stroke-width: 2; }
|
|
7
|
+
.leg-dot { fill: #16846f; stroke: #ffffff; stroke-width: 2; }
|
|
8
|
+
.plot-frame, .plot-box { fill: none; stroke: #26312d; stroke-width: 1.8; }
|
|
9
|
+
.plot-axis { stroke: #26312d; stroke-width: 1.8; }
|
|
10
|
+
.plot-tick { stroke: #26312d; stroke-width: 1.3; }
|
|
11
|
+
.plot-grid { stroke: #d8ded8; stroke-width: 1; }
|
|
12
|
+
.plot-line, .plot-curve { fill: none; stroke: #2563eb; stroke-width: 2; }
|
|
13
|
+
.plot-line-marker { fill: #ffffff; stroke: #2563eb; stroke-width: 2; }
|
|
14
|
+
.plot-axis-label, .plot-tick-label, .plot-label { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #1e2724; }
|
|
15
|
+
</style><defs><marker id="graphsx-arrow-head" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="strokeWidth"><path d="M 2 2 L 10 6 L 2 10 z" fill="context-stroke"></path></marker><marker id="graphsx-arrow-tail" markerWidth="12" markerHeight="12" refX="2" refY="6" orient="auto" markerUnits="strokeWidth"><path d="M 10 2 L 2 6 L 10 10 z" fill="context-stroke"></path></marker></defs><g><path class="edge" marker-end="url(#graphsx-arrow-head)" d="M 180 120 C 228 120, 202 120, 250 120" style="stroke: #7c3aed; stroke-width: 3"></path></g><g></g><g><g><rect class="shape" x="80" y="90" width="100" height="60" rx="6" style="fill: #eef6ff; stroke: #1d4ed8; stroke-width: 2"></rect><text class="node-label" x="130" y="124" text-anchor="middle">alpha</text><g><circle class="leg-dot" cx="180" cy="120" r="5"></circle><text class="leg-label" x="190" y="114" text-anchor="start">xy</text></g></g><g><circle class="shape" cx="290" cy="120" r="40"></circle><text class="node-label" x="290" y="124" text-anchor="middle">B</text><g><circle class="leg-dot" cx="250" cy="120" r="5"></circle></g></g></g></svg>
|