ggaction 0.0.2 → 0.0.4

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.
Files changed (242) hide show
  1. package/CHANGELOG.md +55 -2
  2. package/README.md +95 -72
  3. package/package.json +15 -5
  4. package/src/actions/boxPlots/components.js +3 -3
  5. package/src/actions/boxPlots/edit.js +277 -0
  6. package/src/actions/boxPlots/index.js +2 -0
  7. package/src/actions/boxPlots/materialize.js +12 -1
  8. package/src/actions/boxPlots/options.js +25 -14
  9. package/src/actions/canvas/actions.js +21 -17
  10. package/src/actions/composition/actions.js +247 -0
  11. package/src/actions/composition/index.js +1 -0
  12. package/src/actions/coordinates/actions.js +2 -5
  13. package/src/actions/data/density.js +7 -1
  14. package/src/actions/data/filter.js +3 -1
  15. package/src/actions/data/regression.js +1 -1
  16. package/src/actions/encodings/appearance.js +12 -1
  17. package/src/actions/encodings/barWidth.js +5 -3
  18. package/src/actions/encodings/color/continuous.js +137 -0
  19. package/src/actions/encodings/color/index.js +151 -0
  20. package/src/actions/encodings/color/layout.js +91 -0
  21. package/src/actions/encodings/color/policy.js +88 -0
  22. package/src/actions/encodings/index.js +4 -2
  23. package/src/actions/encodings/offset.js +91 -73
  24. package/src/actions/encodings/position/apply.js +66 -0
  25. package/src/actions/encodings/position/index.js +130 -0
  26. package/src/actions/encodings/position/policies/arc.js +26 -0
  27. package/src/actions/encodings/position/policies/bar.js +9 -5
  28. package/src/actions/encodings/position/policies/index.js +5 -1
  29. package/src/actions/encodings/position/policies/line.js +20 -3
  30. package/src/actions/encodings/position/resolve.js +58 -9
  31. package/src/actions/encodings/ranged.js +30 -8
  32. package/src/actions/encodings/shared.js +17 -21
  33. package/src/actions/encodings/strokeDash.js +1 -1
  34. package/src/actions/encodings/text.js +73 -0
  35. package/src/actions/errorBands/create.js +37 -29
  36. package/src/actions/errorBands/edit.js +206 -0
  37. package/src/actions/errorBands/index.js +9 -0
  38. package/src/actions/errorBands/options.js +39 -0
  39. package/src/actions/errorBars/create.js +50 -43
  40. package/src/actions/errorBars/edit.js +130 -0
  41. package/src/actions/errorBars/index.js +3 -0
  42. package/src/actions/errorBars/options.js +52 -0
  43. package/src/actions/facets/actions.js +162 -0
  44. package/src/actions/facets/derive.js +200 -0
  45. package/src/actions/facets/guides.js +16 -0
  46. package/src/actions/facets/index.js +1 -0
  47. package/src/actions/facets/replay.js +69 -0
  48. package/src/actions/guides/applicability.js +113 -0
  49. package/src/actions/guides/axes/axes.js +70 -16
  50. package/src/actions/guides/axes/edit.js +126 -0
  51. package/src/actions/guides/axes/index.js +12 -0
  52. package/src/actions/guides/axes/labels.js +22 -19
  53. package/src/actions/guides/axes/remove.js +60 -0
  54. package/src/actions/guides/axes/tickGroups.js +7 -5
  55. package/src/actions/guides/axes/ticks.js +10 -5
  56. package/src/actions/guides/axes/titles.js +8 -6
  57. package/src/actions/guides/grids/grid.js +144 -15
  58. package/src/actions/guides/grids/resolve.js +6 -14
  59. package/src/actions/guides/guides.js +17 -75
  60. package/src/actions/guides/index.js +6 -0
  61. package/src/actions/guides/legends/categorical/actions.js +48 -9
  62. package/src/actions/guides/legends/categorical/components.js +16 -16
  63. package/src/actions/guides/legends/categorical/index.js +14 -14
  64. package/src/actions/guides/legends/categorical/layout.js +7 -3
  65. package/src/actions/guides/legends/categorical/options.js +2 -5
  66. package/src/actions/guides/legends/categorical/resolve.js +2 -2
  67. package/src/actions/guides/legends/categorical/symbols.js +14 -14
  68. package/src/actions/guides/legends/continuous/common.js +7 -7
  69. package/src/actions/guides/legends/continuous/interval.js +6 -6
  70. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  71. package/src/actions/guides/legends/edit.js +4 -26
  72. package/src/actions/guides/legends/focused.js +63 -0
  73. package/src/actions/guides/legends/index.js +4 -0
  74. package/src/actions/guides/legends/remove.js +46 -0
  75. package/src/actions/guides/legends/size.js +13 -15
  76. package/src/actions/guides/legends/target.js +25 -0
  77. package/src/actions/guides/polar/axes/facade.js +219 -0
  78. package/src/actions/guides/polar/axes/index.js +55 -0
  79. package/src/actions/guides/polar/axes/labels.js +188 -0
  80. package/src/actions/guides/polar/axes/lines.js +113 -0
  81. package/src/actions/guides/polar/axes/shared.js +112 -0
  82. package/src/actions/guides/polar/axes/ticks.js +164 -0
  83. package/src/actions/guides/polar/axes/titles.js +165 -0
  84. package/src/actions/guides/polar/grids.js +252 -0
  85. package/src/actions/guides/polar/index.js +2 -0
  86. package/src/actions/guides/polar/resolve.js +269 -0
  87. package/src/actions/guides/tickValues.js +1 -1
  88. package/src/actions/index.js +4 -0
  89. package/src/actions/intervals/resolve.js +1 -1
  90. package/src/actions/marks/arc/actions.js +239 -0
  91. package/src/actions/marks/arc/index.js +1 -0
  92. package/src/actions/marks/{area.js → area/actions.js} +51 -38
  93. package/src/actions/marks/area/index.js +4 -0
  94. package/src/actions/marks/bar/create.js +26 -5
  95. package/src/actions/marks/bar/edit.js +1 -1
  96. package/src/actions/marks/bar/materialize.js +25 -31
  97. package/src/actions/marks/index.js +12 -4
  98. package/src/actions/marks/lifecycle.js +27 -0
  99. package/src/actions/marks/line/actions.js +373 -0
  100. package/src/actions/marks/line/index.js +1 -0
  101. package/src/actions/marks/{point.js → point/actions.js} +67 -108
  102. package/src/actions/marks/point/index.js +1 -0
  103. package/src/actions/marks/rect/actions.js +165 -0
  104. package/src/actions/marks/rect/index.js +1 -0
  105. package/src/actions/marks/remove.js +214 -0
  106. package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
  107. package/src/actions/marks/rule/index.js +1 -0
  108. package/src/actions/marks/shared.js +156 -25
  109. package/src/actions/marks/text/actions.js +192 -0
  110. package/src/actions/marks/text/index.js +1 -0
  111. package/src/actions/primitives/createGraphics.js +13 -6
  112. package/src/actions/primitives/editGraphics.js +10 -3
  113. package/src/actions/primitives/semantic.js +26 -3
  114. package/src/actions/primitives/semanticValue.js +26 -4
  115. package/src/actions/regression/components.js +2 -2
  116. package/src/actions/regression/create.js +19 -2
  117. package/src/actions/regression/edit.js +247 -0
  118. package/src/actions/regression/index.js +3 -0
  119. package/src/actions/regression/resolve.js +3 -1
  120. package/src/actions/scales/consumers/common.js +91 -0
  121. package/src/actions/scales/consumers/families.js +47 -0
  122. package/src/actions/scales/consumers/index.js +41 -0
  123. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
  124. package/src/actions/scales/create.js +1 -1
  125. package/src/actions/scales/definitions.js +15 -6
  126. package/src/actions/scales/edit.js +45 -22
  127. package/src/actions/scales/materialize.js +58 -409
  128. package/src/actions/selection/actions.js +38 -24
  129. package/src/actions/selection/index.js +2 -0
  130. package/src/actions/titles/actions.js +63 -2
  131. package/src/actions/titles/index.js +2 -0
  132. package/src/actions/titles/resolve.js +12 -17
  133. package/src/composition.js +10 -0
  134. package/src/core/ChartProgram.js +49 -65
  135. package/src/core/action.js +10 -0
  136. package/src/core/compositionState.js +169 -0
  137. package/src/core/materializationState.js +65 -0
  138. package/src/core/programState.js +28 -0
  139. package/src/core/validation.js +16 -0
  140. package/src/core/vocabulary.js +154 -4
  141. package/src/grammar/arcs.js +154 -0
  142. package/src/grammar/areaSeries.js +1 -1
  143. package/src/grammar/bars/aggregate.js +3 -3
  144. package/src/grammar/bars/geometry.js +3 -3
  145. package/src/grammar/bars/policy.js +7 -1
  146. package/src/grammar/coordinates.js +3 -7
  147. package/src/grammar/density.js +21 -2
  148. package/src/grammar/facets/dependencies.js +185 -0
  149. package/src/grammar/facets/guides.js +257 -0
  150. package/src/grammar/facets/index.js +122 -0
  151. package/src/grammar/facets/scales.js +254 -0
  152. package/src/grammar/interval.js +2 -96
  153. package/src/grammar/lineSeries.js +112 -4
  154. package/src/grammar/palettes.js +5 -7
  155. package/src/grammar/polar.js +102 -0
  156. package/src/grammar/polarGuides.js +243 -0
  157. package/src/grammar/polarLineCommands.js +43 -0
  158. package/src/grammar/polarPaths.js +161 -0
  159. package/src/grammar/positionCompatibility.js +18 -2
  160. package/src/grammar/rects.js +26 -0
  161. package/src/grammar/regression/derive.js +97 -0
  162. package/src/grammar/regression/index.js +22 -0
  163. package/src/grammar/regression/models.js +282 -0
  164. package/src/grammar/regression/parameters.js +106 -0
  165. package/src/grammar/rules.js +1 -1
  166. package/src/grammar/scales/color.js +4 -2
  167. package/src/grammar/scales/continuous.js +6 -1
  168. package/src/grammar/scales/fields.js +7 -0
  169. package/src/grammar/scales/ordinal.js +5 -2
  170. package/src/grammar/scales/policies.js +1 -1
  171. package/src/grammar/schemas/graphic.js +1 -1
  172. package/src/grammar/schemas/graphicBounds.js +64 -9
  173. package/src/grammar/schemas/graphicTree.js +44 -10
  174. package/src/grammar/schemas/semanticPath.js +31 -9
  175. package/src/grammar/seriesLayout.js +13 -2
  176. package/src/grammar/statistics/studentT.js +98 -0
  177. package/src/grammar/text.js +71 -0
  178. package/src/grammar/ticks.js +50 -13
  179. package/src/grammar/transforms.js +1 -1
  180. package/src/index.js +1 -0
  181. package/src/layout/composition.js +310 -0
  182. package/src/layout/facets.js +131 -0
  183. package/src/layout/title.js +6 -0
  184. package/src/materialization/bars/aggregate.js +6 -3
  185. package/src/materialization/bars/grouped.js +75 -37
  186. package/src/materialization/bars/histogram.js +1 -1
  187. package/src/materialization/bars/ranged.js +1 -1
  188. package/src/materialization/composition.js +152 -0
  189. package/src/materialization/compositionSnapshot.js +114 -0
  190. package/src/materialization/density.js +1 -1
  191. package/src/materialization/dependencies.js +47 -73
  192. package/src/materialization/encodings.js +14 -28
  193. package/src/materialization/facetGuides/index.js +2 -0
  194. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  195. package/src/materialization/facetGuides/placement.js +126 -0
  196. package/src/materialization/facetGuides/preparation.js +188 -0
  197. package/src/materialization/facets.js +214 -0
  198. package/src/materialization/graphicHierarchy.js +15 -5
  199. package/src/materialization/guides/resources.js +46 -0
  200. package/src/materialization/marks/capabilities.js +141 -0
  201. package/src/materialization/marks/index.js +105 -0
  202. package/src/materialization/marks/policies.js +87 -0
  203. package/src/materialization/planner.js +105 -12
  204. package/src/materialization/rect.js +138 -0
  205. package/src/materialization/rectConfig.js +38 -0
  206. package/src/materialization/rowEncoding.js +33 -0
  207. package/src/materialization/scaleGuideDependencies.js +94 -0
  208. package/src/materialization/scales/map.js +40 -0
  209. package/src/materialization/scales/policies/arc.js +37 -0
  210. package/src/materialization/scales/policies/bar.js +132 -0
  211. package/src/materialization/scales/policies/series.js +76 -0
  212. package/src/materialization/scales/resolve.js +326 -0
  213. package/src/materialization/selection/items/arc.js +29 -0
  214. package/src/materialization/selection/items/bar.js +255 -0
  215. package/src/materialization/selection/items/common.js +131 -0
  216. package/src/materialization/selection/items/index.js +6 -0
  217. package/src/materialization/selection/items/path.js +59 -0
  218. package/src/materialization/selection/items/point.js +55 -0
  219. package/src/materialization/selection/items/rect.js +23 -0
  220. package/src/materialization/selection/items/rule.js +25 -0
  221. package/src/materialization/selection/policies/arc.js +10 -0
  222. package/src/materialization/selection/policies/area.js +1 -1
  223. package/src/materialization/selection/policies/bar.js +1 -1
  224. package/src/materialization/selection/policies/index.js +5 -1
  225. package/src/materialization/selection/policies/line.js +1 -1
  226. package/src/materialization/selection/policies/point.js +1 -1
  227. package/src/materialization/selection/policies/rect.js +10 -0
  228. package/src/materialization/selection/policies/rule.js +1 -1
  229. package/src/materialization/selection/styles.js +25 -17
  230. package/src/materialization/text.js +156 -0
  231. package/src/renderers/canvas/index.js +68 -22
  232. package/src/renderers/canvas/text.js +14 -1
  233. package/types/extension.d.ts +5 -1
  234. package/types/index.d.ts +56 -1
  235. package/types/program.d.ts +664 -27
  236. package/src/actions/encodings/color.js +0 -329
  237. package/src/actions/encodings/position.js +0 -176
  238. package/src/actions/marks/line.js +0 -248
  239. package/src/grammar/regression.js +0 -578
  240. package/src/grammar/scales.js +0 -1
  241. package/src/materialization/marks.js +0 -110
  242. package/src/materialization/selection/items.js +0 -503
