ggaction 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +3 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +33 -27
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +40 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +37 -2
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +79 -24
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +153 -11
- package/src/actions/errorBars/edit.js +365 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +44 -23
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +138 -14
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +60 -4
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +213 -65
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionCompatibility.js +1 -1
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +9 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/ordinal.js +23 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +86 -23
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facetGuides/legacyCategorical.js +7 -1
- package/src/materialization/facetGuides/placement.js +16 -1
- package/src/materialization/facetGuides/preparation.js +26 -1
- package/src/materialization/facets.js +11 -2
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +30 -7
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +63 -25
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +70 -14
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/bar.js +0 -43
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +48 -18
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1327 -124
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { layoutSeriesPartition } from "./seriesLayout.js";
|
|
3
|
+
|
|
4
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
5
|
+
"type", "category", "group", "value", "mode", "as"
|
|
6
|
+
]);
|
|
7
|
+
const AS_KEYS = Object.freeze(["start", "end", "value", "share"]);
|
|
8
|
+
const MODES = new Set(["stack", "fill", "center", "diverging"]);
|
|
9
|
+
const MAX_OUTPUT_ROWS = 10_000;
|
|
10
|
+
|
|
11
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
12
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
13
|
+
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requireField(value, label) {
|
|
17
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
18
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeAs(value, field) {
|
|
24
|
+
const requested = value ?? {};
|
|
25
|
+
if (!isPlainObject(requested)) return requested;
|
|
26
|
+
return {
|
|
27
|
+
...requested,
|
|
28
|
+
start: requested.start ?? `${field}_start`,
|
|
29
|
+
end: requested.end ?? `${field}_end`,
|
|
30
|
+
value: requested.value ?? `${field}_value`,
|
|
31
|
+
share: requested.share ?? `${field}_share`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizeStackTransform({ category, group, value, mode, as } = {}) {
|
|
36
|
+
const transform = {
|
|
37
|
+
type: "stack",
|
|
38
|
+
category,
|
|
39
|
+
group,
|
|
40
|
+
value,
|
|
41
|
+
mode: mode ?? "stack",
|
|
42
|
+
as: normalizeAs(as, value)
|
|
43
|
+
};
|
|
44
|
+
validateStackTransform(transform);
|
|
45
|
+
return cloneAndFreeze(transform);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function validateStackTransform(transform) {
|
|
49
|
+
if (!isPlainObject(transform)) {
|
|
50
|
+
throw new TypeError("Stack transform must be a plain object.");
|
|
51
|
+
}
|
|
52
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "stack transform");
|
|
53
|
+
if (transform.type !== "stack") {
|
|
54
|
+
throw new Error(`Unsupported stack transform "${transform.type}".`);
|
|
55
|
+
}
|
|
56
|
+
const roles = [
|
|
57
|
+
requireField(transform.category, "Stack category"),
|
|
58
|
+
requireField(transform.group, "Stack group"),
|
|
59
|
+
requireField(transform.value, "Stack value")
|
|
60
|
+
];
|
|
61
|
+
if (new Set(roles).size !== roles.length) {
|
|
62
|
+
throw new Error("Stack category, group, and value fields must be unique.");
|
|
63
|
+
}
|
|
64
|
+
if (!MODES.has(transform.mode)) {
|
|
65
|
+
throw new Error(`Unsupported stack mode "${transform.mode}".`);
|
|
66
|
+
}
|
|
67
|
+
if (!isPlainObject(transform.as)) {
|
|
68
|
+
throw new TypeError("Stack as must be a plain object.");
|
|
69
|
+
}
|
|
70
|
+
rejectUnknownKeys(transform.as, AS_KEYS, "stack as");
|
|
71
|
+
const outputs = AS_KEYS.map(key => requireField(transform.as[key], `Stack as.${key}`));
|
|
72
|
+
if (new Set(outputs).size !== outputs.length) {
|
|
73
|
+
throw new Error("Stack output fields must be unique.");
|
|
74
|
+
}
|
|
75
|
+
return transform;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scalarKey(value, label) {
|
|
79
|
+
if (value === null) return "null";
|
|
80
|
+
if (typeof value === "string") return `string:${value.length}:${value}`;
|
|
81
|
+
if (typeof value === "boolean") return `boolean:${value}`;
|
|
82
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
83
|
+
return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
84
|
+
}
|
|
85
|
+
throw new TypeError(`${label} must contain null, strings, booleans, or finite numbers.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolvedPartition(entries, mode) {
|
|
89
|
+
const values = entries.map(entry => entry.row.value);
|
|
90
|
+
const segments = layoutSeriesPartition(values, mode);
|
|
91
|
+
const byIndex = new Map(segments.map(segment => [segment.index, segment]));
|
|
92
|
+
const shares = layoutSeriesPartition(values.map(Math.abs), "fill");
|
|
93
|
+
const shareByIndex = new Map(shares.map(segment => [
|
|
94
|
+
segment.index,
|
|
95
|
+
segment.end - segment.start
|
|
96
|
+
]));
|
|
97
|
+
let boundary = mode === "center" ? segments[0]?.start ?? 0 : 0;
|
|
98
|
+
return entries.map((entry, index) => {
|
|
99
|
+
const segment = byIndex.get(index);
|
|
100
|
+
const start = segment?.start ?? (mode === "diverging" ? 0 : boundary);
|
|
101
|
+
const end = segment?.end ?? start;
|
|
102
|
+
if (segment !== undefined && mode !== "diverging") boundary = segment.end;
|
|
103
|
+
return {
|
|
104
|
+
index: entry.index,
|
|
105
|
+
value: entry.row.value,
|
|
106
|
+
start,
|
|
107
|
+
end,
|
|
108
|
+
share: shareByIndex.get(index) ?? 0
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function deriveStackRows(rows, transform) {
|
|
114
|
+
validateStackTransform(transform);
|
|
115
|
+
if (rows.length > MAX_OUTPUT_ROWS) {
|
|
116
|
+
throw new RangeError(`Stack output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
|
|
117
|
+
}
|
|
118
|
+
const sourceFields = new Set(rows.flatMap(row => Object.keys(row)));
|
|
119
|
+
for (const output of Object.values(transform.as)) {
|
|
120
|
+
if (sourceFields.has(output)) {
|
|
121
|
+
throw new Error(`Stack output field "${output}" already exists.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const partitions = new Map();
|
|
125
|
+
const groups = new Map();
|
|
126
|
+
rows.forEach((source, index) => {
|
|
127
|
+
for (const field of [transform.category, transform.group, transform.value]) {
|
|
128
|
+
if (!Object.hasOwn(source, field)) {
|
|
129
|
+
throw new Error(`Stack source does not contain field "${field}" at row ${index}.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const categoryKey = scalarKey(source[transform.category], "Stack category field");
|
|
133
|
+
const groupKey = scalarKey(source[transform.group], "Stack group field");
|
|
134
|
+
if (!Number.isFinite(source[transform.value])) {
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
`Stack value field "${transform.value}" must contain a finite number at row ${index}.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (!groups.has(groupKey)) groups.set(groupKey, groups.size);
|
|
140
|
+
let partition = partitions.get(categoryKey);
|
|
141
|
+
if (partition === undefined) {
|
|
142
|
+
partition = new Map();
|
|
143
|
+
partitions.set(categoryKey, partition);
|
|
144
|
+
}
|
|
145
|
+
if (partition.has(groupKey)) {
|
|
146
|
+
throw new Error("Stack source requires one row per category/group cell.");
|
|
147
|
+
}
|
|
148
|
+
partition.set(groupKey, {
|
|
149
|
+
index,
|
|
150
|
+
row: { groupKey, value: source[transform.value] }
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const resolved = new Array(rows.length);
|
|
155
|
+
for (const partition of partitions.values()) {
|
|
156
|
+
const entries = [...partition.values()].sort((left, right) =>
|
|
157
|
+
groups.get(left.row.groupKey) - groups.get(right.row.groupKey)
|
|
158
|
+
);
|
|
159
|
+
for (const item of resolvedPartition(entries, transform.mode)) {
|
|
160
|
+
resolved[item.index] = item;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return rows.map((row, index) => ({
|
|
164
|
+
...row,
|
|
165
|
+
[transform.as.start]: resolved[index].start,
|
|
166
|
+
[transform.as.end]: resolved[index].end,
|
|
167
|
+
[transform.as.value]: resolved[index].value,
|
|
168
|
+
[transform.as.share]: resolved[index].share
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
aggregateRows,
|
|
4
|
+
isScalarAggregate,
|
|
5
|
+
validateAggregate,
|
|
6
|
+
validateAggregateFieldValues
|
|
7
|
+
} from "./aggregate.js";
|
|
8
|
+
|
|
9
|
+
const TRANSFORM_KEYS = Object.freeze([
|
|
10
|
+
"type", "groupBy", "aggregates", "members"
|
|
11
|
+
]);
|
|
12
|
+
const AGGREGATE_KEYS = Object.freeze(["op", "field", "as"]);
|
|
13
|
+
const NOMINAL_OPERATIONS = new Set(["distinct", "valid", "missing"]);
|
|
14
|
+
const MAX_OUTPUT_ROWS = 10_000;
|
|
15
|
+
|
|
16
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
17
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
18
|
+
if (unknown !== undefined) {
|
|
19
|
+
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireField(value, label) {
|
|
24
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
25
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeGroupBy(value) {
|
|
31
|
+
if (value === undefined) return [];
|
|
32
|
+
return Array.isArray(value) ? [...value] : [value];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeAggregate(value) {
|
|
36
|
+
if (!isPlainObject(value)) return value;
|
|
37
|
+
return {
|
|
38
|
+
...value,
|
|
39
|
+
op: validateAggregate(value.op),
|
|
40
|
+
...(Object.hasOwn(value, "field") ? { field: value.field } : {}),
|
|
41
|
+
as: value.as
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeSummaryTransform({ groupBy, aggregates, members } = {}) {
|
|
46
|
+
const transform = {
|
|
47
|
+
type: "summary",
|
|
48
|
+
groupBy: normalizeGroupBy(groupBy),
|
|
49
|
+
aggregates: Array.isArray(aggregates)
|
|
50
|
+
? aggregates.map(normalizeAggregate)
|
|
51
|
+
: aggregates,
|
|
52
|
+
...(members === undefined ? {} : { members })
|
|
53
|
+
};
|
|
54
|
+
validateSummaryTransform(transform);
|
|
55
|
+
return cloneAndFreeze(transform);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateSummaryTransform(transform) {
|
|
59
|
+
if (!isPlainObject(transform)) {
|
|
60
|
+
throw new TypeError("Summary transform must be a plain object.");
|
|
61
|
+
}
|
|
62
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "summary transform");
|
|
63
|
+
if (transform.type !== "summary") {
|
|
64
|
+
throw new Error(`Unsupported summary transform "${transform.type}".`);
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
!Array.isArray(transform.groupBy) ||
|
|
68
|
+
transform.groupBy.some(field => typeof field !== "string" || field.length === 0)
|
|
69
|
+
) {
|
|
70
|
+
throw new TypeError("Summary groupBy must contain field names.");
|
|
71
|
+
}
|
|
72
|
+
if (new Set(transform.groupBy).size !== transform.groupBy.length) {
|
|
73
|
+
throw new Error("Summary groupBy fields must be unique.");
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(transform.aggregates) || transform.aggregates.length === 0) {
|
|
76
|
+
throw new TypeError("Summary aggregates must be a non-empty array.");
|
|
77
|
+
}
|
|
78
|
+
if (transform.aggregates.length > 64) {
|
|
79
|
+
throw new RangeError("Summary aggregates cannot contain more than 64 outputs.");
|
|
80
|
+
}
|
|
81
|
+
const outputs = new Set(transform.groupBy);
|
|
82
|
+
transform.aggregates.forEach((aggregate, index) => {
|
|
83
|
+
if (!isPlainObject(aggregate)) {
|
|
84
|
+
throw new TypeError(`Summary aggregate ${index} must be a plain object.`);
|
|
85
|
+
}
|
|
86
|
+
rejectUnknownKeys(aggregate, AGGREGATE_KEYS, `summary aggregate ${index}`);
|
|
87
|
+
const operation = validateAggregate(aggregate.op);
|
|
88
|
+
if (operation === "count") {
|
|
89
|
+
if (Object.hasOwn(aggregate, "field")) {
|
|
90
|
+
throw new Error("Summary count does not accept a field.");
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
requireField(aggregate.field, `Summary aggregate ${index} field`);
|
|
94
|
+
}
|
|
95
|
+
const output = requireField(aggregate.as, `Summary aggregate ${index} as`);
|
|
96
|
+
if (outputs.has(output)) {
|
|
97
|
+
throw new Error(`Summary output field "${output}" collides with another output.`);
|
|
98
|
+
}
|
|
99
|
+
outputs.add(output);
|
|
100
|
+
});
|
|
101
|
+
if (transform.members !== undefined) {
|
|
102
|
+
const members = requireField(transform.members, "Summary members");
|
|
103
|
+
if (outputs.has(members)) {
|
|
104
|
+
throw new Error(`Summary members field "${members}" collides with another output.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return transform;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function scalarKey(value, label) {
|
|
111
|
+
if (value === null) return "null";
|
|
112
|
+
if (typeof value === "string") return `string:${value.length}:${value}`;
|
|
113
|
+
if (typeof value === "boolean") return `boolean:${value}`;
|
|
114
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
115
|
+
return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
116
|
+
}
|
|
117
|
+
throw new TypeError(
|
|
118
|
+
`${label} must contain null, strings, booleans, or finite numbers.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function requireSourceFields(rows, transform) {
|
|
123
|
+
if (rows.length === 0) return;
|
|
124
|
+
const fields = new Set(rows.flatMap(row => Object.keys(row)));
|
|
125
|
+
for (const field of transform.groupBy) {
|
|
126
|
+
if (!fields.has(field)) {
|
|
127
|
+
throw new Error(`Summary source does not contain group field "${field}".`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const aggregate of transform.aggregates) {
|
|
131
|
+
if (aggregate.field !== undefined && !fields.has(aggregate.field)) {
|
|
132
|
+
throw new Error(`Summary source does not contain aggregate field "${aggregate.field}".`);
|
|
133
|
+
}
|
|
134
|
+
const orderBy = isPlainObject(aggregate.op) ? aggregate.op.orderBy : undefined;
|
|
135
|
+
if (orderBy !== undefined && !fields.has(orderBy)) {
|
|
136
|
+
throw new Error(`Summary source does not contain order field "${orderBy}".`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validateAggregateValues(rows, aggregate) {
|
|
142
|
+
if (aggregate.op === "count") return;
|
|
143
|
+
if (isScalarAggregate(aggregate.op) && NOMINAL_OPERATIONS.has(aggregate.op)) {
|
|
144
|
+
validateAggregateFieldValues(rows, aggregate.field, "nominal");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
validateAggregateFieldValues(rows, aggregate.field, "quantitative");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function deriveSummaryRows(rows, transform) {
|
|
151
|
+
validateSummaryTransform(transform);
|
|
152
|
+
requireSourceFields(rows, transform);
|
|
153
|
+
for (const aggregate of transform.aggregates) {
|
|
154
|
+
validateAggregateValues(rows, aggregate);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const groups = new Map();
|
|
158
|
+
if (transform.groupBy.length === 0) {
|
|
159
|
+
groups.set("all", { values: {}, rows });
|
|
160
|
+
} else {
|
|
161
|
+
rows.forEach(row => {
|
|
162
|
+
const values = Object.fromEntries(transform.groupBy.map(field => [field, row[field]]));
|
|
163
|
+
const key = transform.groupBy.map(field =>
|
|
164
|
+
scalarKey(row[field], `Summary group field "${field}"`)
|
|
165
|
+
).join("|");
|
|
166
|
+
const group = groups.get(key) ?? { values, rows: [] };
|
|
167
|
+
group.rows.push(row);
|
|
168
|
+
groups.set(key, group);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (groups.size > MAX_OUTPUT_ROWS) {
|
|
172
|
+
throw new RangeError(`Summary output cannot exceed ${MAX_OUTPUT_ROWS} groups.`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [...groups.values()].map(group => ({
|
|
176
|
+
...group.values,
|
|
177
|
+
...Object.fromEntries(transform.aggregates.map(aggregate => [
|
|
178
|
+
aggregate.as,
|
|
179
|
+
aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
|
|
180
|
+
])),
|
|
181
|
+
...(transform.members === undefined ? {} : { [transform.members]: group.rows })
|
|
182
|
+
}));
|
|
183
|
+
}
|
package/src/grammar/text.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
2
|
import { validateConcreteGraphicValue } from "./schemas/concreteGraphic.js";
|
|
3
|
+
import {
|
|
4
|
+
formatValue,
|
|
5
|
+
isUtcValueFormat,
|
|
6
|
+
validateValueFormat
|
|
7
|
+
} from "./valueFormat.js";
|
|
8
|
+
import { resolveRotation } from "./rotation.js";
|
|
9
|
+
|
|
10
|
+
export function isSourceOwnedText(layer) {
|
|
11
|
+
return layer?.mark?.type === "text" && layer.source !== undefined;
|
|
12
|
+
}
|
|
3
13
|
|
|
4
14
|
export const DEFAULT_TEXT_MARK = cloneAndFreeze({
|
|
5
15
|
fill: "#334155",
|
|
@@ -14,20 +24,13 @@ export const DEFAULT_TEXT_MARK = cloneAndFreeze({
|
|
|
14
24
|
dy: 0
|
|
15
25
|
});
|
|
16
26
|
|
|
17
|
-
const FIXED_FORMAT = /^\.(\d{1,2})f$/;
|
|
18
|
-
|
|
19
27
|
export function validateTextFormat(format) {
|
|
20
|
-
if (format
|
|
21
|
-
if (typeof format !== "string" || !FIXED_FORMAT.test(format)) {
|
|
28
|
+
if (format !== undefined && typeof format !== "string") {
|
|
22
29
|
throw new Error(
|
|
23
|
-
'Text format must be "auto" or a
|
|
30
|
+
'Text format must be "auto" or a supported numeric/UTC format string.'
|
|
24
31
|
);
|
|
25
32
|
}
|
|
26
|
-
|
|
27
|
-
if (decimals > 12) {
|
|
28
|
-
throw new RangeError("Text fixed-decimal format supports at most 12 decimals.");
|
|
29
|
-
}
|
|
30
|
-
return format;
|
|
33
|
+
return validateValueFormat(format, "Text format");
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export function formatTextValue(value, format = "auto") {
|
|
@@ -37,10 +40,11 @@ export function formatTextValue(value, format = "auto") {
|
|
|
37
40
|
const text = String(value);
|
|
38
41
|
return text.length === 0 ? undefined : text;
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
return formatValue(value, {
|
|
44
|
+
format: resolved,
|
|
45
|
+
valueType: isUtcValueFormat(resolved) ? "temporal" : "quantitative",
|
|
46
|
+
label: "Text format"
|
|
47
|
+
});
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
|
|
@@ -52,14 +56,16 @@ export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
|
|
|
52
56
|
fontFamily: "fontFamily",
|
|
53
57
|
fontWeight: "fontWeight",
|
|
54
58
|
align: "textAlign",
|
|
55
|
-
baseline: "textBaseline"
|
|
56
|
-
rotation: "rotation"
|
|
59
|
+
baseline: "textBaseline"
|
|
57
60
|
};
|
|
58
61
|
for (const [option, property] of Object.entries(mapping)) {
|
|
59
62
|
if (!Object.hasOwn(options, option)) continue;
|
|
60
63
|
validateConcreteGraphicValue("text", property, options[option]);
|
|
61
64
|
config[option] = options[option];
|
|
62
65
|
}
|
|
66
|
+
if (Object.hasOwn(options, "rotation")) {
|
|
67
|
+
config.rotation = resolveRotation(options.rotation, "Text rotation");
|
|
68
|
+
}
|
|
63
69
|
for (const property of ["dx", "dy"]) {
|
|
64
70
|
if (!Object.hasOwn(options, property)) continue;
|
|
65
71
|
if (!Number.isFinite(options[property])) {
|
package/src/grammar/timeUnit.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
-
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
2
|
+
import { normalizeTemporalValue, validateTemporalUnit } from "./scales/fields.js";
|
|
3
3
|
|
|
4
4
|
export function utcTimestamp(
|
|
5
5
|
year,
|
|
@@ -18,6 +18,14 @@ export function validTimestamp(value) {
|
|
|
18
18
|
return Number.isFinite(value) && Number.isFinite(new Date(value).getTime());
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function utcBucket(...parts) {
|
|
22
|
+
const timestamp = utcTimestamp(...parts);
|
|
23
|
+
if (!validTimestamp(timestamp)) {
|
|
24
|
+
throw new RangeError("Time-unit bucket start is outside the supported Date range.");
|
|
25
|
+
}
|
|
26
|
+
return timestamp;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export const TIME_UNITS = cloneAndFreeze([
|
|
22
30
|
"year",
|
|
23
31
|
"quarter",
|
|
@@ -28,7 +36,7 @@ export const TIME_UNITS = cloneAndFreeze([
|
|
|
28
36
|
"second"
|
|
29
37
|
]);
|
|
30
38
|
|
|
31
|
-
const TRANSFORM_KEYS = Object.freeze(["type", "field", "unit", "as"]);
|
|
39
|
+
const TRANSFORM_KEYS = Object.freeze(["type", "field", "unit", "as", "temporalUnit"]);
|
|
32
40
|
|
|
33
41
|
function requireField(value, label) {
|
|
34
42
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -52,6 +60,7 @@ export function validateTimeUnitTransform(transform) {
|
|
|
52
60
|
if (transform.type !== "timeUnit") {
|
|
53
61
|
throw new Error(`Unsupported time-unit transform "${transform.type}".`);
|
|
54
62
|
}
|
|
63
|
+
validateTemporalUnit(transform.temporalUnit);
|
|
55
64
|
const field = requireField(transform.field, "Time-unit field");
|
|
56
65
|
const output = requireField(transform.as, "Time-unit output field");
|
|
57
66
|
if (field === output) {
|
|
@@ -63,8 +72,9 @@ export function validateTimeUnitTransform(transform) {
|
|
|
63
72
|
return transform;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
export function normalizeTimeUnitTransform({ field, unit, as } = {}) {
|
|
67
|
-
const transform = { type: "timeUnit", field, unit, as
|
|
75
|
+
export function normalizeTimeUnitTransform({ field, unit, as, temporalUnit } = {}) {
|
|
76
|
+
const transform = { type: "timeUnit", field, unit, as,
|
|
77
|
+
...(temporalUnit === undefined ? {} : { temporalUnit }) };
|
|
68
78
|
validateTimeUnitTransform(transform);
|
|
69
79
|
return cloneAndFreeze(transform);
|
|
70
80
|
}
|
|
@@ -82,15 +92,15 @@ export function floorUtcTimeUnit(timestamp, unit) {
|
|
|
82
92
|
}
|
|
83
93
|
const year = date.getUTCFullYear();
|
|
84
94
|
const month = date.getUTCMonth();
|
|
85
|
-
if (unit === "year") return
|
|
86
|
-
if (unit === "quarter") return
|
|
87
|
-
if (unit === "month") return
|
|
95
|
+
if (unit === "year") return utcBucket(year);
|
|
96
|
+
if (unit === "quarter") return utcBucket(year, Math.floor(month / 3) * 3);
|
|
97
|
+
if (unit === "month") return utcBucket(year, month);
|
|
88
98
|
const day = date.getUTCDate();
|
|
89
|
-
if (unit === "day") return
|
|
99
|
+
if (unit === "day") return utcBucket(year, month, day);
|
|
90
100
|
const hour = date.getUTCHours();
|
|
91
|
-
if (unit === "hour") return
|
|
101
|
+
if (unit === "hour") return utcBucket(year, month, day, hour);
|
|
92
102
|
const minute = date.getUTCMinutes();
|
|
93
|
-
if (unit === "minute") return
|
|
103
|
+
if (unit === "minute") return utcBucket(year, month, day, hour, minute);
|
|
94
104
|
return Math.floor(timestamp / 1000) * 1000;
|
|
95
105
|
}
|
|
96
106
|
|
|
@@ -108,7 +118,7 @@ export function deriveTimeUnitRows(rows, transform) {
|
|
|
108
118
|
if (Object.hasOwn(row, transform.as)) {
|
|
109
119
|
throw new Error(`Time-unit output field "${transform.as}" already exists.`);
|
|
110
120
|
}
|
|
111
|
-
const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index);
|
|
121
|
+
const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index, transform.temporalUnit);
|
|
112
122
|
return {
|
|
113
123
|
...row,
|
|
114
124
|
[transform.as]: floorUtcTimeUnit(timestamp, transform.unit)
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
const TRANSFORM_TOPOLOGY = Object.freeze({
|
|
2
|
+
bin: Object.freeze({ facetTopology: "statistical" }),
|
|
2
3
|
bin2d: Object.freeze({ facetTopology: "statistical" }),
|
|
4
|
+
computed: Object.freeze({ facetTopology: "rowPreserving" }),
|
|
3
5
|
boxOutlier: Object.freeze({ facetTopology: "statistical" }),
|
|
4
6
|
boxSummary: Object.freeze({ facetTopology: "statistical" }),
|
|
5
7
|
density: Object.freeze({ facetTopology: "statistical" }),
|
|
8
|
+
ecdf: Object.freeze({ facetTopology: "statistical" }),
|
|
6
9
|
filter: Object.freeze({ facetTopology: "rowPreserving" }),
|
|
10
|
+
fold: Object.freeze({ facetTopology: "statistical" }),
|
|
7
11
|
gradientProfile: Object.freeze({ facetTopology: "statistical" }),
|
|
8
12
|
horizon: Object.freeze({ facetTopology: "statistical" }),
|
|
9
13
|
interval: Object.freeze({ facetTopology: "statistical" }),
|
|
10
14
|
markFilter: Object.freeze({ provenanceTransparent: true }),
|
|
11
15
|
regression: Object.freeze({ facetTopology: "statistical" }),
|
|
16
|
+
summary: Object.freeze({ facetTopology: "statistical" }),
|
|
17
|
+
stack: Object.freeze({ facetTopology: "statistical" }),
|
|
12
18
|
timeUnit: Object.freeze({ facetTopology: "rowPreserving" }),
|
|
13
19
|
window: Object.freeze({ facetTopology: "statistical" })
|
|
14
20
|
});
|