ggaction 0.0.13 → 0.0.15

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 (270) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +20 -11
  3. package/knowledge/action-cards.json +1 -1
  4. package/knowledge/intent-taxonomy.json +1 -1
  5. package/knowledge/mcp-resources.json +1 -1
  6. package/package.json +8 -3
  7. package/src/actions/basic.js +2 -2
  8. package/src/actions/boxPlots/components.js +14 -4
  9. package/src/actions/boxPlots/edit.js +31 -5
  10. package/src/actions/boxPlots/materialize.js +11 -3
  11. package/src/actions/boxPlots/options.js +48 -16
  12. package/src/actions/categoryOrder/index.js +15 -4
  13. package/src/actions/charts/area.js +5 -1
  14. package/src/actions/charts/bar.js +2 -1
  15. package/src/actions/charts/density.js +6 -2
  16. package/src/actions/charts/ecdf.js +4 -1
  17. package/src/actions/charts/endpoints.js +25 -3
  18. package/src/actions/charts/heatmap.js +2 -1
  19. package/src/actions/charts/histogram.js +4 -3
  20. package/src/actions/charts/horizon.js +3 -1
  21. package/src/actions/charts/interval-regression.js +5 -2
  22. package/src/actions/charts/line.js +3 -1
  23. package/src/actions/charts/parallel.js +8 -2
  24. package/src/actions/charts/pie.js +10 -2
  25. package/src/actions/charts/polar.js +15 -4
  26. package/src/actions/charts/radar.js +8 -2
  27. package/src/actions/charts/radial.js +10 -2
  28. package/src/actions/charts/rug-strip.js +6 -2
  29. package/src/actions/charts/scatter.js +3 -1
  30. package/src/actions/composition/actions.js +13 -4
  31. package/src/actions/coordinates/actions.js +1 -1
  32. package/src/actions/coordinates/edit.js +76 -0
  33. package/src/actions/data/bin.js +1 -1
  34. package/src/actions/data/bin2d.js +57 -185
  35. package/src/actions/data/complete.js +26 -0
  36. package/src/actions/data/computed.js +2 -2
  37. package/src/actions/data/create.js +2 -2
  38. package/src/actions/data/density.js +12 -20
  39. package/src/actions/data/derived.js +47 -17
  40. package/src/actions/data/edit.js +539 -0
  41. package/src/actions/data/filter.js +14 -2
  42. package/src/actions/data/impute.js +26 -0
  43. package/src/actions/data/index.js +14 -0
  44. package/src/actions/data/normalize.js +27 -0
  45. package/src/actions/data/regression.js +16 -21
  46. package/src/actions/data/revisionBindings.js +145 -0
  47. package/src/actions/data/shared.js +20 -4
  48. package/src/actions/data/summary.js +1 -1
  49. package/src/actions/data/timeUnit.js +4 -1
  50. package/src/actions/data/window.js +1 -1
  51. package/src/actions/encodings/channels.js +415 -0
  52. package/src/actions/encodings/color/index.js +2 -1
  53. package/src/actions/encodings/density.js +5 -2
  54. package/src/actions/encodings/histogram.js +3 -2
  55. package/src/actions/encodings/index.js +4 -0
  56. package/src/actions/encodings/offset.js +20 -13
  57. package/src/actions/encodings/position/apply.js +53 -7
  58. package/src/actions/encodings/position/index.js +10 -2
  59. package/src/actions/encodings/position/policies/bar.js +9 -0
  60. package/src/actions/encodings/position/resolve.js +24 -7
  61. package/src/actions/encodings/remove.js +9 -4
  62. package/src/actions/encodings/ruleAppearance.js +1 -23
  63. package/src/actions/encodings/seriesLayout.js +1 -0
  64. package/src/actions/encodings/shared.js +82 -14
  65. package/src/actions/encodings/stroke.js +226 -0
  66. package/src/actions/errorBands/create.js +23 -3
  67. package/src/actions/errorBands/edit.js +21 -6
  68. package/src/actions/errorBars/create.js +35 -7
  69. package/src/actions/errorBars/edit.js +19 -4
  70. package/src/actions/facets/actions.js +237 -57
  71. package/src/actions/facets/derive.js +91 -26
  72. package/src/actions/gradientPlots/components.js +10 -2
  73. package/src/actions/gradientPlots/edit.js +14 -4
  74. package/src/actions/gradientPlots/materialize.js +3 -1
  75. package/src/actions/gradientPlots/options.js +10 -2
  76. package/src/actions/guides/applicability.js +8 -2
  77. package/src/actions/guides/axes/labels.js +58 -12
  78. package/src/actions/guides/axes/lines.js +6 -2
  79. package/src/actions/guides/axes/parallel/resolve.js +6 -2
  80. package/src/actions/guides/axes/tickGroups.js +2 -1
  81. package/src/actions/guides/axes/ticks.js +6 -2
  82. package/src/actions/guides/axes/titles.js +11 -3
  83. package/src/actions/guides/grids/resolve.js +7 -2
  84. package/src/actions/guides/guides.js +19 -3
  85. package/src/actions/guides/legends/blocks.js +426 -0
  86. package/src/actions/guides/legends/categorical/actions.js +134 -48
  87. package/src/actions/guides/legends/categorical/components.js +17 -12
  88. package/src/actions/guides/legends/categorical/index.js +37 -33
  89. package/src/actions/guides/legends/categorical/layout.js +97 -15
  90. package/src/actions/guides/legends/categorical/options.js +2 -2
  91. package/src/actions/guides/legends/categorical/recipes.js +111 -20
  92. package/src/actions/guides/legends/categorical/resolve.js +12 -8
  93. package/src/actions/guides/legends/categorical/symbols.js +85 -30
  94. package/src/actions/guides/legends/continuous/common.js +30 -12
  95. package/src/actions/guides/legends/continuous/gradient.js +10 -7
  96. package/src/actions/guides/legends/continuous/index.js +2 -0
  97. package/src/actions/guides/legends/continuous/interval.js +8 -3
  98. package/src/actions/guides/legends/continuous/opacity.js +8 -5
  99. package/src/actions/guides/legends/continuous/stroke.js +525 -0
  100. package/src/actions/guides/legends/creation.js +14 -1
  101. package/src/actions/guides/legends/edit.js +158 -59
  102. package/src/actions/guides/legends/index.js +2 -0
  103. package/src/actions/guides/legends/lane.js +54 -19
  104. package/src/actions/guides/legends/lifecycle.js +19 -8
  105. package/src/actions/guides/legends/remove.js +13 -3
  106. package/src/actions/guides/legends/sampling.js +140 -0
  107. package/src/actions/guides/legends/size.js +93 -40
  108. package/src/actions/guides/legends/strokeWidth.js +34 -28
  109. package/src/actions/guides/legends/target.js +112 -1
  110. package/src/actions/guides/legends/transition.js +165 -16
  111. package/src/actions/guides/polar/axes/facade.js +4 -4
  112. package/src/actions/guides/polar/axes/labels.js +24 -5
  113. package/src/actions/guides/polar/axes/lines.js +10 -4
  114. package/src/actions/guides/polar/axes/shared.js +16 -0
  115. package/src/actions/guides/polar/axes/ticks.js +1 -1
  116. package/src/actions/guides/polar/axes/titles.js +1 -1
  117. package/src/actions/guides/polar/grids.js +1 -1
  118. package/src/actions/guides/polar/resolve.js +12 -8
  119. package/src/actions/index.js +14 -3
  120. package/src/actions/marks/arc/actions.js +77 -19
  121. package/src/actions/marks/area/actions.js +34 -9
  122. package/src/actions/marks/area/materialize.js +17 -1
  123. package/src/actions/marks/bar/create.js +11 -2
  124. package/src/actions/marks/bar/edit.js +20 -3
  125. package/src/actions/marks/bar/materialize.js +22 -2
  126. package/src/actions/marks/index.js +10 -1
  127. package/src/actions/marks/line/actions.js +37 -14
  128. package/src/actions/marks/line/materialize.js +29 -10
  129. package/src/actions/marks/point/create.js +8 -2
  130. package/src/actions/marks/point/edit.js +18 -6
  131. package/src/actions/marks/point/materialize.js +49 -15
  132. package/src/actions/marks/rect/actions.js +33 -12
  133. package/src/actions/marks/references.js +601 -30
  134. package/src/actions/marks/remove.js +71 -1
  135. package/src/actions/marks/rule/actions.js +47 -10
  136. package/src/actions/marks/shared.js +1 -1
  137. package/src/actions/marks/text/actions.js +74 -9
  138. package/src/actions/marks/text/index.js +4 -0
  139. package/src/actions/marks/text/layout.js +154 -6
  140. package/src/actions/marks/text/removal.js +173 -0
  141. package/src/actions/marks/text/selection.js +73 -0
  142. package/src/actions/marks/tick/actions.js +22 -5
  143. package/src/actions/primitives/editGraphics.js +26 -0
  144. package/src/actions/primitives/semantic.js +2 -1
  145. package/src/actions/primitives/semanticAction.js +83 -10
  146. package/src/actions/primitives/semanticValidation/guide.js +1 -1
  147. package/src/actions/primitives/semanticValidation/index.js +9 -1
  148. package/src/actions/primitives/semanticValidation/layer.js +5 -0
  149. package/src/actions/primitives/semanticValidation/scale.js +28 -7
  150. package/src/actions/regression/components.js +26 -11
  151. package/src/actions/regression/create.js +10 -2
  152. package/src/actions/regression/edit.js +21 -7
  153. package/src/actions/resources/index.js +7 -0
  154. package/src/actions/resources/remove.js +172 -0
  155. package/src/actions/scales/channels.js +212 -0
  156. package/src/actions/scales/consumers/common.js +1 -1
  157. package/src/actions/scales/consumers/families.js +3 -2
  158. package/src/actions/scales/consumers/index.js +19 -2
  159. package/src/actions/scales/consumers/seriesLayout.js +28 -14
  160. package/src/actions/scales/create.js +54 -22
  161. package/src/actions/scales/definitions.js +94 -40
  162. package/src/actions/scales/edit.js +66 -11
  163. package/src/actions/scales/editPolicy.js +81 -11
  164. package/src/actions/scales/index.js +6 -0
  165. package/src/actions/scales/materialize.js +4 -3
  166. package/src/actions/scales/offset.js +48 -0
  167. package/src/actions/scales/parallel.js +71 -0
  168. package/src/actions/scales/preview.js +111 -10
  169. package/src/actions/selection/actions.js +154 -30
  170. package/src/actions/selection/index.js +3 -0
  171. package/src/actions/theme/actions.js +68 -13
  172. package/src/actions/theme/composition.js +238 -0
  173. package/src/actions/theme/index.js +9 -2
  174. package/src/actions/theme/reconcile.js +315 -67
  175. package/src/actions/theme/state.js +160 -0
  176. package/src/actions/violinPlots/create.js +5 -2
  177. package/src/actions/violinPlots/edit.js +1 -0
  178. package/src/core/action.js +10 -0
  179. package/src/core/compositionState.js +6 -1
  180. package/src/core/programState.js +20 -0
  181. package/src/core/resourceReferences.js +332 -0
  182. package/src/core/vocabulary.js +16 -3
  183. package/src/grammar/bars/aggregate.js +4 -1
  184. package/src/grammar/bars/geometry.js +44 -0
  185. package/src/grammar/bin.js +54 -9
  186. package/src/grammar/complete.js +279 -0
  187. package/src/grammar/computed.js +265 -64
  188. package/src/grammar/coordinates.js +37 -0
  189. package/src/grammar/density.js +239 -37
  190. package/src/grammar/displayLabels.js +57 -0
  191. package/src/grammar/facets/dependencies.js +133 -1
  192. package/src/grammar/facets/guides.js +3 -0
  193. package/src/grammar/facets/index.js +7 -47
  194. package/src/grammar/facets/scales.js +15 -18
  195. package/src/grammar/impute.js +340 -0
  196. package/src/grammar/markLabelSelection.js +57 -0
  197. package/src/grammar/markLabels.js +18 -1
  198. package/src/grammar/normalize.js +366 -0
  199. package/src/grammar/pathSeries.js +26 -8
  200. package/src/grammar/pointShapes.js +13 -4
  201. package/src/grammar/polar.js +90 -8
  202. package/src/grammar/regression/index.js +1 -0
  203. package/src/grammar/regression/parameters.js +23 -0
  204. package/src/grammar/roundedRect.js +180 -0
  205. package/src/grammar/rules.js +1 -0
  206. package/src/grammar/scales/colorConsumers.js +20 -1
  207. package/src/grammar/scales/continuous.js +2 -2
  208. package/src/grammar/scales/definition.js +27 -4
  209. package/src/grammar/scales/index.js +1 -0
  210. package/src/grammar/scales/ordinal.js +12 -3
  211. package/src/grammar/scales/policies.js +2 -2
  212. package/src/grammar/scales/size.js +456 -0
  213. package/src/grammar/schemas/concreteGraphic.js +14 -2
  214. package/src/grammar/schemas/graphic.js +15 -0
  215. package/src/grammar/schemas/graphicBounds.js +179 -30
  216. package/src/grammar/schemas/semanticPath.js +7 -2
  217. package/src/grammar/statisticalReference.js +103 -0
  218. package/src/grammar/strokeStyle.js +77 -0
  219. package/src/grammar/summary.js +78 -16
  220. package/src/grammar/timeUnit.js +216 -5
  221. package/src/grammar/transformTopology.js +4 -0
  222. package/src/grammar/transforms.js +394 -28
  223. package/src/grammar/weightedStatistics.js +374 -0
  224. package/src/grammar/window.js +221 -16
  225. package/src/layout/aspect.js +80 -0
  226. package/src/layout/facets.js +59 -5
  227. package/src/layout/labels.js +343 -0
  228. package/src/materialization/bars/aggregate.js +25 -2
  229. package/src/materialization/bars/histogram.js +84 -24
  230. package/src/materialization/bars/ranged.js +19 -3
  231. package/src/materialization/bars/resolve.js +7 -4
  232. package/src/materialization/composition.js +3 -1
  233. package/src/materialization/coordinateBounds.js +204 -0
  234. package/src/materialization/dependencies.js +36 -0
  235. package/src/materialization/facetGuides/legacyCategorical.js +3 -0
  236. package/src/materialization/facetGuides/placement.js +8 -2
  237. package/src/materialization/facetGuides/preparation.js +12 -7
  238. package/src/materialization/facetHeaders.js +346 -0
  239. package/src/materialization/facets.js +15 -65
  240. package/src/materialization/guides/resources.js +26 -0
  241. package/src/materialization/legends.js +1 -1
  242. package/src/materialization/marks/index.js +11 -5
  243. package/src/materialization/marks/policies.js +2 -1
  244. package/src/materialization/rect.js +25 -12
  245. package/src/materialization/rectConfig.js +5 -1
  246. package/src/materialization/scales/map.js +5 -3
  247. package/src/materialization/scales/policies/binnedPosition.js +2 -2
  248. package/src/materialization/scales/policies/offset.js +50 -8
  249. package/src/materialization/scales/policies/series.js +4 -1
  250. package/src/materialization/scales/resolve.js +22 -6
  251. package/src/materialization/selection/items/arc.js +28 -4
  252. package/src/materialization/selection/items/bar.js +30 -3
  253. package/src/materialization/selection/items/common.js +19 -4
  254. package/src/materialization/selection/items/rect.js +28 -4
  255. package/src/materialization/selection/path.js +1 -1
  256. package/src/materialization/selection/point.js +3 -1
  257. package/src/materialization/text.js +265 -14
  258. package/src/renderers/canvas/circle.js +2 -3
  259. package/src/renderers/canvas/line.js +2 -3
  260. package/src/renderers/canvas/native.js +4 -4
  261. package/src/renderers/canvas/path.js +2 -3
  262. package/src/renderers/canvas/rect.js +2 -3
  263. package/src/renderers/canvas/stroke.js +11 -0
  264. package/src/renderers/svg.js +19 -0
  265. package/src/selectors/datasets.js +32 -1
  266. package/src/selectors/layers.js +4 -0
  267. package/src/theme/defaults.js +101 -0
  268. package/types/basic.d.ts +6 -0
  269. package/types/index.d.ts +85 -0
  270. package/types/program.d.ts +910 -117