package/CHANGELOG.md CHANGED
@@ -2,6 +2,57 @@
2
2
 
3
3
  All notable changes to `ggaction` are recorded in this file.
4
4
 
5
+ ## Unreleased
6
+
7
+ ## [0.0.4] - 2026-07-19
8
+
9
+ ### Changed
10
+
11
+ - Transferred the canonical repository to the `ggaction` organization and moved public documentation to
12
+ `https://ggaction.github.io/ggaction/`.
13
+ - Made public documentation deployment release-scoped so ordinary `main` pushes continue to verify docs without
14
+ changing the published site.
15
+ - Refactored source ownership and materialization boundaries while preserving the public API, stored specifications,
16
+ trace hierarchy, and rendered output.
17
+
18
+ ### Fixed
19
+
20
+ - Normalized numeric Canvas font weights before rendering so valid intermediate values such as `650` retain normal
21
+ text geometry in both Browser Canvas and Node PNG output.
22
+ - Applied right categorical legend offsets from the plot boundary consistently during creation and focused layout
23
+ edits, including labels, titles, and optional backgrounds.
24
+ - Accepted `count` on sequential palette descriptors as a concrete gradient-stop count, consistently across
25
+ top-level palette shorthands, nested ranges, encodings, direct scales, and scale edits.
26
+ - Preserved concrete `ChartProgram` subclasses in the TypeScript signature of wrapped extension actions and added
27
+ a strict NodeNext declaration-merging authoring pattern.
28
+ - Routed every selective `llms.txt` target to a deployed HTML page, stabilized action fragments, and made built-site
29
+ checks validate both HTTP targets and DOM IDs.
30
+
31
+ ## [0.0.3] - 2026-07-19
32
+
33
+ ### Added
34
+
35
+ - Added complete Polar point, line/radar, arc/donut/rose/radial-bar authoring with theta/radius guides, selection,
36
+ highlighting, and Canvas/PNG rendering.
37
+ - Added immutable horizontal and vertical program composition, nested child snapshots, layout editing, stable child
38
+ replacement, and Cartesian facets with derived-data replay, scale resolution, outer axes, and shared legends.
39
+ - Added text and rect marks, directional offsets, horizontal grouped bars, and compatible shared temporal bar/line
40
+ position inference.
41
+
42
+ ### Changed
43
+
44
+ - Nested compositions now preserve their intrinsic layout and honor outer cross-axis alignment instead of stretching
45
+ internal cells or leaving unequal snapshots pinned to the start edge.
46
+
47
+ ### Fixed
48
+
49
+ - Corrected zero-baseline and signed geometry for aggregate bars, and materialized complete ranged or aggregate bars
50
+ with their documented default width.
51
+ - Made horizontal error bands compose with color and explicit boundaries in the same supported cases as vertical bands.
52
+ - Added standalone point-size legends and interval-aware temporal axis labels without duplicate automatic tick text.
53
+ - Published the exact `createDerivedData` transform-array contract and stable `ChartProgram` state-inspection paths in
54
+ TypeScript and user documentation.
55
+
5
56
  ## [0.0.2] - 2026-07-17
