ggaction 0.0.19 → 0.0.21

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.
@@ -4,6 +4,9 @@ import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
4
4
  import { aggregateScalarValues } from "./aggregate.js";
5
5
  import { requireFiniteResult, stableDecimal } from "./numeric.js";
6
6
  import {
7
+ createBootstrapMeanSampler,
8
+ bootstrapMeanBounds,
9
+ validateBootstrapDrawCount,
7
10
  confidenceCriticalValue,
8
11
  normalizeConfidenceInterval
9
12
  } from "./statistics/confidenceInterval.js";
@@ -91,7 +94,8 @@ export function validateIntervalTransform(transform) {
91
94
  if (transform.extent === "ci") {
92
95
  normalizeConfidenceInterval(transform, {
93
96
  defaultMethod: "student-t",
94
- label: "Interval CI"
97
+ label: "Interval CI",
98
+ allowBootstrap: true
95
99
  });
96
100
  } else if (transform.method !== undefined) {
97
101
  throw new Error("Interval method is supported only for ci extent.");
@@ -117,11 +121,12 @@ export function normalizeIntervalParameters({
117
121
  extent = "ci",
118
122
  method,
119
123
  level
120
- } = {}) {
124
+ } = {}, { allowBootstrap = false } = {}) {
121
125
  const confidence = extent === "ci"
122
126
  ? normalizeConfidenceInterval({ method, level }, {
123
127
  defaultMethod: "student-t",
124
- label: "Interval CI"
128
+ label: "Interval CI",
129
+ allowBootstrap
125
130
  })
126
131
  : undefined;
127
132
  const candidate = {
@@ -164,7 +169,11 @@ function isMissing(value) {
164
169
  }
165
170
 
166
171
 
167
- function deriveGroup(values, transform) {
172
+ function deriveGroup(values, transform, bootstrapSampler) {
173
+ if (bootstrapSampler !== undefined) {
174
+ const bounds = bootstrapMeanBounds(bootstrapSampler(values), transform.level);
175
+ return bounds === undefined ? undefined : { center: aggregateScalarValues(values, "mean"), ...bounds };
176
+ }
168
177
  if (transform.center === "median") {
169
178
  if (values.length === 0) return undefined;
170
179
  return {
@@ -235,15 +244,23 @@ export function deriveInterval(rows, transform) {
235
244
  groups.get(key).values.push(value);
236
245
  }
237
246
 
247
+ const bootstrapMethod = transform.method?.type === "bootstrap"
248
+ ? normalizeConfidenceInterval(transform, { defaultMethod: "student-t", allowBootstrap: true }).method : undefined;
249
+ if (bootstrapMethod !== undefined) {
250
+ validateBootstrapDrawCount(bootstrapMethod.samples * [...groups.values()].reduce((count, group) => count + group.values.length, 0));
251
+ }
252
+ const bootstrapSampler = bootstrapMethod === undefined ? undefined : createBootstrapMeanSampler(bootstrapMethod);
253
+
254
+ const outputValue = bootstrapSampler === undefined ? stableDecimal : requireFiniteResult;
238
255
  const output = [];
239
256
  for (const group of groups.values()) {
240
- const result = deriveGroup(group.values, transform);
257
+ const result = deriveGroup(group.values, transform, bootstrapSampler);
241
258
  if (result === undefined) continue;
242
259
  output.push({
243
260
  ...group.fields,
244
- [transform.as.center]: stableDecimal(result.center),
245
- [transform.as.lower]: stableDecimal(result.lower),
246
- [transform.as.upper]: stableDecimal(result.upper)
261
+ [transform.as.center]: outputValue(result.center),
262
+ [transform.as.lower]: outputValue(result.lower),
263
+ [transform.as.upper]: outputValue(result.upper)
247
264
  });
248
265
  }
249
266
  if (transform.missing === undefined) return cloneAndFreeze(output);
@@ -277,7 +294,7 @@ export function normalizeIntervalTransform({
277
294
  }) {
278
295
  nonEmptyString(field, "Interval field");
279
296
  const grouping = normalizeGrouping(groupBy);
280
- const parameters = normalizeIntervalParameters({ center, extent, method, level });
297
+ const parameters = normalizeIntervalParameters({ center, extent, method, level }, { allowBootstrap: true });
281
298
  const transform = {
282
299
  type: "interval",
283
300
  field,
@@ -42,7 +42,7 @@ export function readPathSeriesFields(rows, layer) {
42
42
 
43
43
  function seriesValueIndex(rows, fields, field, channel, encoding = {}) {
44
44
  const quantitative = channel === "strokeWidth" || channel === "opacity";
45
- const values = channel === "stroke"
45
+ const values = channel === "stroke" || (channel === "color" && encoding.fieldType === "quantitative")
46
46
  ? readScaleField(rows, field, encoding.fieldType ?? "nominal", {
47
47
  temporalUnit: encoding.temporalUnit
48
48
  })
@@ -88,7 +88,7 @@ export function validatePathSeriesAppearance(rows, layer) {
88
88
  if (encoding?.field === undefined) continue;
89
89
  const allowed = ["color", "stroke"].includes(channel)
90
90
  ? channel === "color"
91
- ? ["nominal", "ordinal"]
91
+ ? layer.mark.type === "area" ? ["nominal", "ordinal", "quantitative"] : ["nominal", "ordinal"]
92
92
  : ["nominal", "ordinal", "quantitative", "temporal"]
93
93
  : channel === "strokeDash" ? ["nominal"] : ["quantitative"];
94
94
  if (!allowed.includes(encoding.fieldType)) {
@@ -26,8 +26,8 @@ export function validateContinuousColorConsumer(layer, encoding, scale, {
26
26
  }
27
27
  return undefined;
28
28
  }
29
- if (!["point", "bar", "rect", "text"].includes(kind)) {
30
- throw new Error("Continuous color requires a Point, aggregate Bar, Rect, or Text consumer.");
29
+ if (!["point", "bar", "rect", "text", "area"].includes(kind)) {
30
+ throw new Error("Continuous color requires a Point, aggregate Bar, Rect, Text, or quantitative Area consumer.");
31
31
  }
32
32
  if (!["quantitative", "temporal"].includes(encoding.fieldType) ||
33
33
  (isDiscretizedColorScaleType(scale.type) && encoding.fieldType !== "quantitative")) {
@@ -36,6 +36,9 @@ export function validateContinuousColorConsumer(layer, encoding, scale, {
36
36
  if (Object.hasOwn(scale, "unknown") && kind !== "point") {
37
37
  throw new Error("Continuous color scale unknown currently requires a row-owned point mark.");
38
38
  }
39
+ if (kind === "area" && encoding.fieldType !== "quantitative") {
40
+ throw new Error("Continuous area color requires a quantitative field.");
41
+ }
39
42
  if (kind !== "bar") {
40
43
  if (encoding.aggregate !== undefined) throw new Error(`${kind} continuous color does not support aggregate.`);
41
44
  return undefined;
@@ -1,101 +1 @@
1
- import { cloneAndFreeze } from "../../core/immutable.js";
2
- import { studentTCriticalValue } from "./studentT.js";
3
-
4
- export const CONFIDENCE_INTERVAL_METHODS = Object.freeze([
5
- "normal",
6
- "student-t"
7
- ]);
8
-
9
- function validateMethod(method, label) {
10
- if (!CONFIDENCE_INTERVAL_METHODS.includes(method)) {
11
- throw new Error(`Unsupported ${label} method "${method}".`);
12
- }
13
- }
14
-
15
- function validateLevel(level, label) {
16
- if (!Number.isFinite(level) || level <= 0 || level >= 1) {
17
- throw new RangeError(`${label} level must be between 0 and 1.`);
18
- }
19
- }
20
-
21
- // Peter J. Acklam's inverse-normal approximation. The exact 95% compatibility
22
- // value is handled by normalCriticalValue before this approximation is used.
23
- function inverseNormal(probability) {
24
- const a = [
25
- -3.969683028665376e1, 2.209460984245205e2,
26
- -2.759285104469687e2, 1.38357751867269e2,
27
- -3.066479806614716e1, 2.506628277459239
28
- ];
29
- const b = [
30
- -5.447609879822406e1, 1.615858368580409e2,
31
- -1.556989798598866e2, 6.680131188771972e1,
32
- -1.328068155288572e1
33
- ];
34
- const c = [
35
- -7.784894002430293e-3, -3.223964580411365e-1,
36
- -2.400758277161838, -2.549732539343734,
37
- 4.374664141464968, 2.938163982698783
38
- ];
39
- const d = [
40
- 7.784695709041462e-3, 3.224671290700398e-1,
41
- 2.445134137142996, 3.754408661907416
42
- ];
43
- const low = 0.02425;
44
- const high = 1 - low;
45
- if (probability < low) {
46
- const q = Math.sqrt(-2 * Math.log(probability));
47
- return (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
48
- ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
49
- }
50
- if (probability > high) {
51
- const q = Math.sqrt(-2 * Math.log(1 - probability));
52
- return -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5]) /
53
- ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1);
54
- }
55
- const q = probability - 0.5;
56
- const r = q * q;
57
- return (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q /
58
- (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1);
59
- }
60
-
61
- export function normalizeConfidenceInterval({
62
- method,
63
- level
64
- } = {}, {
65
- defaultMethod,
66
- defaultLevel = 0.95,
67
- label = "Confidence interval"
68
- } = {}) {
69
- validateMethod(defaultMethod, label);
70
- const resolvedMethod = method ?? defaultMethod;
71
- const resolvedLevel = level ?? defaultLevel;
72
- validateMethod(resolvedMethod, label);
73
- validateLevel(resolvedLevel, label);
74
- return cloneAndFreeze({ method: resolvedMethod, level: resolvedLevel });
75
- }
76
-
77
- export function normalCriticalValue(level) {
78
- validateLevel(level, "Normal confidence interval");
79
- if (level === 0.95) return 1.96;
80
- return inverseNormal((1 + level) / 2);
81
- }
82
-
83
- export function confidenceCriticalValue({
84
- method,
85
- level,
86
- degreesOfFreedom
87
- }) {
88
- const normalized = normalizeConfidenceInterval(
89
- { method, level },
90
- { defaultMethod: method, label: "Confidence interval" }
91
- );
92
- if (normalized.method === "normal") {
93
- return normalCriticalValue(normalized.level);
94
- }
95
- if (!Number.isInteger(degreesOfFreedom) || degreesOfFreedom < 1) {
96
- throw new RangeError(
97
- "Student-t confidence interval degreesOfFreedom must be a positive integer."
98
- );
99
- }
100
- return studentTCriticalValue(normalized.level, degreesOfFreedom);
101
- }
1
+ import{quantileSorted as d,requireFiniteResult as c}from"../numeric.js";import{cloneAndFreeze as m,isPlainObject as w}from"../../core/immutable.js";import{studentTCriticalValue as g}from"./studentT.js";const h=Object.freeze(["normal","student-t"]);function l(e,n,r=!1){if(r&&w(e)){const t=Object.keys(e).find(a=>!["type","seed","samples"].includes(a));if(t!==void 0)throw new Error(`Unknown bootstrap method property "${t}".`);if(e.type!=="bootstrap")throw new Error(`Unsupported ${n} method type.`);if(!Number.isInteger(e.seed)||e.seed<1||e.seed>2147483646)throw new RangeError("Bootstrap seed must be an integer between 1 and 2147483646.");const o=e.samples===void 0?1e3:e.samples;if(!Number.isInteger(o)||o<1||o>1e4)throw new RangeError("Bootstrap samples must be an integer between 1 and 10000.");return{type:"bootstrap",seed:e.seed,samples:o}}if(!h.includes(e))throw new Error(`Unsupported ${n} method "${e}".`);return e}function f(e,n){if(!Number.isFinite(e)||e<=0||e>=1)throw new RangeError(`${n} level must be between 0 and 1.`)}function E(e){const n=[-39.69683028665376,220.9460984245205,-275.9285104469687,138.357751867269,-30.66479806614716,2.506628277459239],r=[-54.47609879822406,161.5858368580409,-155.6989798598866,66.80131188771972,-13.28068155288572],t=[-.007784894002430293,-.3223964580411365,-2.400758277161838,-2.549732539343734,4.374664141464968,2.938163982698783],o=[.007784695709041462,.3224671290700398,2.445134137142996,3.754408661907416],a=.02425,u=1-a;if(e<a){const s=Math.sqrt(-2*Math.log(e));return(((((t[0]*s+t[1])*s+t[2])*s+t[3])*s+t[4])*s+t[5])/((((o[0]*s+o[1])*s+o[2])*s+o[3])*s+1)}if(e>u){const s=Math.sqrt(-2*Math.log(1-e));return-(((((t[0]*s+t[1])*s+t[2])*s+t[3])*s+t[4])*s+t[5])/((((o[0]*s+o[1])*s+o[2])*s+o[3])*s+1)}const p=e-.5,i=p*p;return(((((n[0]*i+n[1])*i+n[2])*i+n[3])*i+n[4])*i+n[5])*p/(((((r[0]*i+r[1])*i+r[2])*i+r[3])*i+r[4])*i+1)}function b({method:e,level:n}={},{defaultMethod:r,defaultLevel:t=.95,label:o="Confidence interval",allowBootstrap:a=!1}={}){l(r,o);const u=l(e??r,o,a),p=n??t;return f(p,o),m({method:u,level:p})}function B(e){return f(e,"Normal confidence interval"),e===.95?1.96:E((1+e)/2)}function q({method:e,level:n,degreesOfFreedom:r}){const t=b({method:e,level:n},{defaultMethod:e,label:"Confidence interval"});if(t.method==="normal")return B(t.level);if(!Number.isInteger(r)||r<1)throw new RangeError("Student-t confidence interval degreesOfFreedom must be a positive integer.");return g(t.level,r)}function x(e){if(!Number.isSafeInteger(e)||e<0||e>5e7)throw new RangeError("Bootstrap resampling cannot exceed 50000000 draws per action.")}function M(e){const n=l(e,"Bootstrap mean interval",!0);if(n?.type!=="bootstrap")throw new Error("Bootstrap sampler requires a bootstrap method.");let r=n.seed;return t=>{if(t.length===0)return[];const o=[];for(let a=0;a<n.samples;a+=1){let u=0;for(let p=0;p<t.length;p+=1)r=(1103515245*r+12345)%2147483647,u+=t[Math.floor(r/2147483647*t.length)];o.push(c(u/t.length,"Bootstrap sample mean"))}return o.sort((a,u)=>a-u)}}function I(e,n=.95){if(f(n,"Bootstrap mean interval"),e.length===0)return;const r=(1-n)/2;return{lower:c(d(e,r),"Bootstrap lower endpoint"),upper:c(d(e,1-r),"Bootstrap upper endpoint")}}export{h as CONFIDENCE_INTERVAL_METHODS,I as bootstrapMeanBounds,q as confidenceCriticalValue,M as createBootstrapMeanSampler,B as normalCriticalValue,b as normalizeConfidenceInterval,x as validateBootstrapDrawCount};
@@ -1,320 +1 @@
1
- import { scalarKey } from "./transformKeys.js";
2
- import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
3
- import { requireStringValue as requireField } from "../core/validation.js";
4
- import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
5
- import {
6
- aggregateRows,
7
- isScalarAggregate,
8
- validateAggregate,
9
- validateAggregateFieldValues
10
- } from "./aggregate.js";
11
- import {
12
- calculateWeightedAggregate,
13
- normalizeStatisticalWeight,
14
- readStatisticalWeights,
15
- summarizeStatisticalWeights,
16
- validateWeightedAggregate,
17
- validateWeightedNumericFields,
18
- weightedRows
19
- } from "./weightedStatistics.js";
20
-
21
- const TRANSFORM_KEYS = Object.freeze([
22
- "type", "groupBy", "aggregates", "members", "weight", "missing", "empty"
23
- ]);
24
- const AGGREGATE_KEYS = Object.freeze(["op", "field", "as"]);
25
- const NOMINAL_OPERATIONS = new Set(["distinct", "valid", "missing"]);
26
- const MAX_OUTPUT_ROWS = 30_000;
27
-
28
- function normalizeGroupBy(value) {
29
- if (value === undefined) return [];
30
- return Array.isArray(value) ? [...value] : [value];
31
- }
32
-
33
- function normalizeAggregate(value) {
34
- if (!isPlainObject(value)) return value;
35
- return {
36
- ...value,
37
- op: validateAggregate(value.op),
38
- ...(Object.hasOwn(value, "field") ? { field: value.field } : {}),
39
- as: value.as
40
- };
41
- }
42
-
43
- export function normalizeSummaryTransform({ groupBy, aggregates, members, weight, missing, empty } = {}) {
44
- const transform = {
45
- type: "summary",
46
- groupBy: normalizeGroupBy(groupBy),
47
- aggregates: Array.isArray(aggregates)
48
- ? aggregates.map(normalizeAggregate)
49
- : aggregates,
50
- ...(members === undefined ? {} : { members }),
51
- ...(weight === undefined
52
- ? {}
53
- : { weight: normalizeStatisticalWeight(weight, "Summary weight") }),
54
- ...(missing === undefined ? {} : { missing }),
55
- ...(empty === undefined ? {} : { empty })
56
- };
57
- validateSummaryTransform(transform);
58
- return cloneAndFreeze(transform);
59
- }
60
-
61
- export function validateSummaryTransform(transform) {
62
- if (!isPlainObject(transform)) {
63
- throw new TypeError("Summary transform must be a plain object.");
64
- }
65
- rejectUnknownKeys(transform, TRANSFORM_KEYS, "summary transform");
66
- if (transform.type !== "summary") {
67
- throw new Error(`Unsupported summary transform "${transform.type}".`);
68
- }
69
- if (
70
- !Array.isArray(transform.groupBy) ||
71
- transform.groupBy.some(field => typeof field !== "string" || field.length === 0)
72
- ) {
73
- throw new TypeError("Summary groupBy must contain field names.");
74
- }
75
- if (new Set(transform.groupBy).size !== transform.groupBy.length) {
76
- throw new Error("Summary groupBy fields must be unique.");
77
- }
78
- if (!Array.isArray(transform.aggregates) || transform.aggregates.length === 0) {
79
- throw new TypeError("Summary aggregates must be a non-empty array.");
80
- }
81
- if (transform.aggregates.length > 64) {
82
- throw new RangeError("Summary aggregates cannot contain more than 64 outputs.");
83
- }
84
- const outputs = new Set(transform.groupBy);
85
- transform.aggregates.forEach((aggregate, index) => {
86
- if (!isPlainObject(aggregate)) {
87
- throw new TypeError(`Summary aggregate ${index} must be a plain object.`);
88
- }
89
- rejectUnknownKeys(aggregate, AGGREGATE_KEYS, `summary aggregate ${index}`);
90
- const operation = validateAggregate(aggregate.op);
91
- if (transform.weight !== undefined) validateWeightedAggregate(operation);
92
- if (operation === "count") {
93
- if (Object.hasOwn(aggregate, "field")) {
94
- throw new Error("Summary count does not accept a field.");
95
- }
96
- } else {
97
- requireField(aggregate.field, `Summary aggregate ${index} field`);
98
- }
99
- const output = requireField(aggregate.as, `Summary aggregate ${index} as`);
100
- if (outputs.has(output)) {
101
- throw new Error(`Summary output field "${output}" collides with another output.`);
102
- }
103
- outputs.add(output);
104
- });
105
- if (transform.members !== undefined) {
106
- const members = requireField(transform.members, "Summary members");
107
- if (outputs.has(members)) {
108
- throw new Error(`Summary members field "${members}" collides with another output.`);
109
- }
110
- }
111
- if (transform.weight !== undefined) {
112
- normalizeStatisticalWeight(transform.weight, "Summary weight");
113
- }
114
- if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
115
- throw new Error('Summary missing must be "error" or "drop".');
116
- }
117
- if (transform.empty !== undefined && !["null", "identity"].includes(transform.empty)) {
118
- throw new Error('Summary empty must be "null" or "identity".');
119
- }
120
- return transform;
121
- }
122
-
123
- function requireSourceFields(rows, transform) {
124
- if (rows.length === 0) return;
125
- const fields = new Set(rows.flatMap(row => Object.keys(row)));
126
- for (const field of transform.groupBy) {
127
- if (!fields.has(field)) {
128
- throw new Error(`Summary source does not contain group field "${field}".`);
129
- }
130
- }
131
- for (const aggregate of transform.aggregates) {
132
- if (aggregate.field !== undefined && !fields.has(aggregate.field)) {
133
- throw new Error(`Summary source does not contain aggregate field "${aggregate.field}".`);
134
- }
135
- const orderBy = isPlainObject(aggregate.op) ? aggregate.op.orderBy : undefined;
136
- if (orderBy !== undefined && !fields.has(orderBy)) {
137
- throw new Error(`Summary source does not contain order field "${orderBy}".`);
138
- }
139
- }
140
- if (transform.weight !== undefined && !fields.has(transform.weight.field)) {
141
- throw new Error(`Summary source does not contain weight field "${transform.weight.field}".`);
142
- }
143
- }
144
-
145
- function validateAggregateValues(rows, aggregate) {
146
- if (aggregate.op === "count") return;
147
- if (isScalarAggregate(aggregate.op) && NOMINAL_OPERATIONS.has(aggregate.op)) {
148
- validateAggregateFieldValues(rows, aggregate.field, "nominal");
149
- return;
150
- }
151
- validateAggregateFieldValues(rows, aggregate.field, "quantitative");
152
- }
153
-
154
- export function deriveSummaryRows(rows, transform) {
155
- validateSummaryTransform(transform);
156
- requireSourceFields(rows, transform);
157
- const explicitWeighted = transform.weight !== undefined && transform.missing !== undefined;
158
- let weightEntries;
159
- if (transform.weight === undefined) {
160
- for (const aggregate of transform.aggregates) {
161
- validateAggregateValues(rows, aggregate);
162
- }
163
- } else if (!explicitWeighted) {
164
- weightEntries = readStatisticalWeights(rows, transform.weight, "Summary").entries;
165
- validateWeightedNumericFields(
166
- weightEntries,
167
- [...new Set(transform.aggregates.flatMap(aggregate =>
168
- aggregate.field === undefined ? [] : [aggregate.field]
169
- ))],
170
- "Summary"
171
- );
172
- } else {
173
- // Explicit missing policy is evaluated per measure. Validate every
174
- // nonmissing value now, while leaving nullish value/weight pairs for the
175
- // reason-priority accounting below.
176
- readStatisticalWeights(
177
- rows.filter(row => row[transform.weight.field] !== null && row[transform.weight.field] !== undefined),
178
- transform.weight,
179
- "Summary"
180
- );
181
- for (const aggregate of transform.aggregates) {
182
- if (aggregate.field === undefined) continue;
183
- validateAggregateValues(
184
- rows.filter(row => row[aggregate.field] !== null && row[aggregate.field] !== undefined),
185
- aggregate
186
- );
187
- }
188
- }
189
- if (transform.missing === "error") {
190
- if (transform.weight !== undefined) {
191
- const weightIndex = rows.findIndex(row =>
192
- row[transform.weight.field] === null || row[transform.weight.field] === undefined
193
- );
194
- if (weightIndex !== -1) {
195
- throw new TypeError(`Summary weight "${transform.weight.field}" is missing at row ${weightIndex}.`);
196
- }
197
- }
198
- for (const aggregate of transform.aggregates) {
199
- if (aggregate.field === undefined) continue;
200
- const index = rows.findIndex(row => row[aggregate.field] === null || row[aggregate.field] === undefined);
201
- if (index !== -1) throw new TypeError(`Summary field "${aggregate.field}" is missing at row ${index}.`);
202
- }
203
- }
204
-
205
- const groups = new Map();
206
- if (transform.groupBy.length === 0) {
207
- groups.set("all", {
208
- values: {},
209
- rows,
210
- ...(weightEntries === undefined ? {} : { entries: weightEntries })
211
- });
212
- } else {
213
- rows.forEach((row, index) => {
214
- const values = Object.fromEntries(transform.groupBy.map(field => [field, row[field]]));
215
- const key = transform.groupBy.map(field =>
216
- scalarKey(row[field], `Summary group field "${field}"`)
217
- ).join("|");
218
- const group = groups.get(key) ?? {
219
- values,
220
- rows: [],
221
- ...(weightEntries === undefined ? {} : { entries: [] })
222
- };
223
- group.rows.push(row);
224
- if (weightEntries !== undefined) group.entries.push(weightEntries[index]);
225
- groups.set(key, group);
226
- });
227
- }
228
- if (groups.size > MAX_OUTPUT_ROWS) {
229
- throw new RangeError(`Summary output cannot exceed ${MAX_OUTPUT_ROWS} groups.`);
230
- }
231
-
232
- const units = [];
233
- const values = [...groups.values()].map(group => {
234
- const sharedWeightSummary = transform.weight === undefined || explicitWeighted
235
- ? undefined
236
- : summarizeStatisticalWeights(
237
- group.entries,
238
- transform.weight.kind,
239
- "Summary group"
240
- );
241
- const aggregated = Object.fromEntries(transform.aggregates.map(aggregate => {
242
- let missingValue = 0;
243
- let missingWeight = 0;
244
- let zeroWeightRows = 0;
245
- let measureWeightSummary = sharedWeightSummary;
246
- if (explicitWeighted) {
247
- const eligible = group.rows.filter(row => {
248
- if (aggregate.field !== undefined &&
249
- (row[aggregate.field] === null || row[aggregate.field] === undefined)) {
250
- missingValue += 1;
251
- return false;
252
- }
253
- if (row[transform.weight.field] === null || row[transform.weight.field] === undefined) {
254
- missingWeight += 1;
255
- return false;
256
- }
257
- return true;
258
- });
259
- const entries = readStatisticalWeights(eligible, transform.weight, "Summary").entries;
260
- zeroWeightRows = entries.filter(entry => entry.weight === 0).length;
261
- measureWeightSummary = summarizeStatisticalWeights(
262
- entries,
263
- transform.weight.kind,
264
- "Summary group"
265
- );
266
- }
267
- let value = measureWeightSummary === undefined
268
- ? aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
269
- : calculateWeightedAggregate(
270
- measureWeightSummary,
271
- aggregate.field,
272
- aggregate.op,
273
- `Summary aggregate "${aggregate.as}"`
274
- );
275
- if (value === undefined && transform.empty !== undefined) {
276
- const op = typeof aggregate.op === "string" ? aggregate.op : aggregate.op.op;
277
- value = transform.empty === "identity" && op === "sum" ? 0 : null;
278
- }
279
- if (transform.missing !== undefined || transform.empty !== undefined) {
280
- const missing = explicitWeighted
281
- ? missingValue + missingWeight
282
- : aggregate.field === undefined ? 0 : group.rows.filter(row => row[aggregate.field] === null || row[aggregate.field] === undefined).length;
283
- units.push({
284
- role: aggregate.as,
285
- group: group.values,
286
- inputRows: group.rows.length,
287
- usedRows: group.rows.length - missing,
288
- excludedRows: missing,
289
- excludedByReason: missing === 0 ? {} : {
290
- ...(missingValue === 0 && explicitWeighted ? {} : { "missing-value": explicitWeighted ? missingValue : missing }),
291
- ...(missingWeight === 0 ? {} : { "missing-weight": missingWeight })
292
- },
293
- ...(explicitWeighted ? { zeroWeightRows } : {})
294
- });
295
- }
296
- return [aggregate.as, value];
297
- }));
298
- return {
299
- ...group.values,
300
- ...aggregated,
301
- ...(transform.members === undefined
302
- ? {}
303
- : {
304
- [transform.members]: sharedWeightSummary === undefined
305
- ? group.rows
306
- : weightedRows(sharedWeightSummary)
307
- })
308
- };
309
- });
310
- if (transform.missing === undefined && transform.empty === undefined) return values;
311
- return {
312
- values,
313
- report: cloneAndFreeze({
314
- version: 1,
315
- owner: { kind: "data", id: "pending" },
316
- inputs: [],
317
- units
318
- })
319
- };
320
- }
1
+ import{createBootstrapMeanSampler as q,bootstrapMeanBounds as I,validateBootstrapDrawCount as U}from"./statistics/confidenceInterval.js";import{scalarKey as V}from"./transformKeys.js";import{rejectUnknownProperties as W}from"../core/validation.js";import{requireStringValue as E}from"../core/validation.js";import{cloneAndFreeze as v,isPlainObject as g}from"../core/immutable.js";import{aggregateRows as G,isScalarAggregate as K,validateAggregate as B,validateAggregateFieldValues as z}from"./aggregate.js";import{calculateWeightedAggregate as P,normalizeStatisticalWeight as M,readStatisticalWeights as A,summarizeStatisticalWeights as k,validateWeightedAggregate as J,validateWeightedNumericFields as L,weightedRows as Y}from"./weightedStatistics.js";const C=Object.freeze(["type","groupBy","aggregates","members","weight","missing","empty"]),D=Object.freeze(["op","field","as"]),X=new Set(["distinct","valid","missing"]),N=3e4;function H(e){return e===void 0?[]:Array.isArray(e)?[...e]:[e]}function Q(e){return g(e)?{...e,op:B(e.op,{allowBootstrap:!0}),...Object.hasOwn(e,"field")?{field:e.field}:{},as:e.as}:e}function ue({groupBy:e,aggregates:i,members:n,weight:o,missing:u,empty:l}={}){const m={type:"summary",groupBy:H(e),aggregates:Array.isArray(i)?i.map(Q):i,...n===void 0?{}:{members:n},...o===void 0?{}:{weight:M(o,"Summary weight")},...u===void 0?{}:{missing:u},...l===void 0?{}:{empty:l}};return x(m),v(m)}function x(e){if(!g(e))throw new TypeError("Summary transform must be a plain object.");if(W(e,C,"summary transform"),e.type!=="summary")throw new Error(`Unsupported summary transform "${e.type}".`);if(!Array.isArray(e.groupBy)||e.groupBy.some(n=>typeof n!="string"||n.length===0))throw new TypeError("Summary groupBy must contain field names.");if(new Set(e.groupBy).size!==e.groupBy.length)throw new Error("Summary groupBy fields must be unique.");if(!Array.isArray(e.aggregates)||e.aggregates.length===0)throw new TypeError("Summary aggregates must be a non-empty array.");if(e.aggregates.length>64)throw new RangeError("Summary aggregates cannot contain more than 64 outputs.");const i=new Set(e.groupBy);if(e.aggregates.forEach((n,o)=>{if(!g(n))throw new TypeError(`Summary aggregate ${o} must be a plain object.`);W(n,D,`summary aggregate ${o}`);const u=B(n.op,{allowBootstrap:!0});if(e.weight!==void 0&&J(u),u==="count"){if(Object.hasOwn(n,"field"))throw new Error("Summary count does not accept a field.")}else E(n.field,`Summary aggregate ${o} field`);const l=E(n.as,`Summary aggregate ${o} as`);if(i.has(l))throw new Error(`Summary output field "${l}" collides with another output.`);i.add(l)}),e.members!==void 0){const n=E(e.members,"Summary members");if(i.has(n))throw new Error(`Summary members field "${n}" collides with another output.`)}if(e.weight!==void 0&&M(e.weight,"Summary weight"),e.missing!==void 0&&!["error","drop"].includes(e.missing))throw new Error('Summary missing must be "error" or "drop".');if(e.empty!==void 0&&!["null","identity"].includes(e.empty))throw new Error('Summary empty must be "null" or "identity".');return e}function Z(e,i){if(e.length===0)return;const n=new Set(e.flatMap(o=>Object.keys(o)));for(const o of i.groupBy)if(!n.has(o))throw new Error(`Summary source does not contain group field "${o}".`);for(const o of i.aggregates){if(o.field!==void 0&&!n.has(o.field))throw new Error(`Summary source does not contain aggregate field "${o.field}".`);const u=g(o.op)?o.op.orderBy:void 0;if(u!==void 0&&!n.has(u))throw new Error(`Summary source does not contain order field "${u}".`)}if(i.weight!==void 0&&!n.has(i.weight.field))throw new Error(`Summary source does not contain weight field "${i.weight.field}".`)}function F(e,i){if(i.op!=="count"){if(K(i.op)&&X.has(i.op)){z(e,i.field,"nominal");return}z(e,i.field,"quantitative")}}function se(e,i){x(i),Z(e,i);const n=i.aggregates.map(t=>({...t,op:B(t.op,{allowBootstrap:!0})})),o=i.weight!==void 0&&i.missing!==void 0;let u;if(i.weight===void 0)for(const t of n)F(e,t);else if(!o)u=A(e,i.weight,"Summary").entries,L(u,[...new Set(n.flatMap(t=>t.field===void 0?[]:[t.field]))],"Summary");else{A(e.filter(t=>t[i.weight.field]!==null&&t[i.weight.field]!==void 0),i.weight,"Summary");for(const t of n)t.field!==void 0&&F(e.filter(d=>d[t.field]!==null&&d[t.field]!==void 0),t)}if(i.missing==="error"){if(i.weight!==void 0){const t=e.findIndex(d=>d[i.weight.field]===null||d[i.weight.field]===void 0);if(t!==-1)throw new TypeError(`Summary weight "${i.weight.field}" is missing at row ${t}.`)}for(const t of n){if(t.field===void 0)continue;const d=e.findIndex(f=>f[t.field]===null||f[t.field]===void 0);if(d!==-1)throw new TypeError(`Summary field "${t.field}" is missing at row ${d}.`)}}const l=new Map;if(i.groupBy.length===0?l.set("all",{values:{},rows:e,...u===void 0?{}:{entries:u}}):e.forEach((t,d)=>{const f=Object.fromEntries(i.groupBy.map(a=>[a,t[a]])),c=i.groupBy.map(a=>V(t[a],`Summary group field "${a}"`)).join("|"),r=l.get(c)??{values:f,rows:[],...u===void 0?{}:{entries:[]}};r.rows.push(t),u!==void 0&&r.entries.push(u[d]),l.set(c,r)}),l.size>N)throw new RangeError(`Summary output cannot exceed ${N} groups.`);const m=new Map;let O=0;for(const t of n){const d=t.op?.method;if(d?.type!=="bootstrap")continue;const f=JSON.stringify([t.field,d.samples,d.seed]);m.has(f)||(O+=d.samples*e.filter(c=>Number.isFinite(c[t.field])).length,U(O),m.set(f,q(d)))}const $=[],R=[...l.values()].map(t=>{const d=new Map,f=i.weight===void 0||o?void 0:k(t.entries,i.weight.kind,"Summary group"),c=Object.fromEntries(n.map(r=>{let a=0,h=0,j=0,S=f;if(o){const s=t.rows.filter(p=>r.field!==void 0&&(p[r.field]===null||p[r.field]===void 0)?(a+=1,!1):p[i.weight.field]===null||p[i.weight.field]===void 0?(h+=1,!1):!0),y=A(s,i.weight,"Summary").entries;j=y.filter(p=>p.weight===0).length,S=k(y,i.weight.kind,"Summary group")}const w=r.op?.method;let T;if(w?.type==="bootstrap"){const s=JSON.stringify([r.field,w.samples,w.seed]);if(!d.has(s)){const p=t.rows.map(_=>_[r.field]).filter(Number.isFinite);d.set(s,m.get(s)(p))}T=I(d.get(s),r.op.level)?.[r.op.op==="ciLower"?"lower":"upper"]}let b=w?.type==="bootstrap"?T:S===void 0?G(t.rows,r.field??"__row",r.op):P(S,r.field,r.op,`Summary aggregate "${r.as}"`);if(b===void 0&&i.empty!==void 0){const s=typeof r.op=="string"?r.op:r.op.op;b=i.empty==="identity"&&s==="sum"?0:null}if(i.missing!==void 0||i.empty!==void 0){const s=o?a+h:r.field===void 0?0:t.rows.filter(y=>y[r.field]===null||y[r.field]===void 0).length;$.push({role:r.as,group:t.values,inputRows:t.rows.length,usedRows:t.rows.length-s,excludedRows:s,excludedByReason:s===0?{}:{...a===0&&o?{}:{"missing-value":o?a:s},...h===0?{}:{"missing-weight":h}},...o?{zeroWeightRows:j}:{}})}return[r.as,b]}));return{...t.values,...c,...i.members===void 0?{}:{[i.members]:f===void 0?t.rows:Y(f)}}});return i.missing===void 0&&i.empty===void 0?R:{values:R,report:v({version:1,owner:{kind:"data",id:"pending"},inputs:[],units:$})}}export{se as deriveSummaryRows,ue as normalizeSummaryTransform,x as validateSummaryTransform};
@@ -45,7 +45,7 @@ export function assertPathGroupCompatible(program, layer, dataset, group) {
45
45
  ![undefined, "overlay"].includes(layer.encoding?.color?.layout)
46
46
  )) {
47
47
  if (group?.fields !== undefined ||
48
- (layer.encoding?.color !== undefined && field !== layer.encoding.color.field)) {
48
+ (layer.encoding?.color !== undefined && layer.encoding.color.fieldType !== "quantitative" && field !== layer.encoding.color.field)) {
49
49
  throw new Error("Area layout requires one group field matching its color field.");
50
50
  }
51
51
  }
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Must match package.json; enforced by the snapshot producer-version test.
2
- export const packageVersion = "0.0.19";
2
+ export const packageVersion = "0.0.21";
package/types/index.d.ts CHANGED
@@ -391,6 +391,8 @@ export type {
391
391
  StackDataMode,
392
392
  StackDataOptions,
393
393
  StackDataOutputFields,
394
+ BootstrapMeanIntervalMethod,
395
+ MeanConfidenceIntervalMethod,
394
396
  SummaryAggregateOptions,
395
397
  SummaryDataOptions,
396
398
  YStackMode,
@@ -563,7 +563,7 @@ export type DatasetIntervalTransform = {
563
563
  | {
564
564
  center: "mean";
565
565
  extent: "ci";
566
- method?: ConfidenceIntervalMethod;
566
+ method?: MeanConfidenceIntervalMethod;
567
567
  level: number;
568
568
  }
569
569
  | {
@@ -1001,7 +1001,7 @@ export interface DatasetSummaryTransform {
1001
1001
  empty?: "null" | "identity";
1002
1002
  }
1003
1003
  export interface SummaryAggregateOptions {
1004
- op: AggregateOperation;
1004
+ op: AggregateOperation | { op: "ciLower" | "ciUpper"; method: BootstrapMeanIntervalMethod; level?: number };
1005
1005
  field?: string;
1006
1006
  as: string;
1007
1007
  }
@@ -1106,6 +1106,13 @@ export type ScalarAggregateOperation =
1106
1106
  | "variance" | "varianceP" | "stdev" | "stdevP" | "stderr"
1107
1107
  | "q1" | "q3" | "ciLower" | "ciUpper";
1108
1108
  export type ConfidenceIntervalMethod = "normal" | "student-t";
1109
+ /** Seeded percentile bootstrap of nonweighted means; samples defaults to 1000. */
1110
+ export interface BootstrapMeanIntervalMethod {
1111
+ type: "bootstrap";
1112
+ seed: number;
1113
+ samples?: number;
1114
+ }
1115
+ export type MeanConfidenceIntervalMethod = ConfidenceIntervalMethod | BootstrapMeanIntervalMethod;
1109
1116
  export type ParameterizedAggregateOperation =
1110
1117
  | { op: "quantile"; probability: number }
1111
1118
  | {
@@ -2372,7 +2379,7 @@ export interface IntervalDataOptions {
2372
2379
  groupBy?: string | readonly string[];
2373
2380
  center?: IntervalCenter;
2374
2381
  extent?: IntervalExtent;
2375
- method?: ConfidenceIntervalMethod;
2382
+ method?: MeanConfidenceIntervalMethod;
2376
2383
  level?: number;
2377
2384
  missing?: "error" | "drop";
2378
2385
  as?: IntervalOutputFields;
@@ -4321,6 +4328,10 @@ export type DiscretizedColorScaleOptions =
4321
4328
  | QuantileColorScaleOptions
4322
4329
  | ThresholdColorScaleOptions;
4323
4330
 
4331
+ /** Area quantitative color requires one finite value per existing series;
4332
+ * aggregate, temporal color and unknown fallback remain mark-specific restrictions.
4333
+ * Area authoring uses the Full facade; createAreaPlot.color stays categorical.
4334
+ */
4324
4335
  export type ColorEncodingOptions =
4325
4336
  | CategoricalEncodingOptions
4326
4337
  | {