@@ -33,10 +33,47 @@ export const TIME_UNITS = cloneAndFreeze([
33
33
  "day",
34
34
  "hour",
35
35
  "minute",
36
- "second"
36
+ "second",
37
+ "week",
38
+ "weekday"
37
39
  ]);
38
40
 
39
- const TRANSFORM_KEYS = Object.freeze(["type", "field", "unit", "as", "temporalUnit"]);
41
+ const TRANSFORM_KEYS = Object.freeze([
42
+ "type", "field", "unit", "as", "temporalUnit", "timeZone",
43
+ "weekStartsOn", "weekRule"
44
+ ]);
45
+
46
+ const CALENDAR_FORMATTERS = new Map();
47
+
48
+ function formatter(timeZone) {
49
+ if (!CALENDAR_FORMATTERS.has(timeZone)) {
50
+ let value;
51
+ try {
52
+ value = new Intl.DateTimeFormat("en-US-u-ca-gregory-nu-latn", {
53
+ timeZone,
54
+ year: "numeric",
55
+ month: "2-digit",
56
+ day: "2-digit",
57
+ hour: "2-digit",
58
+ minute: "2-digit",
59
+ second: "2-digit",
60
+ hourCycle: "h23"
61
+ });
62
+ } catch {
63
+ throw new RangeError(`Unsupported time zone "${timeZone}".`);
64
+ }
65
+ CALENDAR_FORMATTERS.set(timeZone, value);
66
+ }
67
+ return CALENDAR_FORMATTERS.get(timeZone);
68
+ }
69
+
70
+ function validateTimeZone(timeZone) {
71
+ if (typeof timeZone !== "string" || timeZone.length === 0) {
72
+ throw new TypeError("Time-unit timeZone must be a non-empty IANA time-zone name.");
73
+ }
74
+ formatter(timeZone);
75
+ return timeZone;
76
+ }
40
77
 