6
57
 
7
58
  ### Fixed
@@ -31,5 +82,7 @@ All notable changes to `ggaction` are recorded in this file.
31
82
  - Cartesian charts are the complete current path. Polar semantic tokens exist only where explicitly documented and do
32
83
  not imply complete polar rendering.
33
84
 
34
- [0.0.2]: https://github.com/hj-n/ggaction/releases/tag/v0.0.2
35
- [0.0.1]: https://github.com/hj-n/ggaction/releases/tag/v0.0.1
85
+ [0.0.4]: https://github.com/ggaction/ggaction/releases/tag/v0.0.4
86
+ [0.0.3]: https://github.com/ggaction/ggaction/releases/tag/v0.0.3
87
+ [0.0.2]: https://github.com/ggaction/ggaction/releases/tag/v0.0.2
88
+ [0.0.1]: https://github.com/ggaction/ggaction/releases/tag/v0.0.1
package/README.md CHANGED
@@ -1,108 +1,131 @@
1
1
  # ggaction
2
2
 
3
- `ggaction` is a JavaScript library for building charts through immutable,
4
- traceable actions.
3
+ [![npm version](https://img.shields.io/npm/v/ggaction.svg)](https://www.npmjs.com/package/ggaction) [![license](https://img.shields.io/npm/l/ggaction.svg)](./LICENSE) [![documentation](https://img.shields.io/badge/docs-ggaction.github.io-2563eb)](https://ggaction.github.io/ggaction/)
4
+
5
+ ### A grammar for how charts are made.
6
+
7
+ Most visualization grammars describe a finished chart. **ggaction** represents chart authoring itself as an immutable, traceable sequence of graphical actions.
8
+
9
+ Build, inspect, select, and revise charts one meaningful action at a time.
10
+
11
+ <p align="left">
12
+ <img src="./docs/assets/images/readme-authoring-sequence.gif" width="960" height="540" loading="eager" alt="A ggaction program progressively creates a scatterplot, adds grouped regression fits and confidence bands, highlights the Japan group across chart layers, and finishes with an R-squared annotation.">
13
+ </p>
14
+
15
+ Every frame is rendered from an immutable `ChartProgram`. The final R² label is a custom traceable action composed from ggaction's public extension primitives.
16
+
17
+ ```bash
18
+ npm install ggaction
19
+ ```
20
+
21
+ ## A grammar of graphical action
22
+
23
+ Actions are verbs:
24
+
25
+ `create · transform · encode · edit · select · compose`
26
+
27
+ Chart resources are nouns:
28
+
29
+ `data · marks · scales · coordinates · guides`
30
+
31
+ A `ChartProgram` is the immutable sentence they produce.
32
+
33
+ The following fragment assumes `cars` is an array of row objects:
34
+
35
+ ```javascript
36
+ import { chart } from "ggaction";
37
+
38
+ const program = chart()
39
+ .createCanvas()
40
+ .createData({ values: cars })
41
+ .createPointMark({ id: "points" })
42
+ .encodeX({ field: "Displacement" })
43
+ .encodeY({ field: "Acceleration" })
44
+ .encodeColor({ field: "Origin" })
45
+ .createRegression()
46
+ .createGuides();
47
+ ```
48
+
49
+ ## Why actions?
50
+
51
+ - **Progressive** — build and revise a chart one meaningful operation at a time.
52
+ - **Traceable** — retain high-level actions and the wrapped actions they invoke.
53
+ - **Materialized** — actions create concrete backend-neutral graphics; rendering does not perform hidden semantic compilation.
54
+
55
+ ## Quick start
56
+
57
+ ```bash
58
+ npm install ggaction
59
+ ```
5
60
 
6
61
  ```javascript
7
62
  import { chart, render } from "ggaction";
8
63
 
9
- const cars = [
10
- { horsepower: 88, mpg: 27, origin: "USA" },
11
- { horsepower: 70, mpg: 36, origin: "Japan" },
12
- { horsepower: 110, mpg: 24, origin: "Europe" }
64
+ const observations = [
65
+ { displacement: 97, acceleration: 14.5, origin: "Japan" },
66
+ { displacement: 140, acceleration: 15.5, origin: "USA" },
67
+ { displacement: 86, acceleration: 16.4, origin: "Japan" }
13
68
  ];
14
69
 
15
70
  const program = chart()
16
71
  .createCanvas({
17
72
  width: 640,
18
73
  height: 400,
19
- margin: { top: 30, right: 30, bottom: 60, left: 70 }
74
+ margin: { top: 30, right: 130, bottom: 60, left: 70 }
20
75
  })
21
- .createData({ values: cars })
76
+ .createData({ values: observations })
22
77
  .createPointMark()
23
- .encodeX({ field: "horsepower" })
24
- .encodeY({ field: "mpg" })
78
+ .encodeX({ field: "displacement" })
79
+ .encodeY({ field: "acceleration" })
25
80
  .encodeColor({ field: "origin" })
26
- .encodeRadius({ value: 3 })
27
- .createGuides({
28
- axes: {
29
- x: { title: { text: "Horsepower" } },
30
- y: { title: { text: "Miles per Gallon" } }
31
- }
32
- });
33
-
34
- render(program, document.querySelector("#chart").getContext("2d"));
81
+ .encodeRadius({ value: 4 })
82
+ .createGuides();
83
+
84
+ const context = document.querySelector("#chart").getContext("2d");
85
+ render(program, context);
35
86
  ```
36
87
 
37
- The renderer reads only fully materialized, backend-neutral graphics. Semantic
38
- state is never automatically compiled during rendering.
88
+ For the advanced version, follow the [regression recipe](https://ggaction.github.io/ggaction/recipes/regression-scatterplot/) or open the [runnable example](./examples/cars-regression-scatterplot/).
39
89
 
40
- > **Status:** `0.0.2` is the current experimental public release. APIs may change
41
- > before `1.0.0`; changes are recorded in the [changelog](./CHANGELOG.md).
90
+ ## What it supports
42
91
 
43
- ## Install
92
+ - Cartesian and polar charts
93
+ - Statistical layers and intervals
94
+ - Faceting and program composition
95
+ - Mark selection and coordinated highlighting
96
+ - Browser Canvas and Node PNG output
97
+ - TypeScript declarations and traceable extension actions
44
98
 
45
- ```bash
46
- npm install ggaction
47
- ```
99
+ See the current [supported features](https://ggaction.github.io/ggaction/supported-features/), [tutorials and examples](https://ggaction.github.io/ggaction/tutorials/), and [action reference](https://ggaction.github.io/ggaction/reference/actions/) for exact coverage. Runnable programs are collected in [`examples/`](./examples/README.md).
100
+
101
+ ## Package entries
48
102
 
49
- The package is ESM-only and supports Node.js 20 or later. The default `ggaction`
50
- entry works with modern browser build tools and Canvas. `ggaction/png` is
51
- Node-only and uses a native Canvas dependency.
103
+ The package is ESM-only and requires Node.js 20 or later.
52
104
 
53
105
  | Entry | Purpose |
54
106
  | --- | --- |
55
107
  | `ggaction` | Create chart programs and render them to Browser Canvas |
56
- | `ggaction/extension` | Author wrapped actions and use public low-level primitives |
108
+ | `ggaction/extension` | Author wrapped actions with public low-level primitives |
57
109
  | `ggaction/png` | Render a completed program to a PNG file in Node.js |
58
110
 
59
- All three entries include TypeScript declarations. See [Getting
60
- Started](https://hyeonword.com/ggaction/getting-started/) for a complete browser
61
- setup and [Rendering](https://hyeonword.com/ggaction/api/rendering/) for Node PNG
62
- output.
111
+ All entries include TypeScript declarations. The default entry is browser-safe; the PNG adapter is Node-only.
63
112
 
64
113
  ## Documentation
65
114
 
66
- - [npm package](https://www.npmjs.com/package/ggaction)
67
- - [Getting started](https://hyeonword.com/ggaction/getting-started/)
68
- - [Cars scatterplot tutorial](https://hyeonword.com/ggaction/tutorials/scatterplot/)
69
- - [Cars line chart tutorial](https://hyeonword.com/ggaction/tutorials/line-chart/)
70
- - [Cars histogram tutorial](https://hyeonword.com/ggaction/tutorials/histogram/)
71
- - [Bar chart tutorial](https://hyeonword.com/ggaction/tutorials/grouped-bar/)
72
- - [Regression scatterplot tutorial](https://hyeonword.com/ggaction/tutorials/regression-scatterplot/)
73
- - [Density area chart tutorial](https://hyeonword.com/ggaction/tutorials/density-area/)
74
- - [Error-bar chart tutorial](https://hyeonword.com/ggaction/tutorials/error-bar/)
75
- - [Error-band chart tutorial](https://hyeonword.com/ggaction/tutorials/error-band/)
76
- - [Mark selection and highlighting tutorial](https://hyeonword.com/ggaction/tutorials/mark-selection/)
77
- - [Box-plot API](https://hyeonword.com/ggaction/api/box-plots/)
78
- - [Chart concepts](https://hyeonword.com/ggaction/concepts/chart-program/)
79
- - [Complete action reference](https://hyeonword.com/ggaction/reference/actions/)
80
- - [Supported features](https://hyeonword.com/ggaction/supported-features/)
81
- - [Documentation index](https://hyeonword.com/ggaction/)
82
-
83
- The runnable [cars scatterplot](https://github.com/hj-n/ggaction/tree/main/examples/cars-scatterplot/),
84
- [aggregate line chart](https://github.com/hj-n/ggaction/tree/main/examples/cars-line-chart/), and
85
- [stacked histogram](https://github.com/hj-n/ggaction/tree/main/examples/cars-histogram/) use `data/cars.json`. The
86
- [regression scatterplot](https://github.com/hj-n/ggaction/tree/main/examples/cars-regression-scatterplot/) also uses the
87
- cars data and layers grouped fits with confidence bands. The [grouped bar
88
- chart](https://github.com/hj-n/ggaction/tree/main/examples/jobs-grouped-bar/) uses `data/jobs.json`. The [density area
89
- chart](https://github.com/hj-n/ggaction/tree/main/examples/cars-density-area/) derives grouped Acceleration
90
- distributions from the cars data. The [error-bar chart](https://github.com/hj-n/ggaction/tree/main/examples/cars-error-bar/)
91
- summarizes mean Acceleration with 95% confidence intervals. The
92
- [error-band chart](https://github.com/hj-n/ggaction/tree/main/examples/gapminder-error-band/) summarizes grouped
93
- confidence intervals as closed area paths over time.
94
- The [box plot](https://github.com/hj-n/ggaction/tree/main/examples/cars-box-plot/) supports vertical or horizontal
95
- Tukey/min–max ranges, configurable factor and component appearance, and
96
- optional outlier rendering from categorical and quantitative field pairs.
97
- The [mark-selection example](https://github.com/hj-n/ggaction/tree/main/examples/mark-selection/) compares grouped point,
98
- complete stacked-bar, and line-series highlighting through one selector grammar.
99
- Completed programs can also be exported through
100
- [`ggaction/png`](https://hyeonword.com/ggaction/api/rendering/#png-output).
101
-
102
- ## Development
115
+ - [Getting Started](https://ggaction.github.io/ggaction/getting-started/)
116
+ - [Tutorials and Examples](https://ggaction.github.io/ggaction/tutorials/)
117
+ - [Action Reference](https://ggaction.github.io/ggaction/reference/actions/)
118
+ - [Concepts](https://ggaction.github.io/ggaction/concepts/chart-program/)
119
+ - [Supported Features](https://ggaction.github.io/ggaction/supported-features/)
120
+
121
+ ## Status and development
122
+
123
+ > **Status:** `0.0.4` is the current experimental public release. APIs may change before `1.0.0`; changes are recorded in the [changelog](./CHANGELOG.md).
103
124
 
104
125
  ```bash
105
126
  npm install
127
+ npm run assets:readme
106
128
  npm test
107
129
  npm run test:render
130
+ npm run test:docs
108
131
  ```
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "ggaction",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Build charts through immutable, traceable graphical actions.",
5
5
  "license": "MIT",
6
6
  "author": "Hyeon Jeon",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/hj-n/ggaction.git"
9
+ "url": "git+https://github.com/ggaction/ggaction.git"
10
10
  },
11
- "homepage": "https://hyeonword.com/ggaction/",
11
+ "homepage": "https://ggaction.github.io/ggaction/",
12
12
  "bugs": {
13
- "url": "https://github.com/hj-n/ggaction/issues"
13
+ "url": "https://github.com/ggaction/ggaction/issues"
14
14
  },
15
15
  "keywords": [
16
16
  "visualization",
@@ -55,10 +55,18 @@
55
55
  "test:docs:browser": "node scripts/test-built-docs.js _site",
56
56
  "test:coverage": "node scripts/run-tests.js coverage",
57
57
  "test:render": "node test/support/artifacts.js && node scripts/run-tests.js render && node scripts/generate-roadmap-gallery.js && node scripts/test-roadmap-gallery.js",
58
+ "assets:readme": "node scripts/generate-readme-demo.js",
58
59
  "artifacts:gallery": "node scripts/generate-roadmap-gallery.js",
59
60
  "artifacts:gallery:test": "node scripts/test-roadmap-gallery.js",
60
61
  "docs:images": "node scripts/generate-doc-images.js",
61
62
  "docs:llms": "node scripts/generate-llm-docs.js",
63
+ "docs:capabilities": "node scripts/generate-doc-capabilities.js",
64
+ "docs:capabilities:check": "node scripts/generate-doc-capabilities.js --check",
65
+ "docs:signatures": "node scripts/generate-doc-signatures.js",
66
+ "docs:signatures:check": "node scripts/generate-doc-signatures.js --check",
67
+ "docs:generate": "npm run docs:signatures && npm run docs:capabilities && npm run docs:images && npm run docs:llms",
68
+ "docs:build": "bundle exec jekyll build --source docs --destination _site --baseurl /ggaction",
69
+ "docs:verify": "npm run docs:generate && npm run test:docs && npm run docs:build && npm run test:docs:built && npm run test:docs:browser",
62
70
  "contracts:catalog": "node scripts/generate-action-catalog.js",
63
71
  "contracts:catalog:check": "node scripts/generate-action-catalog.js --check",
64
72
  "package:check": "node scripts/package-artifact.js --check",
@@ -79,7 +87,9 @@
79
87
  },
80
88
  "devDependencies": {
81
89
  "es-module-lexer": "^1.7.0",
90
+ "gifenc": "^1.0.3",
82
91
  "playwright": "^1.61.1",
83
- "typescript": "^7.0.2"
92
+ "typescript": "^7.0.2",
93
+ "vite": "^6.4.3"
84
94
  }
85
95
  }
@@ -64,7 +64,8 @@ export const createBoxOutliers = action(
64
64
  let next = this.createPointMark({
65
65
  id: args.id,
66
66
  data: args.data,
67
- shape: args.shape
67
+ shape: args.shape,
68
+ fill: "#111111"
68
69
  });
69
70
  next = next[categoryAction]({
70
71
  target: args.id,
@@ -82,7 +83,6 @@ export const createBoxOutliers = action(
82
83
  });
83
84
  return next
84
85
  .encodeRadius({ target: args.id, value: args.radius })
85
- .encodeOpacity({ target: args.id, value: args.opacity })
86
- .editGraphics({ target: args.id, property: "fill", value: "#111111" });
86
+ .encodeOpacity({ target: args.id, value: args.opacity });
87
87
  }
88
88
  );
@@ -0,0 +1,277 @@
1
+ import { action } from "../../core/action.js";
2
+ import { isPlainObject } from "../../core/immutable.js";
3
+ import { validateUserId } from "../../core/identifiers.js";
4
+ import { validateKeys } from "../../core/validation.js";
5
+ import { BOX_FIELDS, deriveBoxData, normalizeBoxTransform } from
6
+ "../../grammar/boxPlot.js";
7
+ import { hasDataset } from "../../selectors/index.js";
8
+ import { findDataset } from "../../selectors/datasets.js";
9
+ import { findLayer } from "../../selectors/layers.js";
10
+ import {
11
+ resolveBoxAppearance,
12
+ resolveBoxMedianAppearance,
13
+ resolveBoxOutlierAppearance,
14
+ resolveBoxWhisker,
15
+ resolveBoxWidth
16
+ } from "./options.js";
17
+
18
+ const OPTIONS = Object.freeze([
19
+ "target", "whisker", "width", "outliers", "box", "median", "outlier"
20
+ ]);
21
+
22
+ function resolveBoxOwner(program, requested) {
23
+ const eligible = program.semanticSpec.layers.filter(
24
+ layer => program.markConfigs[layer.id]?.boxPlot?.materialized === true
25
+ );
26
+ if (requested !== undefined) {
27
+ const id = validateUserId(requested, "Box-plot owner id");
28
+ const layer = findLayer(program, id);
29
+ if (layer === undefined || !eligible.includes(layer)) {
30
+ throw new Error(`Unknown box-plot owner "${id}".`);
31
+ }
32
+ return layer;
33
+ }
34
+ const current = findLayer(program, program.context.currentMark);
35
+ if (current !== undefined && eligible.includes(current)) return current;
36
+ if (eligible.length === 1) return eligible[0];
37
+ if (eligible.length === 0) throw new Error("No box-plot owner is available.");
38
+ throw new Error("Box-plot owner is ambiguous; provide target.");
39
+ }
40
+
41
+ function requirePatch(value, label) {
42
+ if (!isPlainObject(value)) {
43
+ throw new TypeError(`editBoxPlot ${label} must be a plain object.`);
44
+ }
45
+ return value;
46
+ }
47
+
48
+ function resolveEditedWhisker(current, value) {
49
+ if (value === undefined) return current;
50
+ const patch = requirePatch(value, "whisker");
51
+ validateKeys(patch, ["type", "factor"], "editBoxPlot whisker");
52
+ const type = patch.type ?? current.type;
53
+ const candidate = type === "minmax"
54
+ ? { type, ...(Object.hasOwn(patch, "factor") ? { factor: patch.factor } : {}) }
55
+ : {
56
+ type,
57
+ factor: Object.hasOwn(patch, "factor")
58
+ ? patch.factor
59
+ : current.type === "tukey" ? current.factor : 1.5
60
+ };
61
+ return resolveBoxWhisker(candidate, "editBoxPlot");
62
+ }
63
+
64
+ function nextRevisionId(program, ownerId, kind) {
65
+ let revision = 1;
66
+ while (hasDataset(program, `${ownerId}${kind}Revision${revision}`)) {
67
+ revision += 1;
68
+ }
69
+ return `${ownerId}${kind}Revision${revision}`;
70
+ }
71
+
72
+ function removeOwnedMark(program, id) {
73
+ return program
74
+ .editSemantic({ property: `layer[${id}]`, remove: true })
75
+ .editGraphics({ target: id, remove: true })
76
+ ._withoutMaterializationConfig(["marks", id]);
77
+ }
78
+
79
+ function rebindRuleData(program, id, data) {
80
+ return program.editSemantic({ property: `layer[${id}].data`, value: data });
81
+ }
82
+
83
+ export const editBoxPlot = action(
84
+ {
85
+ op: "editBoxPlot",
86
+ description: "Revise one box plot and its owned components."
87
+ },
88
+ function (args = {}) {
89
+ validateKeys(args, OPTIONS, "editBoxPlot");
90
+ if (!OPTIONS.slice(1).some(key => Object.hasOwn(args, key))) {
91
+ throw new Error("editBoxPlot requires at least one box-plot option.");
92
+ }
93
+ const owner = resolveBoxOwner(this, args.target);
94
+ const current = this.markConfigs[owner.id].boxPlot;
95
+ const whisker = resolveEditedWhisker(current.whisker, args.whisker);
96
+ const width = Object.hasOwn(args, "width")
97
+ ? resolveBoxWidth(args.width, "editBoxPlot")
98
+ : current.width;
99
+ if (Object.hasOwn(args, "outliers") && typeof args.outliers !== "boolean") {
100
+ throw new TypeError("editBoxPlot outliers must be a boolean.");
101
+ }
102
+ const outliers = args.outliers ?? current.outliers;
103
+ const boxPatch = Object.hasOwn(args, "box")
104
+ ? requirePatch(args.box, "box")
105
+ : {};
106
+ const medianPatch = Object.hasOwn(args, "median")
107
+ ? requirePatch(args.median, "median")
108
+ : {};
109
+ const outlierPatch = Object.hasOwn(args, "outlier")
110
+ ? requirePatch(args.outlier, "outlier")
111
+ : {};
112
+ const box = resolveBoxAppearance(
113
+ { ...current.box, ...boxPatch },
114
+ "editBoxPlot"
115
+ );
116
+ const median = resolveBoxMedianAppearance(
117
+ { ...current.median, ...medianPatch },
118
+ "editBoxPlot"
119
+ );
120
+ const outlier = resolveBoxOutlierAppearance(
121
+ { ...current.outlier, ...outlierPatch },
122
+ "editBoxPlot"
123
+ );
124
+ if (Object.hasOwn(boxPatch, "fill") && owner.encoding?.color !== undefined) {
125
+ throw new Error(
126
+ "editBoxPlot box.fill cannot be combined with a color encoding."
127
+ );
128
+ }
129
+
130
+ const revisesData = JSON.stringify(whisker) !== JSON.stringify(current.whisker) ||
131
+ outliers !== current.outliers;
132
+ const changesBox = Object.hasOwn(args, "box") ||
133
+ Object.hasOwn(args, "width") || revisesData;
134
+ const changesMedian = Object.hasOwn(args, "median") ||
135
+ Object.hasOwn(args, "width") || revisesData;
136
+ const changesOutlier = Object.hasOwn(args, "outlier") || revisesData;
137
+ const categoryEncoding = current.orientation === "vertical"
138
+ ? owner.encoding.x
139
+ : owner.encoding.y;
140
+ const measureEncoding = current.orientation === "vertical"
141
+ ? owner.encoding.y
142
+ : owner.encoding.x;
143
+ let summaryId = current.summaryId;
144
+ let outlierDataId = current.outlierDataId;
145
+ let next = this;
146
+
147
+ const sourceRows = findDataset(this, current.source).values;
148
+ const derived = deriveBoxData(sourceRows, normalizeBoxTransform({
149
+ type: "boxSummary",
150
+ category: current.category,
151
+ field: current.measure,
152
+ whisker: whisker.type,
153
+ ...(whisker.factor === undefined ? {} : { factor: whisker.factor })
154
+ }));
155
+ const hasOutliers = outliers && whisker.type === "tukey" &&
156
+ derived.outliers.length > 0;
157
+
158
+ if (revisesData) {
159
+ summaryId = nextRevisionId(this, owner.id, "SummaryData");
160
+ next = next.createBoxSummaryData({
161
+ id: summaryId,
162
+ source: current.source,
163
+ category: current.category,
164
+ field: current.measure,
165
+ whisker: whisker.type,
166
+ ...(whisker.factor === undefined ? {} : { factor: whisker.factor })
167
+ });
168
+ if (hasOutliers) {
169
+ outlierDataId = nextRevisionId(this, owner.id, "OutlierData");
170
+ next = next.createBoxOutlierData({
171
+ id: outlierDataId,
172
+ source: current.source,
173
+ category: current.category,
174
+ field: current.measure,
175
+ whisker: whisker.type,
176
+ factor: whisker.factor
177
+ });
178
+ } else {
179
+ outlierDataId = undefined;
180
+ }
181
+
182
+ next = next.editSemantic({
183
+ property: `layer[${owner.id}].data`,
184
+ value: summaryId
185
+ });
186
+ const whiskerConfig = next.markConfigs[current.whiskerId];
187
+ const capIds = [
188
+ whiskerConfig.errorBar.lowerCapId,
189
+ whiskerConfig.errorBar.upperCapId
190
+ ].filter(id => id !== undefined);
191
+ next = rebindRuleData(next, current.whiskerId, summaryId)
192
+ ._withMarkConfig(current.whiskerId, {
193
+ ...whiskerConfig,
194
+ errorBar: { ...whiskerConfig.errorBar, data: summaryId }
195
+ });
196
+ for (const capId of capIds) {
197
+ next = rebindRuleData(next, capId, summaryId);
198
+ }
199
+ next = rebindRuleData(next, current.medianId, summaryId);
200
+
201
+ const hadOutlierLayer = findLayer(next, current.outlierId) !== undefined;
202
+ if (hadOutlierLayer && !hasOutliers) {
203
+ next = removeOwnedMark(next, current.outlierId);
204
+ } else if (hadOutlierLayer && hasOutliers) {
205
+ next = next.editSemantic({
206
+ property: `layer[${current.outlierId}].data`,
207
+ value: outlierDataId
208
+ });
209
+ } else if (!hadOutlierLayer && hasOutliers) {
210
+ next = next.createBoxOutliers({
211
+ id: current.outlierId,
212
+ data: outlierDataId,
213
+ category: current.category,
214
+ categoryType: categoryEncoding.fieldType,
215
+ measure: current.measure,
216
+ orientation: current.orientation,
217
+ coordinate: owner.coordinate,
218
+ categoryScale: categoryEncoding.scale,
219
+ measureScale: measureEncoding.scale,
220
+ shape: outlier.shape,
221
+ radius: outlier.radius,
222
+ opacity: outlier.opacity
223
+ });
224
+ }
225
+ }
226
+
227
+ next = next._withMarkConfig(owner.id, {
228
+ ...next.markConfigs[owner.id],
229
+ boxPlot: {
230
+ ...current,
231
+ whisker,
232
+ width,
233
+ outliers,
234
+ box,
235
+ median,
236
+ outlier,
237
+ summaryId,
238
+ outlierDataId
239
+ },
240
+ barWidth: { band: width },
241
+ fill: box.fill,
242
+ opacity: box.opacity,
243
+ stroke: box.stroke,
244
+ strokeWidth: box.strokeWidth
245
+ });
246
+
247
+ if (changesBox) next = next.rematerializeBarMark({ id: owner.id });
248
+ if (revisesData) {
249
+ next = next.rematerializeErrorBar({ id: current.whiskerId });
250
+ }
251
+ if (changesMedian) {
252
+ next = next
253
+ .encodeStroke({ target: current.medianId, value: median.stroke })
254
+ .encodeStrokeWidth({
255
+ target: current.medianId,
256
+ value: median.strokeWidth
257
+ });
258
+ }
259
+ if (findLayer(next, current.outlierId) !== undefined && changesOutlier) {
260
+ next = next
261
+ .editPointMark({
262
+ target: current.outlierId,
263
+ shape: outlier.shape,
264
+ opacity: outlier.opacity
265
+ })
266
+ .encodeRadius({ target: current.outlierId, value: outlier.radius });
267
+ }
268
+
269
+ if (revisesData) {
270
+ next = next.releaseDerivedData({ id: current.summaryId });
271
+ if (current.outlierDataId !== undefined) {
272
+ next = next.releaseDerivedData({ id: current.outlierDataId });
273
+ }
274
+ }
275
+ return next._withContext({ currentMark: owner.id, currentData: current.source });
276
+ }
277
+ );
@@ -1,9 +1,11 @@
1
1
  import { createBoxMedian, createBoxOutliers } from "./components.js";
2
2
  import { createBoxPlot } from "./create.js";
3
+ import { editBoxPlot } from "./edit.js";
3
4
  import { materializeBoxPlot } from "./materialize.js";
4
5
 
5
6
  export function registerBoxPlotActions(ProgramClass) {
6
7
  ProgramClass.prototype.createBoxPlot = createBoxPlot;
8
+ ProgramClass.prototype.editBoxPlot = editBoxPlot;
7
9
  ProgramClass.prototype.materializeBoxPlot = materializeBoxPlot;
8
10
  ProgramClass.prototype.createBoxMedian = createBoxMedian;
9
11
  ProgramClass.prototype.createBoxOutliers = createBoxOutliers;
@@ -50,7 +50,11 @@ export const materializeBoxPlot = action(
50
50
  source,
51
51
  orientation,
52
52
  category: category.field,
53
- measure: measure.field
53
+ measure: measure.field,
54
+ summaryId,
55
+ whiskerId,
56
+ medianId,
57
+ outlierId
54
58
  },
55
59
  barWidth: { band: config.width },
56
60
  fill: config.box.fill,
@@ -178,6 +182,13 @@ export const materializeBoxPlot = action(
178
182
  opacity: config.outlier.opacity
179
183
  });
180
184
  }
185
+ next = next._withMarkConfig(ownerId, {
186
+ ...next.markConfigs[ownerId],
187
+ boxPlot: {
188
+ ...next.markConfigs[ownerId].boxPlot,
189
+ outlierDataId: hasOutliers ? outlierDataId : undefined
190
+ }
191
+ });
181
192
  return next._withContext({ currentMark: ownerId, currentData: source });
182
193
  }
183
194
  );