compostjs 0.0.12 → 0.2.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.
@@ -0,0 +1,25 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-dotnet@v4
16
+ with:
17
+ dotnet-version: '8.0.x'
18
+
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '22'
22
+
23
+ - run: dotnet tool restore
24
+ - run: npm install
25
+ - run: npm test
package/CLAUDE.md ADDED
@@ -0,0 +1,78 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Build Commands
6
+
7
+ Prerequisites: .NET SDK 8+, Node.js, npm. Fable is installed as a dotnet local tool (`.config/dotnet-tools.json`).
8
+
9
+ ```bash
10
+ dotnet tool restore # Install Fable 4.28.0
11
+ npm install # Install Vite + virtual-dom
12
+
13
+ npm start # Dev server at http://localhost:8080 (Fable watch + Vite)
14
+ npm run build # Compile F# to ES modules in dist/ (for npm package)
15
+ npm run rebuild # Clean dist/ and rebuild
16
+ npm run standalone # Bundle standalone IIFE to docs/releases/
17
+ npm test # Compile F# then run Vitest tests (test/*.test.js)
18
+
19
+ npm run release # Full release (runs the three steps below)
20
+ npm run release:version # Bump version in package.json, create git commit + tag (via np)
21
+ npm run release:standalone # Build standalone bundle and commit to git
22
+ npm run release:publish # Rebuild dist/ and npm publish (prompts for 2FA)
23
+ ```
24
+
25
+ Tests use Vitest against the Fable-compiled JS output. F# must be compiled before tests run (`npm test` handles this). Test files go in `test/`. Use `c.foldDom(f, acc, node)` to traverse `DomNode` trees in tests — the callback receives `(acc, tag, attrs)` where `attrs` is a plain JS object of string attribute values. CI runs `npm test` on pushes and PRs to `master` via GitHub Actions (`.github/workflows/test.yml`).
26
+
27
+ ## Architecture
28
+
29
+ Compost.js is a composable data visualization library. The core is written in **F#** and compiled to **JavaScript** via **Fable 4**. The runtime dependency is `virtual-dom` for efficient DOM updates in interactive charts.
30
+
31
+ ### F# Source (`src/compost/`)
32
+
33
+ Three files, compiled in this order (defined in `compost.fsproj`):
34
+
35
+ 1. **`html.fs`** — `Compost.Html` module. Low-level JS interop layer:
36
+ - `Common` module: `[<Emit>]`-based helpers for JS operations (property access, typeof, date formatting)
37
+ - `Virtualdom` module: `[<Import>]` bindings to `virtual-dom` (h, diff, patch, createElement)
38
+ - `DomNode`/`DomAttribute` types and HTML/SVG rendering
39
+ - `foldDom`: generic fold over `DomNode` trees (used in tests to inspect rendered SVG)
40
+ - `createVirtualDomApp`: stateful interactive app loop using virtual-dom diffing
41
+
42
+ 2. **`core.fs`** — `Compost` namespace. The visualization engine (~800 lines):
43
+ - **Domain types**: `Value<'u>` (continuous `COV` or categorical `CAR`), `Scale<'v>`, `Shape<'vx,'vy>`, `Style`, `EventHandler`
44
+ - **Shape** is a discriminated union (Line, Bubble, Shape, Text, Image, Layered, Interactive, Axes, NestX/Y, Padding, etc.) — this is the core composable DSL
45
+ - Uses F# units of measure (`[<Measure>]`) for type-safe coordinate spaces
46
+ - `Scales` module: axis generation, range calculation
47
+ - `Projections` module: coordinate transformation (data space → pixel space)
48
+ - `Drawing` module: converts shapes → SVG elements
49
+ - `Events` module: routes mouse/touch events to interactive handlers
50
+ - `Derived` module: higher-level combinators (FillColor, StrokeColor, Column, Bar, Area)
51
+
52
+ 3. **`compost.fs`** — `main` module. The JavaScript API surface:
53
+ - Exposes `scale` (JsScale) and `compost` (JsCompost) objects
54
+ - Handles JS↔F# value conversion via `parseValue`/`formatValue` (numbers become `COV`, `[string, number]` arrays become `CAR`)
55
+ - All ~20 API methods (`render`, `interactive`, `overlay`, `axes`, `on`, etc.) are defined here
56
+
57
+ ### JavaScript Entry Points (`src/project/`)
58
+
59
+ - **`standalone.js`** — Imports `scale`/`compost` from F# and assigns to `window.s`/`window.c`
60
+ - **`demos.js`** — Interactive demo examples used by the dev server (`index.html`)
61
+ - **`data.js`** — Demo datasets (elections, exchange rates, iris)
62
+
63
+ ### Build Outputs
64
+
65
+ - **`dist/`** — ES modules for npm (`compost.js` is the entry point, `"main"` in package.json)
66
+ - **`docs/releases/`** — Standalone IIFE bundles for `<script>` tag usage
67
+
68
+ ### How Fable Compilation Works
69
+
70
+ `dotnet fable` compiles `.fs` files to `.fs.js` files. During `npm start` (watch mode), these appear next to the source files (`src/compost/*.fs.js`). During `npm run build`, they go to `dist/` via the `-o` flag. Fable library dependencies go into `fable_modules/`. Vite (or any bundler) then treats the `.js` output as standard ES modules.
71
+
72
+ ## Key Patterns
73
+
74
+ - The JS API uses `obj` and `box`/`unbox` heavily for dynamic typing at the JS boundary. The F# internals are fully typed with units of measure.
75
+ - `[<Emit("...")>]` is used for inline JS expressions (property access, typeof, Date operations).
76
+ - `[<Import("name","module")>]` is used for virtual-dom imports.
77
+ - `JsInterop.createObj` creates plain JS objects from F# key-value sequences.
78
+ - Interactive charts use a virtual-dom update cycle: event → update function → render function → diff/patch.
package/README.md CHANGED
@@ -33,6 +33,20 @@ JavaScript file that is added to the `releases` folder of the `docs` with the cu
33
33
  version number in the filename (and also updates the `latest` file).
34
34
  This should all happen automatically when using `npm run release`.
35
35
 
36
+ ### Releasing Compost
37
+
38
+ Before releasing, make sure all changes are committed. Then run:
39
+
40
+ ```
41
+ npm run release
42
+ ```
43
+
44
+ This runs three steps in order:
45
+
46
+ 1. **`npm run release:version`** — bumps the version in `package.json` and creates a git commit and tag (via `np`)
47
+ 2. **`npm run release:standalone`** — builds the standalone bundle and commits it to git
48
+ 3. **`npm run release:publish`** — rebuilds `dist/` and publishes to npm (prompts for 2FA)
49
+
36
50
  ## What is the story behind the name??
37
51
 