41
78
  function requireField(value, label) {
42
79
  if (typeof value !== "string" || value.length === 0) {
@@ -69,12 +106,38 @@ export function validateTimeUnitTransform(transform) {
69
106
  if (!TIME_UNITS.includes(transform.unit)) {
70
107
  throw new Error(`Unsupported time unit "${transform.unit}".`);
71
108
  }
109
+ if (Object.hasOwn(transform, "timeZone")) validateTimeZone(transform.timeZone);
110
+ const hasWeekStartsOn = Object.hasOwn(transform, "weekStartsOn");
111
+ const hasWeekRule = Object.hasOwn(transform, "weekRule");
112
+ if (transform.unit === "week") {
113
+ if (!Number.isInteger(transform.weekStartsOn) ||
114
+ transform.weekStartsOn < 0 || transform.weekStartsOn > 6) {
115
+ throw new RangeError("Time-unit weekStartsOn must be an integer from 0 through 6.");
116
+ }
117
+ if (!["calendar", "iso"].includes(transform.weekRule)) {
118
+ throw new Error(`Unsupported time-unit weekRule "${transform.weekRule}".`);
119
+ }
120
+ if (transform.weekRule === "iso" && transform.weekStartsOn !== 1) {
121
+ throw new Error("Time-unit ISO weeks require weekStartsOn 1 (Monday).");
122
+ }
123
+ } else if (hasWeekStartsOn || hasWeekRule) {
124
+ throw new Error("Time-unit weekStartsOn and weekRule require unit week.");
125
+ }
72
126
  return transform;
73
127
  }
74
128
 
75
- export function normalizeTimeUnitTransform({ field, unit, as, temporalUnit } = {}) {
129
+ export function normalizeTimeUnitTransform(args = {}) {
130
+ const { field, unit, as, temporalUnit, timeZone } = args;
76
131
  const transform = { type: "timeUnit", field, unit, as,
77
- ...(temporalUnit === undefined ? {} : { temporalUnit }) };
132
+ ...(temporalUnit === undefined ? {} : { temporalUnit }),
133
+ ...(timeZone === undefined ? {} : { timeZone }) };
134
+ if (unit === "week") {
135
+ transform.weekStartsOn = args.weekStartsOn ?? 1;
136
+ transform.weekRule = args.weekRule ?? "calendar";
137
+ } else {
138
+ if (Object.hasOwn(args, "weekStartsOn")) transform.weekStartsOn = args.weekStartsOn;
139
+ if (Object.hasOwn(args, "weekRule")) transform.weekRule = args.weekRule;
140
+ }
78
141
  validateTimeUnitTransform(transform);
79
142
  return cloneAndFreeze(transform);
80
143
  }
@@ -96,6 +159,10 @@ export function floorUtcTimeUnit(timestamp, unit) {
96
159
  if (unit === "quarter") return utcBucket(year, Math.floor(month / 3) * 3);
97
160
  if (unit === "month") return utcBucket(year, month);
98
161
  const day = date.getUTCDate();
162
+ if (unit === "weekday") return date.getUTCDay();
163
+ if (unit === "week") {
164
+ throw new Error("floorUtcTimeUnit requires a calendar week policy.");
165
+ }
99
166
  if (unit === "day") return utcBucket(year, month, day);
100
167
  const hour = date.getUTCHours();
101
168
  if (unit === "hour") return utcBucket(year, month, day, hour);
@@ -104,6 +171,150 @@ export function floorUtcTimeUnit(timestamp, unit) {
104
171
  return Math.floor(timestamp / 1000) * 1000;
105
172
  }
106
173
 
174
+ function weekdayFromDate(year, month, day) {
175
+ return new Date(utcTimestamp(year, month - 1, day)).getUTCDay();
176
+ }
177
+
178
+ export function calendarParts(timestamp, timeZone) {
179
+ if (!validTimestamp(timestamp)) {
180
+ throw new TypeError("Time-unit timestamp must represent a valid date.");
181
+ }
182
+ validateTimeZone(timeZone);
183
+ const parts = Object.fromEntries(formatter(timeZone).formatToParts(timestamp)
184
+ .filter(part => part.type !== "literal")
185
+ .map(part => [part.type, Number(part.value)]));
186
+ const millisecond = ((timestamp % 1000) + 1000) % 1000;
187
+ return {
188
+ year: parts.year,
189
+ month: parts.month,
190
+ day: parts.day,
191
+ hour: parts.hour,
192
+ minute: parts.minute,
193
+ second: parts.second,
194
+ millisecond,
195
+ weekday: weekdayFromDate(parts.year, parts.month, parts.day)
196
+ };
197
+ }
198
+
199
+ function civilTimestamp(parts) {
200
+ const timestamp = utcTimestamp(
201
+ parts.year,
202
+ parts.month - 1,
203
+ parts.day,
204
+ parts.hour ?? 0,
205
+ parts.minute ?? 0
206
+ ) + (parts.second ?? 0) * 1000 + (parts.millisecond ?? 0);
207
+ if (!validTimestamp(timestamp)) {
208
+ throw new RangeError("Time-unit calendar boundary is outside the supported Date range.");
209
+ }
210
+ return timestamp;
211
+ }
212
+
213
+ function sameCivil(left, right) {
214
+ return ["year", "month", "day", "hour", "minute", "second", "millisecond"]
215
+ .every(key => left[key] === right[key]);
216
+ }
217
+
218
+ function sameBucket(candidate, target, unit) {
219
+ const keys = unit === "year" ? ["year"]
220
+ : unit === "quarter" ? ["year", "month"]
221
+ : unit === "month" ? ["year", "month"]
222
+ : unit === "day" || unit === "week" ? ["year", "month", "day"]
223
+ : unit === "hour" ? ["year", "month", "day", "hour"]
224
+ : unit === "minute" ? ["year", "month", "day", "hour", "minute"]
225
+ : ["year", "month", "day", "hour", "minute", "second"];
226
+ if (unit === "quarter") {
227
+ return candidate.year === target.year &&
228
+ Math.floor((candidate.month - 1) / 3) === Math.floor((target.month - 1) / 3);
229
+ }
230
+ return keys.every(key => candidate[key] === target[key]);
231
+ }
232
+
233
+ export function resolveCalendarBoundary(localParts, timeZone, unit) {
234
+ const naive = civilTimestamp(localParts);
235
+ const offsets = new Set();
236
+ for (let delta = -72; delta <= 72; delta += 6) {
237
+ const sample = naive + delta * 3_600_000;
238
+ if (!validTimestamp(sample)) continue;
239
+ offsets.add(civilTimestamp(calendarParts(sample, timeZone)) - sample);
240
+ }
241
+ const exact = [];
242
+ const gaps = [];
243
+ for (const offset of offsets) {
244
+ const candidate = naive - offset;
245
+ if (!validTimestamp(candidate)) continue;
246
+ const roundTrip = calendarParts(candidate, timeZone);
247
+ if (sameCivil(roundTrip, localParts)) exact.push(candidate);
248
+ else if (sameBucket(roundTrip, localParts, unit) &&
249
+ civilTimestamp(roundTrip) >= naive) {
250
+ gaps.push({ candidate, local: civilTimestamp(roundTrip) });
251
+ }
252
+ }
253
+ if (exact.length > 0) return Math.min(...exact);
254
+ if (gaps.length > 0) {
255
+ gaps.sort((left, right) => left.local - right.local || left.candidate - right.candidate);
256
+ return gaps[0].candidate;
257
+ }
258
+ throw new RangeError(`Time-unit ${unit} boundary does not exist in time zone "${timeZone}".`);
259
+ }
260
+
261
+ function addCalendarDays(parts, delta) {
262
+ const date = new Date(utcTimestamp(parts.year, parts.month - 1, parts.day));
263
+ date.setUTCDate(date.getUTCDate() + delta);
264
+ return {
265
+ ...parts,
266
+ year: date.getUTCFullYear(),
267
+ month: date.getUTCMonth() + 1,
268
+ day: date.getUTCDate(),
269
+ weekday: date.getUTCDay()
270
+ };
271
+ }
272
+
273
+ function boundaryParts(parts, transform) {
274
+ const result = {
275
+ year: parts.year,
276
+ month: parts.month,
277
+ day: parts.day,
278
+ hour: parts.hour,
279
+ minute: parts.minute,
280
+ second: parts.second,
281
+ millisecond: 0
282
+ };
283
+ if (transform.unit === "year") Object.assign(result, { month: 1, day: 1, hour: 0, minute: 0, second: 0 });
284
+ else if (transform.unit === "quarter") Object.assign(result, {
285
+ month: Math.floor((parts.month - 1) / 3) * 3 + 1,
286
+ day: 1, hour: 0, minute: 0, second: 0
287
+ });
288
+ else if (transform.unit === "month") Object.assign(result, { day: 1, hour: 0, minute: 0, second: 0 });
289
+ else if (["day", "week"].includes(transform.unit)) Object.assign(result, { hour: 0, minute: 0, second: 0 });
290
+ else if (transform.unit === "hour") Object.assign(result, { minute: 0, second: 0 });
291
+ else if (transform.unit === "minute") result.second = 0;
292
+ if (transform.unit === "week") {
293
+ const distance = (parts.weekday - transform.weekStartsOn + 7) % 7;
294
+ return addCalendarDays(result, -distance);
295
+ }
296
+ return result;
297
+ }
298
+
299
+ export function bucketTime(timestamp, transform) {
300
+ const timeZone = transform.timeZone ?? "UTC";
301
+ if (transform.unit === "weekday") {
302
+ return timeZone === "UTC"
303
+ ? new Date(timestamp).getUTCDay()
304
+ : calendarParts(timestamp, timeZone).weekday;
305
+ }
306
+ if (timeZone === "UTC") {
307
+ if (transform.unit === "week") {
308
+ const date = new Date(timestamp);
309
+ const distance = (date.getUTCDay() - transform.weekStartsOn + 7) % 7;
310
+ return utcBucket(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() - distance);
311
+ }
312
+ return floorUtcTimeUnit(timestamp, transform.unit);
313
+ }
314
+ const parts = calendarParts(timestamp, timeZone);
315
+ return resolveCalendarBoundary(boundaryParts(parts, transform), timeZone, transform.unit);
316
+ }
317
+
107
318
  export function deriveTimeUnitRows(rows, transform) {
108
319
  validateTimeUnitTransform(transform);
109
320
  if (!Array.isArray(rows) || !rows.every(isPlainObject)) {
@@ -121,7 +332,7 @@ export function deriveTimeUnitRows(rows, transform) {
121
332
  const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index, transform.temporalUnit);
122
333
  return {
123
334
  ...row,
124
- [transform.as]: floorUtcTimeUnit(timestamp, transform.unit)
335
+ [transform.as]: bucketTime(timestamp, transform)
125
336
  };
126
337
  });
127
338
  return cloneAndFreeze(values);
@@ -2,6 +2,7 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
2
2
  bin: Object.freeze({ facetTopology: "statistical" }),
3
3
  bin2d: Object.freeze({ facetTopology: "statistical" }),
4
4
  computed: Object.freeze({ facetTopology: "rowPreserving" }),
5
+ complete: Object.freeze({ facetTopology: "statistical" }),
5
6
  boxOutlier: Object.freeze({ facetTopology: "statistical" }),
6
7
  boxSummary: Object.freeze({ facetTopology: "statistical" }),
7
8
  density: Object.freeze({ facetTopology: "statistical" }),
@@ -11,10 +12,13 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
11
12
  gradientProfile: Object.freeze({ facetTopology: "statistical" }),
12
13
  horizon: Object.freeze({ facetTopology: "statistical" }),
13
14
  interval: Object.freeze({ facetTopology: "statistical" }),
15
+ impute: Object.freeze({ facetTopology: "statistical" }),
14
16
  markFilter: Object.freeze({ provenanceTransparent: true }),
17
+ normalize: Object.freeze({ facetTopology: "statistical" }),
15
18
  regression: Object.freeze({ facetTopology: "statistical" }),
16
19
  summary: Object.freeze({ facetTopology: "statistical" }),
17
20
  stack: Object.freeze({ facetTopology: "statistical" }),
21
+ statisticalReference: Object.freeze({ facetTopology: "statistical" }),
18
22
  timeUnit: Object.freeze({ facetTopology: "rowPreserving" }),
19
23
  window: Object.freeze({ facetTopology: "statistical" })
20
24
  });