38
52
  ![Compost](https://github.com/compostjs/compost/raw/master/compost.gif)
package/dist/compost.js CHANGED
@@ -1,10 +1,10 @@
1
- import { Compost_createSvg, EventHandler, HorizontalAlign, VerticalAlign, Derived_Bar, Derived_Column, Derived_Font, Derived_StrokeColor, Derived_FillColor, Shape, Scale, categorical, Value, continuous } from "./core.js";
2
- import { printf, toFail } from "./fable_modules/fable-library-js.4.28.0/String.js";
1
+ import { Compost_createSvg, EventHandler, Derived_Bar, Derived_Column, Derived_Font, Derived_StrokeColor, Derived_PreserveAspectRatio, Derived_FillColor, Shape, StyleConfig, Scale, HorizontalAlign, VerticalAlign, categorical, Value, continuous } from "./core.js";
2
+ import { join, printf, toFail } from "./fable_modules/fable-library-js.4.28.0/String.js";
3
3
  import { map as map_1, item } from "./fable_modules/fable-library-js.4.28.0/Array.js";
4
- import { toList, map, delay, toArray } from "./fable_modules/fable-library-js.4.28.0/Seq.js";
5
- import { ofArray } from "./fable_modules/fable-library-js.4.28.0/List.js";
6
- import { defaultOf, equals } from "./fable_modules/fable-library-js.4.28.0/Util.js";
7
- import { renderTo, createVirtualDomApp, DomNode, DomAttribute } from "./html.js";
4
+ import { empty, append, toList, map, singleton, collect, delay, toArray } from "./fable_modules/fable-library-js.4.28.0/Seq.js";
5
+ import { ofArray, ofSeq } from "./fable_modules/fable-library-js.4.28.0/List.js";
6
+ import { createObj, equals, defaultOf } from "./fable_modules/fable-library-js.4.28.0/Util.js";
7
+ import { foldDom, renderTo, createVirtualDomApp, DomNode, DomAttribute } from "./html.js";
8
8
 
9
9
  export function Helpers_formatValue(v) {
10
10
  if (v.tag === 1) {
@@ -34,6 +34,282 @@ export function Helpers_parseValue(v) {
34
34
  }
35
35
  }
36
36
 
37
+ function Serialization_serValue() {
38
+ return Helpers_formatValue;
39
+ }
40
+
41
+ function Serialization_deserValue() {
42
+ return Helpers_parseValue;
43
+ }
44
+
45
+ function Serialization_serializeVerticalAlign(_arg) {
46
+ switch (_arg.tag) {
47
+ case 1:
48
+ return "middle";
49
+ case 2:
50
+ return "hanging";
51
+ default:
52
+ return "baseline";
53
+ }
54
+ }
55
+
56
+ function Serialization_deserializeVerticalAlign(_arg) {
57
+ switch (_arg) {
58
+ case "baseline":
59
+ return new VerticalAlign(0, []);
60
+ case "hanging":
61
+ return new VerticalAlign(2, []);
62
+ default:
63
+ return new VerticalAlign(1, []);
64
+ }
65
+ }
66
+
67
+ function Serialization_serializeHorizontalAlign(_arg) {
68
+ switch (_arg.tag) {
69
+ case 1:
70
+ return "center";
71
+ case 2:
72
+ return "end";
73
+ default:
74
+ return "start";
75
+ }
76
+ }
77
+
78
+ function Serialization_deserializeHorizontalAlign(_arg) {
79
+ switch (_arg) {
80
+ case "start":
81
+ return new HorizontalAlign(0, []);
82
+ case "end":
83
+ return new HorizontalAlign(2, []);
84
+ default:
85
+ return new HorizontalAlign(1, []);
86
+ }
87
+ }
88
+
89
+ function Serialization_serializeScale(_arg) {
90
+ if (_arg.tag === 1) {
91
+ return {
92
+ kind: "categorical",
93
+ cats: toArray(delay(() => collect((matchValue) => singleton(matchValue.fields[0]), _arg.fields[0]))),
94
+ };
95
+ }
96
+ else {
97
+ return {
98
+ kind: "continuous",
99
+ lo: _arg.fields[0].fields[0],
100
+ hi: _arg.fields[1].fields[0],
101
+ };
102
+ }
103
+ }
104
+
105
+ function Serialization_deserializeScale(o) {
106
+ const matchValue = o["kind"];
107
+ switch (matchValue) {
108
+ case "continuous":
109
+ return new Scale(0, [new continuous(o["lo"]), new continuous(o["hi"])]);
110
+ case "categorical":
111
+ return new Scale(1, [toArray(delay(() => map((c) => (new categorical(c)), o["cats"])))]);
112
+ default:
113
+ return toFail(printf("Unknown scale kind: %s"))(matchValue);
114
+ }
115
+ }
116
+
117
+ function Serialization_serializeStyleConfig(_arg) {
118
+ switch (_arg.tag) {
119
+ case 2:
120
+ return {
121
+ kind: "stroke",
122
+ color: _arg.fields[0],
123
+ };
124
+ case 3:
125
+ return {
126
+ kind: "font",
127
+ font: _arg.fields[0],
128
+ color: _arg.fields[1],
129
+ };
130
+ case 4:
131
+ return {
132
+ kind: "aspect",
133
+ value: _arg.fields[0],
134
+ };
135
+ case 0:
136
+ throw new Error("Cannot serialize Custom style config");
137
+ default:
138
+ return {
139
+ kind: "fill",
140
+ color: _arg.fields[0],
141
+ };
142
+ }
143
+ }
144
+
145
+ function Serialization_deserializeStyleConfig(o) {
146
+ const matchValue = o["kind"];
147
+ switch (matchValue) {
148
+ case "fill":
149
+ return new StyleConfig(1, [o["color"]]);
150
+ case "stroke":
151
+ return new StyleConfig(2, [o["color"]]);
152
+ case "font":
153
+ return new StyleConfig(3, [o["font"], o["color"]]);
154
+ case "aspect":
155
+ return new StyleConfig(4, [o["value"]]);
156
+ default:
157
+ return toFail(printf("Unknown style config kind: %s"))(matchValue);
158
+ }
159
+ }
160
+
161
+ function Serialization_serializePoints(pts) {
162
+ return toArray(delay(() => collect((matchValue) => singleton([Serialization_serValue()(matchValue[0]), Serialization_serValue()(matchValue[1])]), pts)));
163
+ }
164
+
165
+ export function Serialization_serializeShape(s) {
166
+ switch (s.tag) {
167
+ case 9:
168
+ return {
169
+ kind: "shape",
170
+ points: Serialization_serializePoints(ofSeq(s.fields[0])),
171
+ };
172
+ case 8:
173
+ return {
174
+ kind: "bubble",
175
+ x: Serialization_serValue()(s.fields[0]),
176
+ y: Serialization_serValue()(s.fields[1]),
177
+ w: s.fields[2],
178
+ h: s.fields[3],
179
+ };
180
+ case 2:
181
+ return {
182
+ kind: "text",
183
+ x: Serialization_serValue()(s.fields[0]),
184
+ y: Serialization_serValue()(s.fields[1]),
185
+ valign: Serialization_serializeVerticalAlign(s.fields[2]),
186
+ halign: Serialization_serializeHorizontalAlign(s.fields[3]),
187
+ rotation: s.fields[4],
188
+ text: s.fields[5],
189
+ };
190
+ case 0:
191
+ return {
192
+ kind: "image",
193
+ href: s.fields[0],
194
+ p1: [Serialization_serValue()(s.fields[1][0]), Serialization_serValue()(s.fields[1][1])],
195
+ p2: [Serialization_serValue()(s.fields[2][0]), Serialization_serValue()(s.fields[2][1])],
196
+ };
197
+ case 10:
198
+ return {
199
+ kind: "layered",
200
+ shapes: toArray(delay(() => map(Serialization_serializeShape, s.fields[0]))),
201
+ };
202
+ case 11:
203
+ return {
204
+ kind: "axes",
205
+ axes: join(" ", toList(delay(() => append(s.fields[0] ? singleton("top") : empty(), delay(() => append(s.fields[1] ? singleton("right") : empty(), delay(() => append(s.fields[2] ? singleton("bottom") : empty(), delay(() => (s.fields[3] ? singleton("left") : empty())))))))))),
206
+ shape: Serialization_serializeShape(s.fields[4]),
207
+ };
208
+ case 13:
209
+ return {
210
+ kind: "padding",
211
+ top: s.fields[0][0],
212
+ right: s.fields[0][1],
213
+ bottom: s.fields[0][2],
214
+ left: s.fields[0][3],
215
+ shape: Serialization_serializeShape(s.fields[1]),
216
+ };
217
+ case 5:
218
+ return {
219
+ kind: "nestx",
220
+ lx: Serialization_serValue()(s.fields[0]),
221
+ hx: Serialization_serValue()(s.fields[1]),
222
+ shape: Serialization_serializeShape(s.fields[2]),
223
+ };
224
+ case 6:
225
+ return {
226
+ kind: "nesty",
227
+ ly: Serialization_serValue()(s.fields[0]),
228
+ hy: Serialization_serValue()(s.fields[1]),
229
+ shape: Serialization_serializeShape(s.fields[2]),
230
+ };
231
+ case 4: {
232
+ const sy = s.fields[1];
233
+ const sx = s.fields[0];
234
+ return {
235
+ kind: "scale",
236
+ sx: (sx == null) ? defaultOf() : Serialization_serializeScale(sx),
237
+ sy: (sy == null) ? defaultOf() : Serialization_serializeScale(sy),
238
+ shape: Serialization_serializeShape(s.fields[2]),
239
+ };
240
+ }
241
+ case 1:
242
+ return {
243
+ kind: "styled",
244
+ param: Serialization_serializeStyleConfig(s.fields[0]),
245
+ shape: Serialization_serializeShape(s.fields[1]),
246
+ };
247
+ case 3:
248
+ return {
249
+ kind: "autoscale",
250
+ x: s.fields[0],
251
+ y: s.fields[1],
252
+ shape: Serialization_serializeShape(s.fields[2]),
253
+ };
254
+ case 14:
255
+ return {
256
+ kind: "offset",
257
+ dx: s.fields[0][0],
258
+ dy: s.fields[0][1],
259
+ shape: Serialization_serializeShape(s.fields[1]),
260
+ };
261
+ case 12:
262
+ throw new Error("Cannot serialize Interactive shapes");
263
+ default:
264
+ return {
265
+ kind: "line",
266
+ points: Serialization_serializePoints(ofSeq(s.fields[0])),
267
+ };
268
+ }
269
+ }
270
+
271
+ export function Serialization_deserializeShape(o) {
272
+ const matchValue = o["kind"];
273
+ switch (matchValue) {
274
+ case "line":
275
+ return new Shape(7, [toList(delay(() => map((p) => [Serialization_deserValue()(item(0, p)), Serialization_deserValue()(item(1, p))], o["points"])))]);
276
+ case "shape":
277
+ return new Shape(9, [toList(delay(() => map((p_1) => [Serialization_deserValue()(item(0, p_1)), Serialization_deserValue()(item(1, p_1))], o["points"])))]);
278
+ case "bubble":
279
+ return new Shape(8, [Serialization_deserValue()(o["x"]), Serialization_deserValue()(o["y"]), o["w"], o["h"]]);
280
+ case "text":
281
+ return new Shape(2, [Serialization_deserValue()(o["x"]), Serialization_deserValue()(o["y"]), Serialization_deserializeVerticalAlign(o["valign"]), Serialization_deserializeHorizontalAlign(o["halign"]), o["rotation"], o["text"]]);
282
+ case "image": {
283
+ const matchValue_1 = o["p1"];
284
+ const p2 = o["p2"];
285
+ const p1 = matchValue_1;
286
+ return new Shape(0, [o["href"], [Serialization_deserValue()(item(0, p1)), Serialization_deserValue()(item(1, p1))], [Serialization_deserValue()(item(0, p2)), Serialization_deserValue()(item(1, p2))]]);
287
+ }
288
+ case "layered":
289
+ return new Shape(10, [toList(delay(() => map(Serialization_deserializeShape, o["shapes"])))]);
290
+ case "axes": {
291
+ const a = o["axes"];
292
+ return new Shape(11, [a.indexOf("top") >= 0, a.indexOf("right") >= 0, a.indexOf("bottom") >= 0, a.indexOf("left") >= 0, Serialization_deserializeShape(o["shape"])]);
293
+ }
294
+ case "padding":
295
+ return new Shape(13, [[o["top"], o["right"], o["bottom"], o["left"]], Serialization_deserializeShape(o["shape"])]);
296
+ case "nestx":
297
+ return new Shape(5, [Serialization_deserValue()(o["lx"]), Serialization_deserValue()(o["hx"]), Serialization_deserializeShape(o["shape"])]);
298
+ case "nesty":
299
+ return new Shape(6, [Serialization_deserValue()(o["ly"]), Serialization_deserValue()(o["hy"]), Serialization_deserializeShape(o["shape"])]);
300
+ case "scale":
301
+ return new Shape(4, [equals(o["sx"], defaultOf()) ? undefined : Serialization_deserializeScale(o["sx"]), equals(o["sy"], defaultOf()) ? undefined : Serialization_deserializeScale(o["sy"]), Serialization_deserializeShape(o["shape"])]);
302
+ case "styled":
303
+ return new Shape(1, [Serialization_deserializeStyleConfig(o["param"]), Serialization_deserializeShape(o["shape"])]);
304
+ case "autoscale":
305
+ return new Shape(3, [o["x"], o["y"], Serialization_deserializeShape(o["shape"])]);
306
+ case "offset":
307
+ return new Shape(14, [[o["dx"], o["dy"]], Serialization_deserializeShape(o["shape"])]);
308
+ default:
309
+ return toFail(printf("Unknown shape kind: %s"))(matchValue);
310
+ }
311
+ }
312
+
37
313
  export const scale = {
38
314
  continuous(lo, hi) {
39
315
  return new Scale(0, [new continuous(lo), new continuous(hi)]);
@@ -45,37 +321,40 @@ export const scale = {
45
321
 
46
322
  export const compost = {
47
323
  scaleX(sc, sh) {
48
- return new Shape(3, [sc, undefined, sh]);
324
+ return new Shape(4, [sc, undefined, sh]);
49
325
  },
50
326
  scaleY(sc_1, sh_1) {
51
- return new Shape(3, [undefined, sc_1, sh_1]);
327
+ return new Shape(4, [undefined, sc_1, sh_1]);
52
328
  },
53
329
  scale(sx, sy, sh_2) {
54
- return new Shape(3, [sx, sy, sh_2]);
330
+ return new Shape(4, [sx, sy, sh_2]);
55
331
  },
56
332
  nestX(lx, hx, s) {
57
- return new Shape(4, [Helpers_parseValue(lx), Helpers_parseValue(hx), s]);
333
+ return new Shape(5, [Helpers_parseValue(lx), Helpers_parseValue(hx), s]);
58
334
  },
59
335
  nestY(ly, hy, s_1) {
60
- return new Shape(5, [Helpers_parseValue(ly), Helpers_parseValue(hy), s_1]);
336
+ return new Shape(6, [Helpers_parseValue(ly), Helpers_parseValue(hy), s_1]);
61
337
  },
62
338
  nest(lx_1, hx_1, ly_1, hy_1, s_2) {
63
- return new Shape(5, [Helpers_parseValue(ly_1), Helpers_parseValue(hy_1), new Shape(4, [Helpers_parseValue(lx_1), Helpers_parseValue(hx_1), s_2])]);
339
+ return new Shape(6, [Helpers_parseValue(ly_1), Helpers_parseValue(hy_1), new Shape(5, [Helpers_parseValue(lx_1), Helpers_parseValue(hx_1), s_2])]);
64
340
  },
65
341
  overlay(sh_3) {
66
- return new Shape(9, [ofArray(sh_3)]);
342
+ return new Shape(10, [ofArray(sh_3)]);
67
343
  },
68
344
  padding(t, r, b, l, s_3) {
69
- return new Shape(12, [[t, r, b, l], s_3]);
345
+ return new Shape(13, [[t, r, b, l], s_3]);
70
346
  },
71
347
  fillColor(c, s_4) {
72
348
  return Derived_FillColor(c, s_4);
73
349
  },
74
- strokeColor(c_1, s_5) {
75
- return Derived_StrokeColor(c_1, s_5);
350
+ preserveAspectRatio(pa, s_5) {
351
+ return Derived_PreserveAspectRatio(pa, s_5);
352
+ },
353
+ strokeColor(c_1, s_6) {
354
+ return Derived_StrokeColor(c_1, s_6);
76
355
  },
77
- font(f, c_2, s_6) {
78
- return Derived_Font(f, c_2, s_6);
356
+ font(f, c_2, s_7) {
357
+ return Derived_Font(f, c_2, s_7);
79
358
  },
80
359
  column(xp, yp) {
81
360
  return Derived_Column(new categorical(xp), new continuous(yp));
@@ -84,26 +363,29 @@ export const compost = {
84
363
  return Derived_Bar(new continuous(xp_1), new categorical(yp_1));
85
364
  },
86
365
  bubble(xp_2, yp_2, w, h) {
87
- return new Shape(7, [Helpers_parseValue(xp_2), Helpers_parseValue(yp_2), w, h]);
366
+ return new Shape(8, [Helpers_parseValue(xp_2), Helpers_parseValue(yp_2), w, h]);
88
367
  },
89
- text(xp_3, yp_3, t_1, s_7, r_1) {
368
+ text(xp_3, yp_3, t_1, s_8, r_1) {
90
369
  const r_2 = equals(r_1, defaultOf()) ? 0 : r_1;
91
- const s_8 = equals(s_7, defaultOf()) ? "" : s_7;
92
- const va = (s_8.indexOf("baseline") >= 0) ? (new VerticalAlign(0, [])) : ((s_8.indexOf("hanging") >= 0) ? (new VerticalAlign(2, [])) : (new VerticalAlign(1, [])));
93
- const ha = (s_8.indexOf("start") >= 0) ? (new HorizontalAlign(0, [])) : ((s_8.indexOf("end") >= 0) ? (new HorizontalAlign(2, [])) : (new HorizontalAlign(1, [])));
94
- return new Shape(1, [Helpers_parseValue(xp_3), Helpers_parseValue(yp_3), va, ha, r_2, t_1]);
370
+ const s_9 = equals(s_8, defaultOf()) ? "" : s_8;
371
+ const va = (s_9.indexOf("baseline") >= 0) ? (new VerticalAlign(0, [])) : ((s_9.indexOf("hanging") >= 0) ? (new VerticalAlign(2, [])) : (new VerticalAlign(1, [])));
372
+ const ha = (s_9.indexOf("start") >= 0) ? (new HorizontalAlign(0, [])) : ((s_9.indexOf("end") >= 0) ? (new HorizontalAlign(2, [])) : (new HorizontalAlign(1, [])));
373
+ return new Shape(2, [Helpers_parseValue(xp_3), Helpers_parseValue(yp_3), va, ha, r_2, t_1]);
95
374
  },
96
375
  shape(a) {
97
- return new Shape(8, [toList(delay(() => map((p) => [Helpers_parseValue(item(0, p)), Helpers_parseValue(item(1, p))], a)))]);
376
+ return new Shape(9, [toList(delay(() => map((p) => [Helpers_parseValue(item(0, p)), Helpers_parseValue(item(1, p))], a)))]);
98
377
  },
99
378
  line(a_1) {
100
- return new Shape(6, [toList(delay(() => map((p_1) => [Helpers_parseValue(item(0, p_1)), Helpers_parseValue(item(1, p_1))], a_1)))]);
379
+ return new Shape(7, [toList(delay(() => map((p_1) => [Helpers_parseValue(item(0, p_1)), Helpers_parseValue(item(1, p_1))], a_1)))]);
380
+ },
381
+ image(href, pt1, pt2) {
382
+ return new Shape(0, [href, [Helpers_parseValue(item(0, pt1)), Helpers_parseValue(item(1, pt1))], [Helpers_parseValue(item(0, pt2)), Helpers_parseValue(item(1, pt2))]]);
101
383
  },
102
- axes(a_2, s_9) {
103
- return new Shape(10, [a_2.indexOf("top") >= 0, a_2.indexOf("right") >= 0, a_2.indexOf("bottom") >= 0, a_2.indexOf("left") >= 0, s_9]);
384
+ axes(a_2, s_10) {
385
+ return new Shape(11, [a_2.indexOf("top") >= 0, a_2.indexOf("right") >= 0, a_2.indexOf("bottom") >= 0, a_2.indexOf("left") >= 0, s_10]);
104
386
  },
105
- on(o, s_10) {
106
- return new Shape(11, [toList(delay(() => map((k) => {
387
+ on(o, s_11) {
388
+ return new Shape(12, [toList(delay(() => map((k) => {
107
389
  let f_2;
108
390
  const f_1 = o[k];
109
391
  f_2 = ((args) => {
@@ -126,7 +408,7 @@ export const compost = {
126
408
  }])) : ((k === "touchend") ? (new EventHandler(6, [(me_7) => {
127
409
  f_2([me_7]);
128
410
  }])) : toFail(printf("Unsupported event type \'%s\' passed to the \'on\' primitive."))(k))))))));
129
- }, Object.keys(o)))), s_10]);
411
+ }, Object.keys(o)))), s_11]);
130
412
  },
131
413
  svg(w_1, h_1, shape) {
132
414
  return Compost_createSvg(false, false, w_1, h_1, shape);
@@ -149,9 +431,9 @@ export const compost = {
149
431
  return new DomNode(1, [defaultOf(), tag, attrs_1, children_1]);
150
432
  },
151
433
  interactive(id, init, update, render) {
152
- createVirtualDomApp(id, init, (t_2, s_12) => {
434
+ createVirtualDomApp(id, init, (t_2, s_13) => {
153
435
  const el = document.getElementById(id);
154
- const res = render(t_2, s_12);
436
+ const res = render(t_2, s_13);
155
437
  if (equals(res["constructor"], (new DomNode(0, [""]))["constructor"])) {
156
438
  return res;
157
439
  }
@@ -164,5 +446,22 @@ export const compost = {
164
446
  const el_1 = document.getElementById(id_1);
165
447
  renderTo(el_1, Compost_createSvg(false, false, el_1.clientWidth, el_1.clientHeight, viz));
166
448
  },
449
+ foldDom(f_3, acc, node) {
450
+ return foldDom((acc_1, _ns, tag_1, attrs_3) => f_3(acc_1, tag_1, createObj(toArray(delay(() => collect((matchValue) => {
451
+ const matchValue_1 = matchValue[1];
452
+ if (matchValue_1.tag === 1) {
453
+ return singleton([matchValue[0], matchValue_1.fields[0]]);
454
+ }
455
+ else {
456
+ return empty();
457
+ }
458
+ }, attrs_3))))), acc, node);
459
+ },
460
+ serialize(s_15) {
461
+ return Serialization_serializeShape(s_15);
462
+ },
463
+ deserialize(o_1) {
464
+ return Serialization_deserializeShape(o_1);
465
+ },
167
466
  };
168
467
 
package/dist/core.js CHANGED
@@ -164,7 +164,7 @@ export function Scale_$reflection(gen0) {
164
164
  }
165
165
 
166
166
  export class Style extends Record {
167
- constructor(StrokeColor, StrokeWidth, StrokeDashArray, Fill, Animation, Font, Cursor, FormatAxisXLabel, FormatAxisYLabel) {
167
+ constructor(StrokeColor, StrokeWidth, StrokeDashArray, Fill, Animation, Font, Cursor, PreserveAspectRatio, FormatAxisXLabel, FormatAxisYLabel) {
168
168
  super();
169
169
  this.StrokeColor = StrokeColor;
170
170
  this.StrokeWidth = StrokeWidth;
@@ -173,13 +173,46 @@ export class Style extends Record {
173
173
  this.Animation = Animation;
174
174
  this.Font = Font;
175
175
  this.Cursor = Cursor;
176
+ this.PreserveAspectRatio = PreserveAspectRatio;
176
177
  this.FormatAxisXLabel = FormatAxisXLabel;
177
178
  this.FormatAxisYLabel = FormatAxisYLabel;
178
179
  }
179
180
  }
180
181
 
181
182
  export function Style_$reflection() {
182
- return record_type("Compost.Style", [], Style, () => [["StrokeColor", tuple_type(float64_type, Color_$reflection())], ["StrokeWidth", Width_$reflection()], ["StrokeDashArray", class_type("System.Collections.Generic.IEnumerable`1", [Number$_$reflection()])], ["Fill", FillStyle_$reflection()], ["Animation", option_type(tuple_type(int32_type, string_type, lambda_type(Style_$reflection(), Style_$reflection())))], ["Font", string_type], ["Cursor", string_type], ["FormatAxisXLabel", lambda_type(Scale_$reflection(class_type("Microsoft.FSharp.Core.CompilerServices.MeasureOne")), lambda_type(Value_$reflection(class_type("Microsoft.FSharp.Core.CompilerServices.MeasureOne")), string_type))], ["FormatAxisYLabel", lambda_type(Scale_$reflection(class_type("Microsoft.FSharp.Core.CompilerServices.MeasureOne")), lambda_type(Value_$reflection(class_type("Microsoft.FSharp.Core.CompilerServices.MeasureOne")), string_type))]]);
183
+ return record_type("Compost.Style", [], Style, () => [["StrokeColor", tuple_type(float64_type, Color_$reflection())], ["StrokeWidth", Width_$reflection()], ["StrokeDashArray", class_type("System.Collections.Generic.IEnumerable`1", [Number$_$reflection()])], ["Fill", FillStyle_$reflection()], ["Animation", option_type(tuple_type(int32_type, string_type, lambda_type(Style_$reflection(), Style_$reflection())))], ["Font", string_type], ["Cursor", string_type], ["PreserveAspectRatio", string_type], ["FormatAxisXLabel", lambda_type(Scale_$reflection(class_type("Microsoft.FSharp.Core.CompilerServices.MeasureOne")), lambda_type(Value_$reflection(class_type("Microsoft.FSharp.Core.CompilerServices.MeasureOne")), string_type))], ["FormatAxisYLabel", lambda_type(Scale_$reflection(class_type("Microsoft.FSharp.Core.CompilerServices.MeasureOne")), lambda_type(Value_$reflection(class_type("Microsoft.FSharp.Core.CompilerServices.MeasureOne")), string_type))]]);
184
+ }
185
+
186
+ export class StyleConfig extends Union {
187
+ constructor(tag, fields) {
188
+ super();
189
+ this.tag = tag;
190
+ this.fields = fields;
191
+ }
192
+ cases() {
193
+ return ["Custom", "FillColor", "StrokeColor", "Font", "PreserveAspectRatio"];
194
+ }
195
+ }
196
+
197
+ export function StyleConfig_$reflection() {
198
+ return union_type("Compost.StyleConfig", [], StyleConfig, () => [[["Item", lambda_type(Style_$reflection(), Style_$reflection())]], [["Item", string_type]], [["Item", string_type]], [["Item1", string_type], ["Item2", string_type]], [["Item", string_type]]]);
199
+ }
200
+
201
+ export function StyleConfig__Apply_4E2CA0C5(this$, style) {
202
+ switch (this$.tag) {
203
+ case 1:
204
+ return new Style(style.StrokeColor, style.StrokeWidth, style.StrokeDashArray, new FillStyle(0, [[1, new Color(1, [this$.fields[0]])]]), style.Animation, style.Font, style.Cursor, style.PreserveAspectRatio, style.FormatAxisXLabel, style.FormatAxisYLabel);
205
+ case 2:
206
+ return new Style([1, new Color(1, [this$.fields[0]])], style.StrokeWidth, style.StrokeDashArray, style.Fill, style.Animation, style.Font, style.Cursor, style.PreserveAspectRatio, style.FormatAxisXLabel, style.FormatAxisYLabel);
207
+ case 3: {
208
+ const clr_2 = this$.fields[1];
209
+ return new Style([0, new Color(1, [clr_2])], style.StrokeWidth, style.StrokeDashArray, new FillStyle(0, [[1, new Color(1, [clr_2])]]), style.Animation, this$.fields[0], style.Cursor, style.PreserveAspectRatio, style.FormatAxisXLabel, style.FormatAxisYLabel);
210
+ }
211
+ case 4:
212
+ return new Style(style.StrokeColor, style.StrokeWidth, style.StrokeDashArray, style.Fill, style.Animation, style.Font, style.Cursor, this$.fields[0], style.FormatAxisXLabel, style.FormatAxisYLabel);
213
+ default:
214
+ return this$.fields[0](style);
215
+ }
183
216
  }
184
217
 
185
218
  export class EventHandler extends Union {
@@ -219,12 +252,12 @@ export class Shape extends Union {
219
252
  this.fields = fields;
220
253
  }
221
254
  cases() {
222
- return ["Style", "Text", "AutoScale", "InnerScale", "NestX", "NestY", "Line", "Bubble", "Shape", "Layered", "Axes", "Interactive", "Padding", "Offset"];
255
+ return ["Image", "Style", "Text", "AutoScale", "InnerScale", "NestX", "NestY", "Line", "Bubble", "Shape", "Layered", "Axes", "Interactive", "Padding", "Offset"];
223
256
  }
224
257
  }
225
258
 
226
259
  export function Shape_$reflection(gen0, gen1) {
227
- return union_type("Compost.Shape", [gen0, gen1], Shape, () => [[["Item1", lambda_type(Style_$reflection(), Style_$reflection())], ["Item2", Shape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen1)], ["Item3", VerticalAlign_$reflection()], ["Item4", HorizontalAlign_$reflection()], ["Item5", float64_type], ["Item6", string_type]], [["Item1", bool_type], ["Item2", bool_type], ["Item3", Shape_$reflection(gen0, gen1)]], [["Item1", option_type(Scale_$reflection(gen0))], ["Item2", option_type(Scale_$reflection(gen1))], ["Item3", Shape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen0)], ["Item3", Shape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen1)], ["Item2", Value_$reflection(gen1)], ["Item3", Shape_$reflection(gen0, gen1)]], [["Item", class_type("System.Collections.Generic.IEnumerable`1", [tuple_type(Value_$reflection(gen0), Value_$reflection(gen1))])]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen1)], ["Item3", float64_type], ["Item4", float64_type]], [["Item", class_type("System.Collections.Generic.IEnumerable`1", [tuple_type(Value_$reflection(gen0), Value_$reflection(gen1))])]], [["Item", class_type("System.Collections.Generic.IEnumerable`1", [Shape_$reflection(gen0, gen1)])]], [["Item1", bool_type], ["Item2", bool_type], ["Item3", bool_type], ["Item4", bool_type], ["Item5", Shape_$reflection(gen0, gen1)]], [["Item1", class_type("System.Collections.Generic.IEnumerable`1", [EventHandler_$reflection(gen0, gen1)])], ["Item2", Shape_$reflection(gen0, gen1)]], [["Item1", tuple_type(float64_type, float64_type, float64_type, float64_type)], ["Item2", Shape_$reflection(gen0, gen1)]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", Shape_$reflection(gen0, gen1)]]]);
260
+ return union_type("Compost.Shape", [gen0, gen1], Shape, () => [[["Item1", string_type], ["Item2", tuple_type(Value_$reflection(gen0), Value_$reflection(gen1))], ["Item3", tuple_type(Value_$reflection(gen0), Value_$reflection(gen1))]], [["Item1", StyleConfig_$reflection()], ["Item2", Shape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen1)], ["Item3", VerticalAlign_$reflection()], ["Item4", HorizontalAlign_$reflection()], ["Item5", float64_type], ["Item6", string_type]], [["Item1", bool_type], ["Item2", bool_type], ["Item3", Shape_$reflection(gen0, gen1)]], [["Item1", option_type(Scale_$reflection(gen0))], ["Item2", option_type(Scale_$reflection(gen1))], ["Item3", Shape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen0)], ["Item3", Shape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen1)], ["Item2", Value_$reflection(gen1)], ["Item3", Shape_$reflection(gen0, gen1)]], [["Item", class_type("System.Collections.Generic.IEnumerable`1", [tuple_type(Value_$reflection(gen0), Value_$reflection(gen1))])]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen1)], ["Item3", float64_type], ["Item4", float64_type]], [["Item", class_type("System.Collections.Generic.IEnumerable`1", [tuple_type(Value_$reflection(gen0), Value_$reflection(gen1))])]], [["Item", class_type("System.Collections.Generic.IEnumerable`1", [Shape_$reflection(gen0, gen1)])]], [["Item1", bool_type], ["Item2", bool_type], ["Item3", bool_type], ["Item4", bool_type], ["Item5", Shape_$reflection(gen0, gen1)]], [["Item1", class_type("System.Collections.Generic.IEnumerable`1", [EventHandler_$reflection(gen0, gen1)])], ["Item2", Shape_$reflection(gen0, gen1)]], [["Item1", tuple_type(float64_type, float64_type, float64_type, float64_type)], ["Item2", Shape_$reflection(gen0, gen1)]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", Shape_$reflection(gen0, gen1)]]]);
228
261
  }
229
262
 
230
263
  export class Svg_StringBuilder {
@@ -271,17 +304,17 @@ export class Svg_Svg extends Union {
271
304
  this.fields = fields;
272
305
  }
273
306
  cases() {
274
- return ["Path", "Ellipse", "Rect", "Text", "Combine", "Empty"];
307
+ return ["Image", "Path", "Ellipse", "Rect", "Text", "Combine", "Empty"];
275
308
  }
276
309
  }
277
310
 
278
311
  export function Svg_Svg_$reflection() {
279
- return union_type("Compost.Svg.Svg", [], Svg_Svg, () => [[["Item1", array_type(Svg_PathSegment_$reflection())], ["Item2", string_type]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", tuple_type(float64_type, float64_type)], ["Item3", string_type]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", tuple_type(float64_type, float64_type)], ["Item3", string_type]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", string_type], ["Item3", float64_type], ["Item4", string_type]], [["Item", array_type(Svg_Svg_$reflection())]], []]);
312
+ return union_type("Compost.Svg.Svg", [], Svg_Svg, () => [[["Item1", string_type], ["Item2", tuple_type(float64_type, float64_type)], ["Item3", tuple_type(float64_type, float64_type)], ["Item4", string_type]], [["Item1", array_type(Svg_PathSegment_$reflection())], ["Item2", string_type]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", tuple_type(float64_type, float64_type)], ["Item3", string_type]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", tuple_type(float64_type, float64_type)], ["Item3", string_type]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", string_type], ["Item3", float64_type], ["Item4", string_type]], [["Item", array_type(Svg_Svg_$reflection())]], []]);
280
313
  }
281
314
 
282
315
  export function Svg_mapSvg(f, _arg) {
283
- if (_arg.tag === 4) {
284
- return new Svg_Svg(4, [map((_arg_1) => Svg_mapSvg(f, _arg_1), _arg.fields[0])]);
316
+ if (_arg.tag === 5) {
317
+ return new Svg_Svg(5, [map((_arg_1) => Svg_mapSvg(f, _arg_1), _arg.fields[0])]);
285
318
  }
286
319
  else {
287
320
  return f(_arg);
@@ -322,17 +355,19 @@ export function Svg_RenderingContext_$reflection() {
322
355
  export function Svg_renderSvg(ctx, svg) {
323
356
  return delay(() => {
324
357
  switch (svg.tag) {
325
- case 3: {
358
+ case 4: {
326
359
  const y = svg.fields[0][1];
327
360
  const x = svg.fields[0][0];
328
361
  const rotation = svg.fields[2];
329
362
  return singleton(El_op_Dynamic_Z451691CD(s_6, "text")(toList(delay(() => append(singleton(op_EqualsGreater("style", svg.fields[3])), delay(() => ((rotation === 0) ? append(singleton(op_EqualsGreater("x", x.toString())), delay(() => singleton(op_EqualsGreater("y", y.toString())))) : append(singleton(op_EqualsGreater("x", "0")), delay(() => append(singleton(op_EqualsGreater("y", "0")), delay(() => singleton(op_EqualsGreater("transform", toText(printf("translate(%f,%f) rotate(%f)"))(x)(y)(rotation)))))))))))))(singleton_1(text(svg.fields[1]))));
330
363
  }
331
- case 4:
364
+ case 5:
332
365
  return collect((s) => Svg_renderSvg(ctx, s), svg.fields[0]);
333
- case 1:
366
+ case 2:
334
367
  return singleton(El_op_Dynamic_Z451691CD(s_6, "ellipse")(ofArray([op_EqualsGreater("cx", svg.fields[0][0].toString()), op_EqualsGreater("cy", svg.fields[0][1].toString()), op_EqualsGreater("rx", svg.fields[1][0].toString()), op_EqualsGreater("ry", svg.fields[1][1].toString()), op_EqualsGreater("style", svg.fields[2])]))(empty()));
335
- case 2: {
368
+ case 0:
369
+ return singleton(El_op_Dynamic_Z451691CD(s_6, "image")(ofArray([op_EqualsGreater("href", svg.fields[0]), op_EqualsGreater("preserveAspectRatio", svg.fields[3]), op_EqualsGreater("x", svg.fields[1][0].toString()), op_EqualsGreater("y", svg.fields[1][1].toString()), op_EqualsGreater("width", svg.fields[2][0].toString()), op_EqualsGreater("height", svg.fields[2][1].toString())]))(empty()));
370
+ case 3: {
336
371
  const y2 = svg.fields[1][1];
337
372
  const y1 = svg.fields[0][1];
338
373
  const x2 = svg.fields[1][0];
@@ -343,7 +378,7 @@ export function Svg_renderSvg(ctx, svg) {
343
378
  const matchValue_3 = Math.abs(y1 - y2);
344
379
  return singleton(El_op_Dynamic_Z451691CD(s_6, "rect")(ofArray([op_EqualsGreater("x", matchValue.toString()), op_EqualsGreater("y", matchValue_1.toString()), op_EqualsGreater("width", matchValue_2.toString()), op_EqualsGreater("height", matchValue_3.toString()), op_EqualsGreater("style", svg.fields[2])]))(empty()));
345
380
  }
346
- case 0:
381
+ case 1:
347
382
  return singleton(El_op_Dynamic_Z451691CD(s_6, "path")(ofArray([op_EqualsGreater("d", Svg_formatPath(svg.fields[0])), op_EqualsGreater("style", svg.fields[1])]))(empty()));
348
383
  default: {
349
384
  return empty_1();
@@ -382,8 +417,8 @@ export function Svg_formatStyle(defs, style) {
382
417
  const ease = matchValue[1];
383
418
  const anim = matchValue[2];
384
419
  const id = "anim_" + replace((copyOfStruct = newGuid(), copyOfStruct), "-", "");
385
- const fromstyle = Svg_formatStyle(defs, new Style(style.StrokeColor, style.StrokeWidth, style.StrokeDashArray, style.Fill, undefined, style.Font, style.Cursor, style.FormatAxisXLabel, style.FormatAxisYLabel));
386
- const tostyle = Svg_formatStyle(defs, (bind$0040 = anim(style), new Style(bind$0040.StrokeColor, bind$0040.StrokeWidth, bind$0040.StrokeDashArray, bind$0040.Fill, undefined, bind$0040.Font, bind$0040.Cursor, bind$0040.FormatAxisXLabel, bind$0040.FormatAxisYLabel)));
420
+ const fromstyle = Svg_formatStyle(defs, new Style(style.StrokeColor, style.StrokeWidth, style.StrokeDashArray, style.Fill, undefined, style.Font, style.Cursor, style.PreserveAspectRatio, style.FormatAxisXLabel, style.FormatAxisYLabel));
421
+ const tostyle = Svg_formatStyle(defs, (bind$0040 = anim(style), new Style(bind$0040.StrokeColor, bind$0040.StrokeWidth, bind$0040.StrokeDashArray, bind$0040.Fill, undefined, bind$0040.Font, bind$0040.Cursor, bind$0040.PreserveAspectRatio, bind$0040.FormatAxisXLabel, bind$0040.FormatAxisYLabel)));
387
422
  const item = El_op_Dynamic_Z451691CD(h_2, "style")(empty())(singleton_1(text(toText(printf("@keyframes %s { from { %s } to { %s } }"))(id)(fromstyle)(tostyle))));
388
423
  void (defs.push(item));
389
424
  patternInput = [anim(style), toText(printf("animation: %s %dms %s; "))(id)(ms)(ease)];
@@ -399,12 +434,12 @@ export class Scales_ScaledShape extends Union {
399
434
  this.fields = fields;
400
435
  }
401
436
  cases() {
402
- return ["ScaledStyle", "ScaledText", "ScaledLine", "ScaledBubble", "ScaledShape", "ScaledLayered", "ScaledInteractive", "ScaledPadding", "ScaledOffset", "ScaledNestX", "ScaledNestY"];
437
+ return ["ScaledStyle", "ScaledText", "ScaledLine", "ScaledBubble", "ScaledShape", "ScaledImage", "ScaledLayered", "ScaledInteractive", "ScaledPadding", "ScaledOffset", "ScaledNestX", "ScaledNestY"];
403
438
  }
404
439
  }
405
440
 
406
441
  export function Scales_ScaledShape_$reflection(gen0, gen1) {
407
- return union_type("Compost.Scales.ScaledShape", [gen0, gen1], Scales_ScaledShape, () => [[["Item1", lambda_type(Style_$reflection(), Style_$reflection())], ["Item2", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen1)], ["Item3", VerticalAlign_$reflection()], ["Item4", HorizontalAlign_$reflection()], ["Item5", float64_type], ["Item6", string_type]], [["Item", array_type(tuple_type(Value_$reflection(gen0), Value_$reflection(gen1)))]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen1)], ["Item3", float64_type], ["Item4", float64_type]], [["Item", array_type(tuple_type(Value_$reflection(gen0), Value_$reflection(gen1)))]], [["Item", array_type(Scales_ScaledShape_$reflection(gen0, gen1))]], [["Item1", class_type("System.Collections.Generic.IEnumerable`1", [EventHandler_$reflection(gen0, gen1)])], ["Item2", Scale_$reflection(gen0)], ["Item3", Scale_$reflection(gen1)], ["Item4", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", tuple_type(float64_type, float64_type, float64_type, float64_type)], ["Item2", Scale_$reflection(gen0)], ["Item3", Scale_$reflection(gen1)], ["Item4", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen0)], ["Item3", Scale_$reflection(gen0)], ["Item4", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen1)], ["Item2", Value_$reflection(gen1)], ["Item3", Scale_$reflection(gen1)], ["Item4", Scales_ScaledShape_$reflection(gen0, gen1)]]]);
442
+ return union_type("Compost.Scales.ScaledShape", [gen0, gen1], Scales_ScaledShape, () => [[["Item1", StyleConfig_$reflection()], ["Item2", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen1)], ["Item3", VerticalAlign_$reflection()], ["Item4", HorizontalAlign_$reflection()], ["Item5", float64_type], ["Item6", string_type]], [["Item", array_type(tuple_type(Value_$reflection(gen0), Value_$reflection(gen1)))]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen1)], ["Item3", float64_type], ["Item4", float64_type]], [["Item", array_type(tuple_type(Value_$reflection(gen0), Value_$reflection(gen1)))]], [["Item1", string_type], ["Item2", tuple_type(Value_$reflection(gen0), Value_$reflection(gen1))], ["Item3", tuple_type(Value_$reflection(gen0), Value_$reflection(gen1))]], [["Item", array_type(Scales_ScaledShape_$reflection(gen0, gen1))]], [["Item1", class_type("System.Collections.Generic.IEnumerable`1", [EventHandler_$reflection(gen0, gen1)])], ["Item2", Scale_$reflection(gen0)], ["Item3", Scale_$reflection(gen1)], ["Item4", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", tuple_type(float64_type, float64_type, float64_type, float64_type)], ["Item2", Scale_$reflection(gen0)], ["Item3", Scale_$reflection(gen1)], ["Item4", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", tuple_type(float64_type, float64_type)], ["Item2", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen0)], ["Item2", Value_$reflection(gen0)], ["Item3", Scale_$reflection(gen0)], ["Item4", Scales_ScaledShape_$reflection(gen0, gen1)]], [["Item1", Value_$reflection(gen1)], ["Item2", Value_$reflection(gen1)], ["Item3", Scale_$reflection(gen1)], ["Item4", Scales_ScaledShape_$reflection(gen0, gen1)]]]);
408
443
  }
409
444
 
410
445
  export function Scales_getExtremes(_arg) {
@@ -597,25 +632,25 @@ export function Scales_calculateShapeScales(points) {
597
632
  export function Scales_calculateScales(style, shape) {
598
633
  const calculateScales = (shape_2) => Scales_calculateScales(style, shape_2);
599
634
  switch (shape.tag) {
600
- case 4: {
635
+ case 5: {
601
636
  const nx2 = shape.fields[1];
602
637
  const nx1 = shape.fields[0];
603
638
  const patternInput_1 = calculateScales(shape.fields[2]);
604
- return [[Scales_calculateShapeScale([nx1, nx2]), patternInput_1[0][1]], new Scales_ScaledShape(9, [nx1, nx2, patternInput_1[0][0], patternInput_1[1]])];
639
+ return [[Scales_calculateShapeScale([nx1, nx2]), patternInput_1[0][1]], new Scales_ScaledShape(10, [nx1, nx2, patternInput_1[0][0], patternInput_1[1]])];
605
640
  }
606
- case 5: {
641
+ case 6: {
607
642
  const ny2 = shape.fields[1];
608
643
  const ny1 = shape.fields[0];
609
644
  const patternInput_2 = calculateScales(shape.fields[2]);
610
- return [[patternInput_2[0][0], Scales_calculateShapeScale([ny1, ny2])], new Scales_ScaledShape(10, [ny1, ny2, patternInput_2[0][1], patternInput_2[1]])];
645
+ return [[patternInput_2[0][0], Scales_calculateShapeScale([ny1, ny2])], new Scales_ScaledShape(11, [ny1, ny2, patternInput_2[0][1], patternInput_2[1]])];
611
646
  }
612
- case 3: {
647
+ case 4: {
613
648
  const sy = shape.fields[1];
614
649
  const sx = shape.fields[0];
615
650
  const patternInput_3 = calculateScales(shape.fields[2]);
616
651
  return [[(sx != null) ? sx : patternInput_3[0][0], (sy != null) ? sy : patternInput_3[0][1]], patternInput_3[1]];
617
652
  }
618
- case 2: {
653
+ case 3: {
619
654
  const patternInput_4 = calculateScales(shape.fields[2]);
620
655
  const isy_3 = patternInput_4[0][1];
621
656
  const isx_3 = patternInput_4[0][0];
@@ -630,17 +665,17 @@ export function Scales_calculateScales(style, shape) {
630
665
  };
631
666
  return [[shape.fields[0] ? autoScale(isx_3) : isx_3, shape.fields[1] ? autoScale(isy_3) : isy_3], patternInput_4[1]];
632
667
  }
633
- case 13: {
668
+ case 14: {
634
669
  const patternInput_6 = calculateScales(shape.fields[1]);
635
- return [patternInput_6[0], new Scales_ScaledShape(8, [shape.fields[0], patternInput_6[1]])];
670
+ return [patternInput_6[0], new Scales_ScaledShape(9, [shape.fields[0], patternInput_6[1]])];
636
671
  }
637
- case 12: {
672
+ case 13: {
638
673
  const patternInput_7 = calculateScales(shape.fields[1]);
639
674
  const sy_3 = patternInput_7[0][1];
640
675
  const sx_3 = patternInput_7[0][0];
641
- return [[sx_3, sy_3], new Scales_ScaledShape(7, [shape.fields[0], sx_3, sy_3, patternInput_7[1]])];
676
+ return [[sx_3, sy_3], new Scales_ScaledShape(8, [shape.fields[0], sx_3, sy_3, patternInput_7[1]])];
642
677
  }
643
- case 7: {
678
+ case 8: {
644
679
  const y = shape.fields[1];
645
680
  const x = shape.fields[0];
646
681
  const makeSingletonScale = (_arg_1) => {
@@ -654,7 +689,7 @@ export function Scales_calculateScales(style, shape) {
654
689
  };
655
690
  return [[makeSingletonScale(x), makeSingletonScale(y)], new Scales_ScaledShape(3, [x, y, shape.fields[2], shape.fields[3]])];
656
691
  }
657
- case 1: {
692
+ case 2: {
658
693
  const y_1 = shape.fields[1];
659
694
  const x_1 = shape.fields[0];
660
695
  const makeSingletonScale_1 = (_arg_2) => {
@@ -668,15 +703,20 @@ export function Scales_calculateScales(style, shape) {
668
703
  };
669
704
  return [[makeSingletonScale_1(x_1), makeSingletonScale_1(y_1)], new Scales_ScaledShape(1, [x_1, y_1, shape.fields[2], shape.fields[3], shape.fields[4], shape.fields[5]])];
670
705
  }
671
- case 6: {
706
+ case 7: {
672
707
  const line_1 = toArray(shape.fields[0]);
673
708
  return [Scales_calculateShapeScales(line_1), new Scales_ScaledShape(2, [line_1])];
674
709
  }
675
- case 8: {
710
+ case 0: {
711
+ const pt2 = shape.fields[2];
712
+ const pt1 = shape.fields[1];
713
+ return [Scales_calculateShapeScales([pt1, pt2]), new Scales_ScaledShape(5, [shape.fields[0], pt1, pt2])];
714
+ }
715
+ case 9: {
676
716
  const points_1 = toArray(shape.fields[0]);
677
717
  return [Scales_calculateShapeScales(points_1), new Scales_ScaledShape(4, [points_1])];
678
718
  }
679
- case 10: {
719
+ case 11: {
680
720
  const showTop = shape.fields[0];
681
721
  const showRight = shape.fields[1];
682
722
  const showLeft = shape.fields[3];
@@ -691,25 +731,25 @@ export function Scales_calculateScales(style, shape) {
691
731
  const lx = matchValue[0];
692
732
  const hy = matchValue_1[1];
693
733
  const hx = matchValue[1];
694
- const LineStyle = (clr, alpha, width, shape_18) => (new Shape(0, [(s) => (new Style([alpha, new Color(1, [clr])], new Width(width), s.StrokeDashArray, new FillStyle(0, [[1, new Color(1, ["transparent"])]]), s.Animation, s.Font, s.Cursor, s.FormatAxisXLabel, s.FormatAxisYLabel)), shape_18]));
695
- const FontStyle = (style_2, shape_19) => (new Shape(0, [(s_1) => (new Style([0, new Color(1, ["transparent"])], s_1.StrokeWidth, s_1.StrokeDashArray, new FillStyle(0, [[1, new Color(1, ["black"])]]), s_1.Animation, style_2, s_1.Cursor, s_1.FormatAxisXLabel, s_1.FormatAxisYLabel)), shape_19]));
696
- return calculateScales(new Shape(12, [[showTop ? 30 : 0, showRight ? 50 : 0, showBottom ? 30 : 0, showLeft ? 50 : 0], new Shape(9, [toList(delay(() => append(singleton(new Shape(3, [sx_4, sy_4, new Shape(9, [toList(delay(() => append(map_1((x_2) => LineStyle("#e4e4e4", 1, 1, new Shape(6, [[[x_2, ly], [x_2, hy]]])), Scales_generateAxisSteps(sx_4)), delay(() => map_1((y_2) => LineStyle("#e4e4e4", 1, 1, new Shape(6, [[[lx, y_2], [hx, y_2]]])), Scales_generateAxisSteps(sy_4))))))])])), delay(() => append(showTop ? append(singleton(LineStyle("black", 1, 2, new Shape(6, [[[lx, hy], [hx, hy]]]))), delay(() => collect((matchValue_2) => singleton(FontStyle("9pt sans-serif", new Shape(13, [[0, -10], new Shape(1, [matchValue_2[0], hy, new VerticalAlign(0, []), new HorizontalAlign(1, []), 0, matchValue_2[1]])]))), Scales_generateAxisLabels(style.FormatAxisXLabel, sx_4)))) : empty_1(), delay(() => append(showRight ? append(singleton(LineStyle("black", 1, 2, new Shape(6, [[[hx, hy], [hx, ly]]]))), delay(() => collect((matchValue_3) => singleton(FontStyle("9pt sans-serif", new Shape(13, [[10, 0], new Shape(1, [hx, matchValue_3[0], new VerticalAlign(1, []), new HorizontalAlign(0, []), 0, matchValue_3[1]])]))), Scales_generateAxisLabels(style.FormatAxisYLabel, sy_4)))) : empty_1(), delay(() => append(showBottom ? append(singleton(LineStyle("black", 1, 2, new Shape(6, [[[lx, ly], [hx, ly]]]))), delay(() => collect((matchValue_4) => singleton(FontStyle("9pt sans-serif", new Shape(13, [[0, 10], new Shape(1, [matchValue_4[0], ly, new VerticalAlign(2, []), new HorizontalAlign(1, []), 0, matchValue_4[1]])]))), Scales_generateAxisLabels(style.FormatAxisXLabel, sx_4)))) : empty_1(), delay(() => append(showLeft ? append(singleton(LineStyle("black", 1, 2, new Shape(6, [[[lx, hy], [lx, ly]]]))), delay(() => collect((matchValue_5) => singleton(FontStyle("9pt sans-serif", new Shape(13, [[-10, 0], new Shape(1, [lx, matchValue_5[0], new VerticalAlign(1, []), new HorizontalAlign(2, []), 0, matchValue_5[1]])]))), Scales_generateAxisLabels(style.FormatAxisYLabel, sy_4)))) : empty_1(), delay(() => singleton(shape_17)))))))))))))])]));
734
+ const LineStyle = (clr, alpha, width, shape_18) => (new Shape(1, [new StyleConfig(0, [(s) => (new Style([alpha, new Color(1, [clr])], new Width(width), s.StrokeDashArray, new FillStyle(0, [[1, new Color(1, ["transparent"])]]), s.Animation, s.Font, s.Cursor, s.PreserveAspectRatio, s.FormatAxisXLabel, s.FormatAxisYLabel))]), shape_18]));
735
+ const FontStyle = (style_2, shape_19) => (new Shape(1, [new StyleConfig(0, [(s_1) => (new Style([0, new Color(1, ["transparent"])], s_1.StrokeWidth, s_1.StrokeDashArray, new FillStyle(0, [[1, new Color(1, ["black"])]]), s_1.Animation, style_2, s_1.Cursor, s_1.PreserveAspectRatio, s_1.FormatAxisXLabel, s_1.FormatAxisYLabel))]), shape_19]));
736
+ return calculateScales(new Shape(13, [[showTop ? 30 : 0, showRight ? 50 : 0, showBottom ? 30 : 0, showLeft ? 50 : 0], new Shape(10, [toList(delay(() => append(singleton(new Shape(4, [sx_4, sy_4, new Shape(10, [toList(delay(() => append(map_1((x_2) => LineStyle("#e4e4e4", 1, 1, new Shape(7, [[[x_2, ly], [x_2, hy]]])), Scales_generateAxisSteps(sx_4)), delay(() => map_1((y_2) => LineStyle("#e4e4e4", 1, 1, new Shape(7, [[[lx, y_2], [hx, y_2]]])), Scales_generateAxisSteps(sy_4))))))])])), delay(() => append(showTop ? append(singleton(LineStyle("black", 1, 2, new Shape(7, [[[lx, hy], [hx, hy]]]))), delay(() => collect((matchValue_2) => singleton(FontStyle("9pt sans-serif", new Shape(14, [[0, -10], new Shape(2, [matchValue_2[0], hy, new VerticalAlign(0, []), new HorizontalAlign(1, []), 0, matchValue_2[1]])]))), Scales_generateAxisLabels(style.FormatAxisXLabel, sx_4)))) : empty_1(), delay(() => append(showRight ? append(singleton(LineStyle("black", 1, 2, new Shape(7, [[[hx, hy], [hx, ly]]]))), delay(() => collect((matchValue_3) => singleton(FontStyle("9pt sans-serif", new Shape(14, [[10, 0], new Shape(2, [hx, matchValue_3[0], new VerticalAlign(1, []), new HorizontalAlign(0, []), 0, matchValue_3[1]])]))), Scales_generateAxisLabels(style.FormatAxisYLabel, sy_4)))) : empty_1(), delay(() => append(showBottom ? append(singleton(LineStyle("black", 1, 2, new Shape(7, [[[lx, ly], [hx, ly]]]))), delay(() => collect((matchValue_4) => singleton(FontStyle("9pt sans-serif", new Shape(14, [[0, 10], new Shape(2, [matchValue_4[0], ly, new VerticalAlign(2, []), new HorizontalAlign(1, []), 0, matchValue_4[1]])]))), Scales_generateAxisLabels(style.FormatAxisXLabel, sx_4)))) : empty_1(), delay(() => append(showLeft ? append(singleton(LineStyle("black", 1, 2, new Shape(7, [[[lx, hy], [lx, ly]]]))), delay(() => collect((matchValue_5) => singleton(FontStyle("9pt sans-serif", new Shape(14, [[-10, 0], new Shape(2, [lx, matchValue_5[0], new VerticalAlign(1, []), new HorizontalAlign(2, []), 0, matchValue_5[1]])]))), Scales_generateAxisLabels(style.FormatAxisYLabel, sy_4)))) : empty_1(), delay(() => singleton(shape_17)))))))))))))])]));
697
737
  }
698
- case 9: {
738
+ case 10: {
699
739
  const scaled = map(calculateScales, Array.from(shape.fields[0]));
700
740
  const sxs = map((tupledArg) => tupledArg[0][0], scaled);
701
741
  const sys = map((tupledArg_1) => tupledArg_1[0][1], scaled);
702
- return [[sxs.reduce(Scales_unionScales), sys.reduce(Scales_unionScales)], new Scales_ScaledShape(5, [map((tuple) => tuple[1], scaled)])];
742
+ return [[sxs.reduce(Scales_unionScales), sys.reduce(Scales_unionScales)], new Scales_ScaledShape(6, [map((tuple) => tuple[1], scaled)])];
703
743
  }
704
- case 11: {
744
+ case 12: {
705
745
  const patternInput_10 = calculateScales(shape.fields[1]);
706
- const scales_8 = patternInput_10[0];
707
- return [scales_8, new Scales_ScaledShape(6, [shape.fields[0], scales_8[0], scales_8[1], patternInput_10[1]])];
746
+ const scales_9 = patternInput_10[0];
747
+ return [scales_9, new Scales_ScaledShape(7, [shape.fields[0], scales_9[0], scales_9[1], patternInput_10[1]])];
708
748
  }
709
749
  default: {
710
- const f = shape.fields[0];
711
- const patternInput = Scales_calculateScales(f(style), shape.fields[1]);
712
- return [patternInput[0], new Scales_ScaledShape(0, [f, patternInput[1]])];
750
+ const sc = shape.fields[0];
751
+ const patternInput = Scales_calculateScales(StyleConfig__Apply_4E2CA0C5(sc, style), shape.fields[1]);
752
+ return [patternInput[0], new Scales_ScaledShape(0, [sc, patternInput[1]])];
713
753
  }
714
754
  }
715
755
  }
@@ -795,12 +835,12 @@ export function Drawing_DrawingContext_$reflection() {
795
835
 
796
836
  export function Drawing_hideFill(style) {
797
837
  let matchValue, f;
798
- return new Style(style.StrokeColor, style.StrokeWidth, style.StrokeDashArray, new FillStyle(0, [[0, new Color(0, [0, 0, 0])]]), (matchValue = style.Animation, (matchValue != null) ? ((f = matchValue[2], [matchValue[0], matchValue[1], (arg) => Drawing_hideFill(f(arg))])) : undefined), style.Font, style.Cursor, style.FormatAxisXLabel, style.FormatAxisYLabel);
838
+ return new Style(style.StrokeColor, style.StrokeWidth, style.StrokeDashArray, new FillStyle(0, [[0, new Color(0, [0, 0, 0])]]), (matchValue = style.Animation, (matchValue != null) ? ((f = matchValue[2], [matchValue[0], matchValue[1], (arg) => Drawing_hideFill(f(arg))])) : undefined), style.Font, style.Cursor, style.PreserveAspectRatio, style.FormatAxisXLabel, style.FormatAxisYLabel);
799
839
  }
800
840
 
801
841
  export function Drawing_hideStroke(style) {
802
842
  let matchValue, f;
803
- return new Style([0, style.StrokeColor[1]], style.StrokeWidth, style.StrokeDashArray, style.Fill, (matchValue = style.Animation, (matchValue != null) ? ((f = matchValue[2], [matchValue[0], matchValue[1], (arg) => Drawing_hideStroke(f(arg))])) : undefined), style.Font, style.Cursor, style.FormatAxisXLabel, style.FormatAxisYLabel);
843
+ return new Style([0, style.StrokeColor[1]], style.StrokeWidth, style.StrokeDashArray, style.Fill, (matchValue = style.Animation, (matchValue != null) ? ((f = matchValue[2], [matchValue[0], matchValue[1], (arg) => Drawing_hideStroke(f(arg))])) : undefined), style.Font, style.Cursor, style.PreserveAspectRatio, style.FormatAxisXLabel, style.FormatAxisYLabel);
804
844
  }
805
845
 
806
846
  export function Drawing_drawShape(ctx_mut, area__mut, area__1_mut, area__2_mut, area__3_mut, scales__mut, scales__1_mut, shape_mut) {
@@ -817,7 +857,7 @@ export function Drawing_drawShape(ctx_mut, area__mut, area__1_mut, area__2_mut,
817
857
  const sx = scales_1[0];
818
858
  const project = (tupledArg) => [Projections_projectOneX(x1, x2)(sx)(tupledArg[0]), Projections_projectOneY(y1, y2)(sy)(tupledArg[1])];
819
859
  switch (shape.tag) {
820
- case 10: {
860
+ case 11: {
821
861
  ctx_mut = ctx;
822
862
  area__mut = x1;
823
863
  area__1_mut = Projections_projectOneY(y1, y2)(sy)(shape.fields[0]);
@@ -828,7 +868,7 @@ export function Drawing_drawShape(ctx_mut, area__mut, area__1_mut, area__2_mut,
828
868
  shape_mut = shape.fields[3];
829
869
  continue Drawing_drawShape;
830
870
  }
831
- case 8: {
871
+ case 9: {
832
872
  const dy = shape.fields[0][1];
833
873
  const dx = shape.fields[0][0];
834
874
  ctx_mut = ctx;
@@ -841,10 +881,10 @@ export function Drawing_drawShape(ctx_mut, area__mut, area__1_mut, area__2_mut,
841
881
  shape_mut = shape.fields[1];
842
882
  continue Drawing_drawShape;
843
883
  }
844
- case 5:
845
- return new Svg_Svg(4, [map((shape_4) => Drawing_drawShape(ctx, area_1[0], area_1[1], area_1[2], area_1[3], scales_1[0], scales_1[1], shape_4), shape.fields[0])]);
884
+ case 6:
885
+ return new Svg_Svg(5, [map((shape_4) => Drawing_drawShape(ctx, area_1[0], area_1[1], area_1[2], area_1[3], scales_1[0], scales_1[1], shape_4), shape.fields[0])]);
846
886
  case 0: {
847
- ctx_mut = (new Drawing_DrawingContext(shape.fields[0](ctx.Style), ctx.Definitions));
887
+ ctx_mut = (new Drawing_DrawingContext(StyleConfig__Apply_4E2CA0C5(shape.fields[0], ctx.Style), ctx.Definitions));
848
888
  area__mut = area_1[0];
849
889
  area__1_mut = area_1[1];
850
890
  area__2_mut = area_1[2];
@@ -856,9 +896,18 @@ export function Drawing_drawShape(ctx_mut, area__mut, area__1_mut, area__2_mut,
856
896
  }
857
897
  case 4: {
858
898
  const points = shape.fields[0];
859
- return new Svg_Svg(0, [toArray(delay(() => append(singleton(new Svg_PathSegment(0, [project(item_2(0, points))])), delay(() => append(map_1((pt) => (new Svg_PathSegment(1, [project(pt)])), skip(1, points)), delay(() => singleton(new Svg_PathSegment(1, [project(item_2(0, points))])))))))), Svg_formatStyle(ctx.Definitions, Drawing_hideStroke(ctx.Style))]);
899
+ return new Svg_Svg(1, [toArray(delay(() => append(singleton(new Svg_PathSegment(0, [project(item_2(0, points))])), delay(() => append(map_1((pt) => (new Svg_PathSegment(1, [project(pt)])), skip(1, points)), delay(() => singleton(new Svg_PathSegment(1, [project(item_2(0, points))])))))))), Svg_formatStyle(ctx.Definitions, Drawing_hideStroke(ctx.Style))]);
860
900
  }
861
- case 7: {
901
+ case 5: {
902
+ const patternInput = project(shape.fields[1]);
903
+ const y1_1 = patternInput[1];
904
+ const x1_1 = patternInput[0];
905
+ const patternInput_1 = project(shape.fields[2]);
906
+ const y2_1 = patternInput_1[1];
907
+ const x2_1 = patternInput_1[0];
908
+ return new Svg_Svg(0, [shape.fields[0], [min(x1_1, x2_1), min(y1_1, y2_1)], [Math.abs(x2_1 - x1_1), Math.abs(y2_1 - y1_1)], ctx.Style.PreserveAspectRatio]);
909
+ }
910
+ case 8: {
862
911
  const isy_1 = shape.fields[2];
863
912
  const isx_1 = shape.fields[1];
864
913
  const calculateNestedRange = (rev) => ((tupledArg_1) => ((ins) => {
@@ -883,13 +932,13 @@ export function Drawing_drawShape(ctx_mut, area__mut, area__1_mut, area__2_mut,
883
932
  }
884
933
  };
885
934
  }));
886
- const patternInput = calculateNestedRange(false)([x1, x2])(isx_1)(sx);
887
- const patternInput_1 = calculateNestedRange(true)([y1, y2])(isy_1)(sy);
935
+ const patternInput_2 = calculateNestedRange(false)([x1, x2])(isx_1)(sx);
936
+ const patternInput_3 = calculateNestedRange(true)([y1, y2])(isy_1)(sy);
888
937
  ctx_mut = ctx;
889
- area__mut = (patternInput[0] + shape.fields[0][3]);
890
- area__1_mut = (patternInput_1[0] + shape.fields[0][0]);
891
- area__2_mut = (patternInput[1] - shape.fields[0][1]);
892
- area__3_mut = (patternInput_1[1] - shape.fields[0][2]);
938
+ area__mut = (patternInput_2[0] + shape.fields[0][3]);
939
+ area__1_mut = (patternInput_3[0] + shape.fields[0][0]);
940
+ area__2_mut = (patternInput_2[1] - shape.fields[0][1]);
941
+ area__3_mut = (patternInput_3[1] - shape.fields[0][2]);
893
942
  scales__mut = isx_1;
894
943
  scales__1_mut = isy_1;
895
944
  shape_mut = shape.fields[3];
@@ -897,18 +946,18 @@ export function Drawing_drawShape(ctx_mut, area__mut, area__1_mut, area__2_mut,
897
946
  }
898
947
  case 2: {
899
948
  const line = shape.fields[0];
900
- return new Svg_Svg(0, [Array.from(delay(() => append(singleton(new Svg_PathSegment(0, [project(head(line))])), delay(() => map_1((pt_1) => (new Svg_PathSegment(1, [project(pt_1)])), skip(1, line)))))), Svg_formatStyle(ctx.Definitions, Drawing_hideFill(ctx.Style))]);
949
+ return new Svg_Svg(1, [Array.from(delay(() => append(singleton(new Svg_PathSegment(0, [project(head(line))])), delay(() => map_1((pt_1) => (new Svg_PathSegment(1, [project(pt_1)])), skip(1, line)))))), Svg_formatStyle(ctx.Definitions, Drawing_hideFill(ctx.Style))]);
901
950
  }
902
951
  case 1: {
903
952
  const va = shape.fields[2];
904
953
  const ha = shape.fields[3];
905
954
  const va_1 = (va.tag === 2) ? "hanging" : ((va.tag === 1) ? "middle" : "baseline");
906
955
  const ha_1 = (ha.tag === 1) ? "middle" : ((ha.tag === 2) ? "end" : "start");
907
- return new Svg_Svg(3, [project([shape.fields[0], shape.fields[1]]), shape.fields[5], shape.fields[4], toText(printf("alignment-baseline:%s; text-anchor:%s;"))(va_1)(ha_1) + Svg_formatStyle(ctx.Definitions, ctx.Style)]);
956
+ return new Svg_Svg(4, [project([shape.fields[0], shape.fields[1]]), shape.fields[5], shape.fields[4], toText(printf("alignment-baseline:%s; text-anchor:%s;"))(va_1)(ha_1) + Svg_formatStyle(ctx.Definitions, ctx.Style)]);
908
957
  }
909
958
  case 3:
910
- return new Svg_Svg(1, [project([shape.fields[0], shape.fields[1]]), [shape.fields[2], shape.fields[3]], Svg_formatStyle(ctx.Definitions, ctx.Style)]);
911
- case 6: {
959
+ return new Svg_Svg(2, [project([shape.fields[0], shape.fields[1]]), [shape.fields[2], shape.fields[3]], Svg_formatStyle(ctx.Definitions, ctx.Style)]);
960
+ case 7: {
912
961
  ctx_mut = ctx;
913
962
  area__mut = area_1[0];
914
963
  area__1_mut = area_1[1];
@@ -1123,7 +1172,7 @@ export function Events_triggerEvent(area__mut, area__1_mut, area__2_mut, area__3
1123
1172
  continue Events_triggerEvent;
1124
1173
  break;
1125
1174
  }
1126
- case 8: {
1175
+ case 9: {
1127
1176
  const dy = shape.fields[0][1];
1128
1177
  const dx = shape.fields[0][0];
1129
1178
  area__mut = (x1 + dx);
@@ -1138,7 +1187,7 @@ export function Events_triggerEvent(area__mut, area__1_mut, area__2_mut, area__3
1138
1187
  continue Events_triggerEvent;
1139
1188
  break;
1140
1189
  }
1141
- case 9: {
1190
+ case 10: {
1142
1191
  area__mut = Projections_projectOneX(x1, x2)(sx)(shape.fields[0]);
1143
1192
  area__1_mut = y1;
1144
1193
  area__2_mut = Projections_projectOneX(x1, x2)(sx)(shape.fields[1]);
@@ -1151,7 +1200,7 @@ export function Events_triggerEvent(area__mut, area__1_mut, area__2_mut, area__3
1151
1200
  continue Events_triggerEvent;
1152
1201
  break;
1153
1202
  }
1154
- case 10: {
1203
+ case 11: {
1155
1204
  area__mut = x1;
1156
1205
  area__1_mut = Projections_projectOneY(y1, y2)(sy)(shape.fields[0]);
1157
1206
  area__2_mut = x2;
@@ -1164,7 +1213,7 @@ export function Events_triggerEvent(area__mut, area__1_mut, area__2_mut, area__3
1164
1213
  continue Events_triggerEvent;
1165
1214
  break;
1166
1215
  }
1167
- case 7: {
1216
+ case 8: {
1168
1217
  const isy_1 = shape.fields[2];
1169
1218
  const isx_1 = shape.fields[1];
1170
1219
  const calculateNestedRange = (rev) => ((tupledArg) => ((ins) => {
@@ -1203,14 +1252,14 @@ export function Events_triggerEvent(area__mut, area__1_mut, area__2_mut, area__3
1203
1252
  continue Events_triggerEvent;
1204
1253
  break;
1205
1254
  }
1206
- case 5: {
1255
+ case 6: {
1207
1256
  const shapes = shape.fields[0];
1208
1257
  for (let idx = 0; idx <= (shapes.length - 1); idx++) {
1209
1258
  Events_triggerEvent(area_1[0], area_1[1], area_1[2], area_1[3], scales_1[0], scales_1[1], item_2(idx, shapes), jse, event);
1210
1259
  }
1211
1260
  break;
1212
1261
  }
1213
- case 6: {
1262
+ case 7: {
1214
1263
  const localEvent = Events_projectEvent(area_1[0], area_1[1], area_1[2], area_1[3], scales_1[0], scales_1[1], event);
1215
1264
  if (Events_inScales(scales_1[0], scales_1[1], localEvent)) {
1216
1265
  const enumerator = getEnumerator(shape.fields[0]);
@@ -1355,20 +1404,24 @@ export function Events_triggerEvent(area__mut, area__1_mut, area__2_mut, area__3
1355
1404
  }
1356
1405
  }
1357
1406
 
1407
+ export function Derived_PreserveAspectRatio(pa, s) {
1408
+ return new Shape(1, [new StyleConfig(4, [pa]), s]);
1409
+ }
1410
+
1358
1411
  export function Derived_StrokeColor(clr, s) {
1359
- return new Shape(0, [(s_1) => (new Style([1, new Color(1, [clr])], s_1.StrokeWidth, s_1.StrokeDashArray, s_1.Fill, s_1.Animation, s_1.Font, s_1.Cursor, s_1.FormatAxisXLabel, s_1.FormatAxisYLabel)), s]);
1412
+ return new Shape(1, [new StyleConfig(2, [clr]), s]);
1360
1413
  }
1361
1414
 
1362
1415
  export function Derived_FillColor(clr, s) {
1363
- return new Shape(0, [(s_1) => (new Style(s_1.StrokeColor, s_1.StrokeWidth, s_1.StrokeDashArray, new FillStyle(0, [[1, new Color(1, [clr])]]), s_1.Animation, s_1.Font, s_1.Cursor, s_1.FormatAxisXLabel, s_1.FormatAxisYLabel)), s]);
1416
+ return new Shape(1, [new StyleConfig(1, [clr]), s]);
1364
1417
  }
1365
1418
 
1366
1419
  export function Derived_Font(font, clr, s) {
1367
- return new Shape(0, [(s_1) => (new Style([0, new Color(1, [clr])], s_1.StrokeWidth, s_1.StrokeDashArray, new FillStyle(0, [[1, new Color(1, [clr])]]), s_1.Animation, font, s_1.Cursor, s_1.FormatAxisXLabel, s_1.FormatAxisYLabel)), s]);
1420
+ return new Shape(1, [new StyleConfig(3, [font, clr]), s]);
1368
1421
  }
1369
1422
 
1370
1423
  export function Derived_Area(line) {
1371
- return new Shape(8, [delay(() => {
1424
+ return new Shape(9, [delay(() => {
1372
1425
  const line_1 = Array.from(line);
1373
1426
  const matchValue = item_2(0, line_1)[0];
1374
1427
  const matchValue_1 = item_2(line_1.length - 1, line_1)[0];
@@ -1378,7 +1431,7 @@ export function Derived_Area(line) {
1378
1431
  }
1379
1432
 
1380
1433
  export function Derived_VArea(line) {
1381
- return new Shape(8, [delay(() => {
1434
+ return new Shape(9, [delay(() => {
1382
1435
  const line_1 = Array.from(line);
1383
1436
  const matchValue = item_2(0, line_1)[1];
1384
1437
  const matchValue_1 = item_2(line_1.length - 1, line_1)[1];
@@ -1388,7 +1441,7 @@ export function Derived_VArea(line) {
1388
1441
  }
1389
1442
 
1390
1443
  export function Derived_VShiftedArea(offs, line) {
1391
- return new Shape(8, [delay(() => {
1444
+ return new Shape(9, [delay(() => {
1392
1445
  const line_1 = Array.from(line);
1393
1446
  const matchValue = item_2(0, line_1)[1];
1394
1447
  const matchValue_1 = item_2(line_1.length - 1, line_1)[1];
@@ -1398,11 +1451,11 @@ export function Derived_VShiftedArea(offs, line) {
1398
1451
  }
1399
1452
 
1400
1453
  export function Derived_Bar(x, y) {
1401
- return new Shape(8, [delay(() => append(singleton([new Value(1, [x]), new Value(0, [y, 0])]), delay(() => append(singleton([new Value(1, [x]), new Value(0, [y, 1])]), delay(() => append(singleton([new Value(1, [new continuous(0)]), new Value(0, [y, 1])]), delay(() => singleton([new Value(1, [new continuous(0)]), new Value(0, [y, 0])]))))))))]);
1454
+ return new Shape(9, [delay(() => append(singleton([new Value(1, [x]), new Value(0, [y, 0])]), delay(() => append(singleton([new Value(1, [x]), new Value(0, [y, 1])]), delay(() => append(singleton([new Value(1, [new continuous(0)]), new Value(0, [y, 1])]), delay(() => singleton([new Value(1, [new continuous(0)]), new Value(0, [y, 0])]))))))))]);
1402
1455
  }
1403
1456
 
1404
1457
  export function Derived_Column(x, y) {
1405
- return new Shape(8, [delay(() => append(singleton([new Value(0, [x, 0]), new Value(1, [y])]), delay(() => append(singleton([new Value(0, [x, 1]), new Value(1, [y])]), delay(() => append(singleton([new Value(0, [x, 1]), new Value(1, [new continuous(0)])]), delay(() => singleton([new Value(0, [x, 0]), new Value(1, [new continuous(0)])]))))))))]);
1458
+ return new Shape(9, [delay(() => append(singleton([new Value(0, [x, 0]), new Value(1, [y])]), delay(() => append(singleton([new Value(0, [x, 1]), new Value(1, [y])]), delay(() => append(singleton([new Value(0, [x, 1]), new Value(1, [new continuous(0)])]), delay(() => singleton([new Value(0, [x, 0]), new Value(1, [new continuous(0)])]))))))))]);
1406
1459
  }
1407
1460
 
1408
1461
  export function Compost_niceNumber(num, decs) {
@@ -1445,7 +1498,7 @@ export function Compost_defaultFormat(scale, value) {
1445
1498
  }
1446
1499
  }
1447
1500
 
1448
- export const Compost_defstyle = new Style([1, new Color(0, [256, 0, 0])], new Width(2), [], new FillStyle(0, [[1, new Color(0, [196, 196, 196])]]), undefined, "10pt sans-serif", "default", Compost_defaultFormat, Compost_defaultFormat);
1501
+ export const Compost_defstyle = new Style([1, new Color(0, [256, 0, 0])], new Width(2), [], new FillStyle(0, [[1, new Color(0, [196, 196, 196])]]), undefined, "10pt sans-serif", "default", "", Compost_defaultFormat, Compost_defaultFormat);
1449
1502
 
1450
1503
  export function Compost_getRelativeLocation(el, x, y) {
1451
1504
  const getOffset = (parent_mut, tupledArg_mut) => {
package/dist/html.js CHANGED
@@ -7,7 +7,7 @@ import { equals, createAtom, defaultOf, createObj, disposeSafe, getEnumerator }
7
7
  import { array_type, tuple_type, union_type, obj_type, string_type, lambda_type, unit_type, class_type } from "./fable_modules/fable-library-js.4.28.0/Reflection.js";
8
8
  import { patch, diff, h as h_1 } from "virtual-dom";
9
9
  import { toArray as toArray_1, singleton, empty, append as append_1 } from "./fable_modules/fable-library-js.4.28.0/List.js";
10
- import { item, map as map_1 } from "./fable_modules/fable-library-js.4.28.0/Array.js";
10
+ import { fold, item, map as map_1 } from "./fable_modules/fable-library-js.4.28.0/Array.js";
11
11
  import { Event as Event$ } from "./fable_modules/fable-library-js.4.28.0/Event.js";
12
12
  import { startImmediate } from "./fable_modules/fable-library-js.4.28.0/Async.js";
13
13
  import { singleton as singleton_1 } from "./fable_modules/fable-library-js.4.28.0/AsyncBuilder.js";
@@ -212,6 +212,15 @@ export function createVirtualDomApp(id, initial, r, u) {
212
212
  }, event.Publish);
213
213
  }
214
214
 
215
+ export function foldDom(f, acc, node) {
216
+ if (node.tag === 1) {
217
+ return fold((acc_2, node_1) => foldDom(f, acc_2, node_1), f(acc, node.fields[0], node.fields[1], node.fields[2]), node.fields[3]);
218
+ }
219
+ else {
220
+ return acc;
221
+ }
222
+ }
223
+
215
224
  export function text(s_1) {
216
225
  return new DomNode(0, [s_1]);
217
226
  }
package/package.json CHANGED
@@ -1,17 +1,20 @@
1
1
  {
2
2
  "name": "compostjs",
3
- "version": "0.0.12",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Composable data visualization library for JavaScript",
6
6
  "author": "Tomas Petricek",
7
7
  "license": "MIT",
8
8
  "scripts": {
9
9
  "start": "dotnet fable watch src/compost/compost.fsproj --run npx vite",
10
- "build": "dotnet fable src/compost/compost.fsproj -o dist && rm dist/fable_modules/.gitignore",
10
+ "build": "dotnet fable src/compost/compost.fsproj -o dist && rm -f dist/fable_modules/.gitignore",
11
11
  "rebuild": "rm -rf dist && npm run build",
12
- "standalone": "dotnet fable src/compost/compost.fsproj --run npx vite build && copy-latest.sh",
13
- "updatejs": "npm run standalone && git add . && git commit -m \"Update standalone release file in docs\"",
14
- "release": "np --yolo --no-release-draft --no-publish && npm run updatejs && npm run rebuild && npm publish"
12
+ "standalone": "dotnet fable src/compost/compost.fsproj --run npx vite build && sh copy-latest.sh",
13
+ "test": "dotnet fable src/compost/compost.fsproj && npx vitest run",
14
+ "release:version": "np --yolo --no-release-draft --no-publish",
15
+ "release:standalone": "npm run standalone && git add . && git commit -m \"Update standalone release file in docs\"",
16
+ "release:publish": "npm run rebuild && npm publish",
17
+ "release": "npm run release:version && npm run release:standalone && npm run release:publish"
15
18
  },
16
19
  "repository": {
17
20
  "type": "git",
@@ -32,6 +35,7 @@
32
35
  },
33
36
  "devDependencies": {
34
37
  "np": "^11.0.2",
35
- "vite": "^6.0.0"
38
+ "vite": "^6.0.0",
39
+ "vitest": "^4.0.18"
36
40
  }
37
41
  }
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { scale as s, compost as c } from '../src/compost/compost.fs.js';
3
+
4
+ // Collect all elements matching a predicate using the F# foldDom helper.
5
+ // The fold callback receives (acc, tag, attrs) where attrs is a plain
6
+ // object with string attribute values (Events/Properties are excluded).
7
+ function findElements(node, pred) {
8
+ return c.foldDom((acc, tag, attrs) => {
9
+ if (pred(tag, attrs)) acc.push({ tag, attrs });
10
+ return acc;
11
+ }, [], node);
12
+ }
13
+
14
+ // Parse an SVG path "d" attribute into an array of {cmd, x, y} objects.
15
+ // Handles format like "M0 200 L200 0 " where command letter is directly
16
+ // followed by the x coordinate.
17
+ function parsePath(d) {
18
+ const coords = [];
19
+ for (const m of d.matchAll(/([ML])(-?[\d.]+)\s+(-?[\d.]+)/g)) {
20
+ coords.push({ cmd: m[1], x: parseFloat(m[2]), y: parseFloat(m[3]) });
21
+ }
22
+ return coords;
23
+ }
24
+
25
+ describe('style rendering', () => {
26
+ it('renders a shape with fill color set', () => {
27
+ const sh = c.shape([[0, 0], [1, 0], [1, 1], [0, 1]]);
28
+ const svg = c.svg(200, 200, c.fillColor("red", sh));
29
+ const paths = findElements(svg, (tag) => tag === 'path');
30
+ expect(paths.length).toBe(1);
31
+ expect(paths[0].attrs.style).toContain('fill:red');
32
+ });
33
+
34
+ it('renders a line with stroke color set', () => {
35
+ const line = c.line([[0, 0], [1, 1]]);
36
+ const svg = c.svg(200, 200, c.strokeColor("blue", line));
37
+ const paths = findElements(svg, (tag) => tag === 'path');
38
+ expect(paths.length).toBe(1);
39
+ expect(paths[0].attrs.style).toContain('stroke:blue');
40
+ });
41
+ });
42
+
43
+ describe('svg rendering', () => {
44
+ it('renders a line from (0,0) to (1,1) as an SVG path', () => {
45
+ const line = c.line([[0, 0], [1, 1]]);
46
+ const svg = c.svg(200, 200, line);
47
+
48
+ const paths = findElements(svg, (tag) => tag === 'path');
49
+ expect(paths.length).toBe(1);
50
+
51
+ const coords = parsePath(paths[0].attrs.d);
52
+
53
+ // X: 0->0, 1->200. Y: 0->200, 1->0 (SVG Y-axis is inverted)
54
+ expect(coords).toHaveLength(2);
55
+ expect(coords[0]).toEqual({ cmd: 'M', x: 0, y: 200 });
56
+ expect(coords[1]).toEqual({ cmd: 'L', x: 200, y: 0 });
57
+ });
58
+
59
+ it('renders a column as a path filling the full SVG area', () => {
60
+ const col = c.column("test", 10);
61
+ const svg = c.svg(200, 200, col);
62
+
63
+ const paths = findElements(svg, (tag) => tag === 'path');
64
+ expect(paths.length).toBe(1);
65
+
66
+ const coords = parsePath(paths[0].attrs.d);
67
+
68
+ // Single column auto-scales to fill the entire 200x200 area
69
+ // Closed path covering all four corners
70
+ expect(coords).toHaveLength(5);
71
+ expect(coords[0]).toEqual({ cmd: 'M', x: 0, y: 0 });
72
+ expect(coords[1]).toEqual({ cmd: 'L', x: 200, y: 0 });
73
+ expect(coords[2]).toEqual({ cmd: 'L', x: 200, y: 200 });
74
+ expect(coords[3]).toEqual({ cmd: 'L', x: 0, y: 200 });
75
+ expect(coords[4]).toEqual({ cmd: 'L', x: 0, y: 0 });
76
+ });
77
+ });
78
+
79
+ describe('serialization', () => {
80
+ it('round-trips a line through serialize/deserialize', () => {
81
+ const line = c.line([[0, 0], [1, 1]]);
82
+ const line2 = c.deserialize(c.serialize(line));
83
+ const paths1 = findElements(c.svg(200, 200, line), (tag) => tag === 'path');
84
+ const paths2 = findElements(c.svg(200, 200, line2), (tag) => tag === 'path');
85
+ expect(paths2[0].attrs.d).toEqual(paths1[0].attrs.d);
86
+ });
87
+
88
+ it('round-trips a styled shape through serialize/deserialize', () => {
89
+ const shape = c.fillColor("red", c.strokeColor("blue", c.line([[0, 0], [1, 1]])));
90
+ const shape2 = c.deserialize(c.serialize(shape));
91
+ const paths1 = findElements(c.svg(200, 200, shape), (tag) => tag === 'path');
92
+ const paths2 = findElements(c.svg(200, 200, shape2), (tag) => tag === 'path');
93
+ expect(paths2[0].attrs.style).toEqual(paths1[0].attrs.style);
94
+ expect(paths2[0].attrs.d).toEqual(paths1[0].attrs.d);
95
+ });
96
+
97
+ it('round-trips a multi-series bar chart through serialize/deserialize', () => {
98
+ const chart = c.axes("left bottom",
99
+ c.scaleY(s.continuous(0, 50),
100
+ c.overlay([
101
+ c.padding(0, 5, 0, 5, c.fillColor("#DC4B4A", c.column("apples", 30))),
102
+ c.padding(0, 5, 0, 5, c.fillColor("#424498", c.column("plums", 20))),
103
+ c.padding(0, 5, 0, 5, c.fillColor("#A0CB5B", c.column("kiwi", 40))),
104
+ ])
105
+ )
106
+ );
107
+ const chart2 = c.deserialize(c.serialize(chart));
108
+ const paths1 = findElements(c.svg(400, 300, chart), (tag) => tag === 'path');
109
+ const paths2 = findElements(c.svg(400, 300, chart2), (tag) => tag === 'path');
110
+ expect(paths2.length).toBe(paths1.length);
111
+ paths1.forEach((p, i) => {
112
+ expect(paths2[i].attrs.d).toEqual(p.attrs.d);
113
+ expect(paths2[i].attrs.style).toEqual(p.attrs.style);
114
+ });
115
+ });
116
+ });