ggaction 0.0.9 → 0.0.11
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 +84 -0
- package/README.md +5 -4
- package/knowledge/action-card.schema.json +1 -0
- package/knowledge/action-cards.json +1 -10941
- package/knowledge/action-cards.schema.json +1 -0
- package/knowledge/extension-authoring.md +248 -0
- package/knowledge/intent-taxonomy.json +1 -183
- package/knowledge/intent-taxonomy.schema.json +1 -0
- package/knowledge/mcp-resources.json +1 -95
- package/knowledge/mcp-resources.schema.json +1 -0
- package/knowledge/task-packet.schema.json +1 -159
- package/knowledge/task-resolver.js +1 -1230
- package/package.json +19 -2
- package/src/actions/boxPlots/edit.js +45 -298
- package/src/actions/boxPlots/options.js +46 -77
- package/src/actions/boxPlots/resolve.js +1 -1
- package/src/actions/charts/bar.js +3 -0
- package/src/actions/charts/heatmap.js +73 -83
- package/src/actions/charts/index.js +1 -1
- package/src/actions/charts/parallel.js +35 -1
- package/src/actions/charts/shared.js +20 -0
- package/src/actions/composition/actions.js +46 -49
- package/src/actions/data/bin2d.js +105 -2
- package/src/actions/data/box.js +17 -17
- package/src/actions/data/density.js +42 -78
- package/src/actions/data/gradientProfile.js +22 -52
- package/src/actions/data/horizon.js +16 -41
- package/src/actions/data/index.js +10 -10
- package/src/actions/data/interval.js +16 -38
- package/src/actions/data/intervalEdit.js +55 -0
- package/src/actions/data/regression.js +7 -17
- package/src/actions/data/shared.js +47 -0
- package/src/actions/data/timeUnit.js +14 -38
- package/src/actions/data/window.js +14 -38
- package/src/actions/distributions/revision.js +154 -34
- package/src/actions/encodings/angle.js +9 -10
- package/src/actions/encodings/appearance.js +17 -51
- package/src/actions/encodings/density/resolve.js +3 -1
- package/src/actions/encodings/density.js +1 -15
- package/src/actions/encodings/horizon/resolve.js +13 -3
- package/src/actions/encodings/horizon.js +8 -7
- package/src/actions/encodings/index.js +9 -9
- package/src/actions/encodings/offset.js +8 -24
- package/src/actions/encodings/parallel.js +9 -10
- package/src/actions/encodings/pathOrder.js +6 -13
- package/src/actions/encodings/position/policies/index.js +4 -4
- package/src/actions/encodings/position/policies/line.js +47 -17
- package/src/actions/encodings/position/resolve.js +24 -5
- package/src/actions/encodings/ranged.js +18 -23
- package/src/actions/encodings/remove.js +15 -25
- package/src/actions/encodings/ruleAppearance.js +9 -24
- package/src/actions/encodings/shared.js +37 -0
- package/src/actions/encodings/strokeDash.js +8 -23
- package/src/actions/encodings/text.js +9 -10
- package/src/actions/errorBands/create.js +105 -39
- package/src/actions/errorBands/edit.js +89 -93
- package/src/actions/errorBands/index.js +1 -2
- package/src/actions/errorBars/create.js +78 -27
- package/src/actions/errorBars/edit.js +81 -48
- package/src/actions/errorBars/index.js +1 -2
- package/src/actions/facets/actions.js +45 -69
- package/src/actions/facets/derive.js +38 -53
- package/src/actions/facets/replay.js +0 -1
- package/src/actions/gradientPlots/components.js +59 -48
- package/src/actions/gradientPlots/edit.js +35 -216
- package/src/actions/gradientPlots/index.js +11 -10
- package/src/actions/gradientPlots/materialize.js +21 -35
- package/src/actions/gradientPlots/options.js +5 -7
- package/src/actions/gradientPlots/paint.js +9 -6
- package/src/actions/gradientPlots/resolve.js +1 -1
- package/src/actions/guides/axes/index.js +3 -3
- package/src/actions/guides/axes/labels.js +68 -26
- package/src/actions/guides/axes/lines.js +22 -2
- package/src/actions/guides/axes/policy.js +28 -8
- package/src/actions/guides/axes/ticks.js +27 -9
- package/src/actions/guides/axes/titles.js +113 -65
- package/src/actions/guides/grids/resolve.js +14 -0
- package/src/actions/guides/guides.js +2 -4
- package/src/actions/guides/index.js +2 -2
- package/src/actions/guides/legends/categorical/actions.js +44 -68
- package/src/actions/guides/legends/categorical/components.js +6 -1
- package/src/actions/guides/legends/categorical/index.js +2 -2
- package/src/actions/guides/legends/categorical/layout.js +148 -175
- package/src/actions/guides/legends/categorical/recipes.js +5 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -1
- package/src/actions/guides/legends/continuous/common.js +117 -66
- package/src/actions/guides/legends/continuous/gradient.js +38 -37
- package/src/actions/guides/legends/continuous/interval.js +97 -76
- package/src/actions/guides/legends/continuous/opacity.js +28 -38
- package/src/actions/guides/legends/edit.js +14 -1
- package/src/actions/guides/legends/index.js +1 -1
- package/src/actions/guides/legends/lane.js +2 -1
- package/src/actions/guides/legends/remove.js +8 -0
- package/src/actions/guides/legends/size.js +36 -43
- package/src/actions/guides/legends/strokeWidth.js +20 -26
- package/src/actions/guides/polar/axes/labels.js +17 -1
- package/src/actions/guides/polar/axes/shared.js +7 -1
- package/src/actions/guides/polar/axes/titles.js +19 -2
- package/src/actions/guides/polar/grids.js +4 -0
- package/src/actions/guides/polar/resolve.js +6 -0
- package/src/actions/index.js +13 -13
- package/src/actions/intervals/resolve.js +87 -117
- package/src/actions/marks/arc/actions.js +31 -36
- package/src/actions/marks/area/actions.js +23 -31
- package/src/actions/marks/index.js +6 -6
- package/src/actions/marks/line/actions.js +33 -52
- package/src/actions/marks/line/materialize.js +7 -5
- package/src/actions/marks/point/materialize.js +5 -3
- package/src/actions/marks/remove.js +21 -0
- package/src/actions/marks/rule/actions.js +53 -53
- package/src/actions/marks/shared.js +8 -4
- package/src/actions/marks/text/actions.js +14 -6
- package/src/actions/marks/text/layout.js +21 -45
- package/src/actions/marks/tick/actions.js +22 -21
- package/src/actions/primitives/createGraphics.js +4 -0
- package/src/actions/primitives/editGraphics.js +4 -0
- package/src/actions/primitives/index.js +3 -3
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +2 -2
- package/src/actions/primitives/semanticValidation/layer.js +1 -1
- package/src/actions/regression/edit.js +3 -43
- package/src/actions/scales/consumers/families.js +24 -4
- package/src/actions/scales/consumers/index.js +1 -1
- package/src/actions/scales/consumers/seriesLayout.js +17 -0
- package/src/actions/scales/definitions.js +100 -103
- package/src/actions/scales/materialize.js +5 -0
- package/src/actions/selection/actions.js +79 -121
- package/src/actions/titles/actions.js +48 -94
- package/src/actions/titles/resolve.js +14 -27
- package/src/actions/violinPlots/create.js +37 -44
- package/src/core/action.js +20 -5
- package/src/core/extensionRegistry.js +104 -0
- package/src/core/textMetrics.js +74 -37
- package/src/core/validation.js +18 -0
- package/src/extension.js +8 -1
- package/src/grammar/aggregate.js +48 -28
- package/src/grammar/arcs.js +58 -11
- package/src/grammar/areaSeries.js +14 -16
- package/src/grammar/bin2d.js +37 -23
- package/src/grammar/boxPlot.js +14 -13
- package/src/grammar/categoricalDensity.js +14 -14
- package/src/grammar/categoryOrder.js +8 -14
- package/src/grammar/curveCommands.js +87 -20
- package/src/grammar/density.js +162 -141
- package/src/grammar/facets/index.js +14 -0
- package/src/grammar/facets/scales.js +7 -4
- package/src/grammar/gradientProfile.js +12 -15
- package/src/grammar/histogram.js +145 -83
- package/src/grammar/horizon.js +60 -23
- package/src/grammar/interval.js +14 -13
- package/src/grammar/lineSeries.js +91 -61
- package/src/grammar/numeric.js +290 -0
- package/src/grammar/paint.js +3 -4
- package/src/grammar/palettes.js +21 -32
- package/src/grammar/pathCommands.js +6 -0
- package/src/grammar/polarPaths.js +5 -5
- package/src/grammar/regression/derive.js +48 -9
- package/src/grammar/regression/models.js +248 -69
- package/src/grammar/regression/parameters.js +8 -0
- package/src/grammar/scales/color.js +35 -26
- package/src/grammar/scales/continuous.js +71 -24
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/discretized.js +42 -12
- package/src/grammar/scales/fields.js +21 -4
- package/src/grammar/scales/ordinal.js +117 -61
- package/src/grammar/scales/temporal.js +40 -11
- package/src/grammar/scales/transformed.js +199 -52
- package/src/grammar/schemas/graphicBounds.js +270 -79
- package/src/grammar/seriesLayout.js +50 -18
- package/src/grammar/ticks.js +123 -53
- package/src/grammar/timeUnit.js +18 -8
- package/src/grammar/transforms.js +4 -1
- package/src/grammar/window.js +70 -57
- package/src/index.js +1 -1
- package/src/layout/canvas.js +4 -8
- package/src/layout/composition.js +39 -40
- package/src/layout/facets.js +10 -14
- package/src/layout/labels.js +91 -15
- package/src/layout/legend.js +22 -20
- package/src/layout/legendLane.js +54 -40
- package/src/layout/title.js +7 -7
- package/src/materialization/bars/aggregate.js +14 -40
- package/src/materialization/bars/grouped.js +7 -18
- package/src/materialization/bars/histogram.js +2 -19
- package/src/materialization/bars/ranged.js +20 -15
- package/src/materialization/bars/resolve.js +21 -0
- package/src/materialization/facetGuides/legacyCategorical.js +2 -1
- package/src/materialization/facetGuides/placement.js +34 -19
- package/src/materialization/facetGuides/preparation.js +9 -5
- package/src/materialization/facets.js +39 -27
- package/src/materialization/guides/resources.js +2 -1
- package/src/materialization/marks/capabilities.js +5 -2
- package/src/materialization/marks/index.js +24 -4
- package/src/materialization/scales/policies/bar.js +61 -59
- package/src/materialization/scales/policies/binnedPosition.js +34 -0
- package/src/materialization/scales/policies/series.js +13 -7
- package/src/materialization/scales/resolve.js +4 -5
- package/src/materialization/selection/items/arc.js +16 -2
- package/src/materialization/selection/items/bar.js +4 -2
- package/src/materialization/selection/items/common.js +21 -1
- package/src/materialization/selection/items/path.js +19 -2
- package/src/materialization/selection/items/point.js +4 -16
- package/src/materialization/selection/items/tick.js +13 -21
- package/src/materialization/selection/point.js +11 -4
- package/src/materialization/selection/styles.js +19 -27
- package/src/materialization/text.js +4 -1
- package/src/mcp/adapter.js +8 -3
- package/src/renderers/canvas/fill.js +2 -2
- package/src/renderers/canvas/index.js +35 -2
- package/src/renderers/canvas/native.js +160 -0
- package/src/renderers/pdf.js +14 -0
- package/src/renderers/svg.js +37 -4
- package/types/extension.d.ts +19 -0
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +509 -131
- package/types/svg.d.ts +1 -0
- package/src/actions/errorBands/components.js +0 -81
- package/src/actions/errorBands/options.js +0 -39
- package/src/actions/errorBands/resolve.js +0 -28
- package/src/actions/errorBars/components.js +0 -62
- package/src/actions/errorBars/options.js +0 -58
- package/src/actions/errorBars/resolve.js +0 -25
package/src/grammar/ticks.js
CHANGED
|
@@ -1,23 +1,77 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
MAX_GENERATED_ITEMS,
|
|
4
|
+
validateGeneratedItemLimit
|
|
5
|
+
} from "../core/validation.js";
|
|
6
|
+
import {
|
|
7
|
+
alignNumericStep,
|
|
8
|
+
cleanNumericValue,
|
|
9
|
+
niceNumericStep,
|
|
10
|
+
normalizeNumericRange
|
|
11
|
+
} from "./numeric.js";
|
|
12
|
+
import { utcTimestamp, validTimestamp } from "./timeUnit.js";
|
|
2
13
|
|
|
3
|
-
|
|
14
|
+
function ticksForStep(start, end, step, low, high, scale) {
|
|
15
|
+
const length = Math.floor((end - start) / step + 1e-10) + 1;
|
|
16
|
+
if (!Number.isFinite(length) || length <= 0) return undefined;
|
|
17
|
+
const size = Math.min(length, MAX_GENERATED_ITEMS);
|
|
18
|
+
const values = [];
|
|
19
|
+
for (let index = 0; index < size; index += 1) {
|
|
20
|
+
const tick = length === size || size === 1
|
|
21
|
+
? index
|
|
22
|
+
: Math.round(index * (length - 1) / (size - 1));
|
|
23
|
+
const raw = start + tick * step;
|
|
24
|
+
const value = scale === undefined
|
|
25
|
+
? Number(raw.toPrecision(12))
|
|
26
|
+
: cleanNumericValue(raw * scale, step * scale);
|
|
27
|
+
if (
|
|
28
|
+
!Number.isFinite(value) || value < low || value > high ||
|
|
29
|
+
(values.length > 0 && value <= values.at(-1))
|
|
30
|
+
) {
|
|
31
|
+
if (scale === undefined) return undefined;
|
|
32
|
+
} else {
|
|
33
|
+
values.push(value);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return values;
|
|
37
|
+
}
|
|
4
38
|
|
|
5
39
|
export function niceTicks(domain, count) {
|
|
6
40
|
if (!Number.isInteger(count) || count <= 0) throw new RangeError("Tick count must be a positive integer.");
|
|
41
|
+
const effectiveCount = Math.min(count, MAX_GENERATED_ITEMS);
|
|
7
42
|
const low = Math.min(...domain);
|
|
8
43
|
const high = Math.max(...domain);
|
|
9
44
|
if (low === high) return cloneAndFreeze([low]);
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
45
|
+
const directSpan = high - low;
|
|
46
|
+
if (Number.isFinite(directSpan) && directSpan > 0) {
|
|
47
|
+
const directStep = niceNumericStep(directSpan, effectiveCount);
|
|
48
|
+
const tolerance = directStep * 1e-10;
|
|
49
|
+
const directStart = Math.ceil((low - tolerance) / directStep) * directStep;
|
|
50
|
+
const directEnd = Math.floor((high + tolerance) / directStep) * directStep;
|
|
51
|
+
const direct = ticksForStep(
|
|
52
|
+
directStart,
|
|
53
|
+
directEnd,
|
|
54
|
+
directStep,
|
|
55
|
+
low,
|
|
56
|
+
high
|
|
57
|
+
);
|
|
58
|
+
if (direct !== undefined && direct.length <= MAX_GENERATED_ITEMS) {
|
|
59
|
+
return cloneAndFreeze(direct);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const normalized = normalizeNumericRange(low, high);
|
|
63
|
+
const normalizedStep = niceNumericStep(normalized.span, effectiveCount);
|
|
64
|
+
const start = alignNumericStep(normalized.start, normalizedStep, "ceil");
|
|
65
|
+
const end = alignNumericStep(normalized.end, normalizedStep, "floor");
|
|
66
|
+
const values = ticksForStep(
|
|
67
|
+
start,
|
|
68
|
+
end,
|
|
69
|
+
normalizedStep,
|
|
70
|
+
low,
|
|
71
|
+
high,
|
|
72
|
+
normalized.scale
|
|
73
|
+
);
|
|
74
|
+
return cloneAndFreeze(values?.length > 0 ? values : [low, high]);
|
|
21
75
|
}
|
|
22
76
|
|
|
23
77
|
const SECOND = 1000;
|
|
@@ -39,19 +93,31 @@ function validateTimeDomain(domain) {
|
|
|
39
93
|
if (
|
|
40
94
|
!Array.isArray(domain) ||
|
|
41
95
|
domain.length !== 2 ||
|
|
42
|
-
!domain.every(
|
|
96
|
+
!domain.every(validTimestamp)
|
|
43
97
|
) {
|
|
44
|
-
throw new TypeError(
|
|
98
|
+
throw new TypeError(
|
|
99
|
+
"Time tick domain must contain two finite timestamps representing valid dates."
|
|
100
|
+
);
|
|
45
101
|
}
|
|
46
102
|
|
|
47
103
|
return [Math.min(...domain), Math.max(...domain)];
|
|
48
104
|
}
|
|
49
105
|
|
|
50
106
|
function selectTimeInterval(span, count) {
|
|
51
|
-
|
|
107
|
+
let interval = TIME_INTERVALS[0];
|
|
108
|
+
let bestError = Infinity;
|
|
109
|
+
for (const candidate of TIME_INTERVALS) {
|
|
52
110
|
const error = Math.abs(span / candidate.duration - count);
|
|
53
|
-
|
|
54
|
-
|
|
111
|
+
if (error < bestError) {
|
|
112
|
+
interval = candidate;
|
|
113
|
+
bestError = error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (span / interval.duration + 1 > Math.max(count * 10, 100)) {
|
|
117
|
+
const step = niceNumericStep(span / YEAR, count);
|
|
118
|
+
return { unit: "year", step, duration: step * YEAR };
|
|
119
|
+
}
|
|
120
|
+
return interval;
|
|
55
121
|
}
|
|
56
122
|
|
|
57
123
|
function floorCalendar(timestamp, { unit, step }) {
|
|
@@ -59,13 +125,13 @@ function floorCalendar(timestamp, { unit, step }) {
|
|
|
59
125
|
|
|
60
126
|
if (unit === "year") {
|
|
61
127
|
const year = Math.floor(date.getUTCFullYear() / step) * step;
|
|
62
|
-
return
|
|
128
|
+
return utcTimestamp(year);
|
|
63
129
|
}
|
|
64
130
|
|
|
65
131
|
if (unit === "month") {
|
|
66
132
|
const month = date.getUTCFullYear() * 12 + date.getUTCMonth();
|
|
67
133
|
const aligned = Math.floor(month / step) * step;
|
|
68
|
-
return
|
|
134
|
+
return utcTimestamp(Math.floor(aligned / 12), aligned % 12);
|
|
69
135
|
}
|
|
70
136
|
|
|
71
137
|
const duration = unit === "day"
|
|
@@ -79,43 +145,44 @@ function floorCalendar(timestamp, { unit, step }) {
|
|
|
79
145
|
return Math.floor(timestamp / duration) * duration;
|
|
80
146
|
}
|
|
81
147
|
|
|
82
|
-
function addCalendar(timestamp,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
148
|
+
function addCalendar(timestamp, interval) {
|
|
149
|
+
if (interval.unit === "year" || interval.unit === "month") {
|
|
150
|
+
const date = new Date(timestamp);
|
|
151
|
+
return utcTimestamp(
|
|
152
|
+
date.getUTCFullYear() + (interval.unit === "year" ? interval.step : 0),
|
|
153
|
+
date.getUTCMonth() + (interval.unit === "month" ? interval.step : 0)
|
|
154
|
+
);
|
|
87
155
|
}
|
|
88
|
-
|
|
89
|
-
if (unit === "month") {
|
|
90
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + step, 1);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const duration = unit === "day"
|
|
94
|
-
? step * DAY
|
|
95
|
-
: unit === "hour"
|
|
96
|
-
? step * HOUR
|
|
97
|
-
: unit === "minute"
|
|
98
|
-
? step * MINUTE
|
|
99
|
-
: step * SECOND;
|
|
100
|
-
|
|
101
|
-
return timestamp + duration;
|
|
156
|
+
return timestamp + interval.duration;
|
|
102
157
|
}
|
|
103
158
|
|
|
104
159
|
export function timeTicks(domain, count) {
|
|
105
160
|
if (!Number.isInteger(count) || count <= 0) {
|
|
106
161
|
throw new RangeError("Time tick count must be a positive integer.");
|
|
107
162
|
}
|
|
163
|
+
validateGeneratedItemLimit(count, "Time tick count");
|
|
108
164
|
|
|
109
165
|
const [low, high] = validateTimeDomain(domain);
|
|
110
166
|
if (low === high) return cloneAndFreeze([low]);
|
|
111
167
|
const interval = selectTimeInterval(high - low, count);
|
|
112
168
|
let value = floorCalendar(low, interval);
|
|
169
|
+
if (!validTimestamp(value) && interval.unit === "year") {
|
|
170
|
+
const year = new Date(low).getUTCFullYear();
|
|
171
|
+
value = utcTimestamp(Math.ceil(year / interval.step) * interval.step);
|
|
172
|
+
}
|
|
113
173
|
if (value < low) value = addCalendar(value, interval);
|
|
114
174
|
const values = [];
|
|
115
175
|
|
|
116
176
|
while (value <= high) {
|
|
177
|
+
if (values.length === MAX_GENERATED_ITEMS) {
|
|
178
|
+
throw new RangeError(
|
|
179
|
+
`Time tick range must generate at most ${MAX_GENERATED_ITEMS} ticks.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
117
182
|
values.push(value);
|
|
118
|
-
|
|
183
|
+
const next = addCalendar(value, interval);
|
|
184
|
+
if (!validTimestamp(next) || next <= value) break;
|
|
185
|
+
value = next;
|
|
119
186
|
}
|
|
120
187
|
|
|
121
188
|
return cloneAndFreeze(values.length === 0 ? [low, high] : values);
|
|
@@ -125,9 +192,18 @@ function pad(value) {
|
|
|
125
192
|
return String(value).padStart(2, "0");
|
|
126
193
|
}
|
|
127
194
|
|
|
195
|
+
function formatYear(value) {
|
|
196
|
+
if (value >= 0 && value <= 9999) return String(value).padStart(4, "0");
|
|
197
|
+
const sign = value < 0 ? "-" : "+";
|
|
198
|
+
return `${sign}${String(Math.abs(value)).padStart(6, "0")}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
128
201
|
function formatTimePrecision(value, precision) {
|
|
129
202
|
const date = new Date(value);
|
|
130
|
-
|
|
203
|
+
if (!Number.isFinite(date.getTime())) {
|
|
204
|
+
throw new TypeError("Time tick value must represent a valid date.");
|
|
205
|
+
}
|
|
206
|
+
const year = formatYear(date.getUTCFullYear());
|
|
131
207
|
const month = pad(date.getUTCMonth() + 1);
|
|
132
208
|
const day = pad(date.getUTCDate());
|
|
133
209
|
const hour = pad(date.getUTCHours());
|
|
@@ -157,22 +233,14 @@ function timePrecision(domain) {
|
|
|
157
233
|
}
|
|
158
234
|
|
|
159
235
|
export function formatTimeTick(value, domain) {
|
|
160
|
-
if (!
|
|
161
|
-
throw new TypeError(
|
|
236
|
+
if (!validTimestamp(value)) {
|
|
237
|
+
throw new TypeError(
|
|
238
|
+
"Time tick value must be a finite timestamp representing a valid date."
|
|
239
|
+
);
|
|
162
240
|
}
|
|
163
241
|
return formatTimePrecision(value, timePrecision(domain));
|
|
164
242
|
}
|
|
165
243
|
|
|
166
|
-
function distinguishesTicks(values, labels) {
|
|
167
|
-
const timestamps = new Map();
|
|
168
|
-
for (let index = 0; index < values.length; index += 1) {
|
|
169
|
-
const previous = timestamps.get(labels[index]);
|
|
170
|
-
if (previous !== undefined && previous !== values[index]) return false;
|
|
171
|
-
timestamps.set(labels[index], values[index]);
|
|
172
|
-
}
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
244
|
export function formatTimeTicks(values, domain) {
|
|
177
245
|
if (!Array.isArray(values) || !values.every(Number.isFinite)) {
|
|
178
246
|
throw new TypeError("Time tick values must be finite timestamps.");
|
|
@@ -180,7 +248,9 @@ export function formatTimeTicks(values, domain) {
|
|
|
180
248
|
const initial = timePrecision(domain);
|
|
181
249
|
for (let precision = initial; precision <= 6; precision += 1) {
|
|
182
250
|
const labels = values.map(value => formatTimePrecision(value, precision));
|
|
183
|
-
if (
|
|
251
|
+
if (new Set(labels).size === new Set(values).size) {
|
|
252
|
+
return cloneAndFreeze(labels);
|
|
253
|
+
}
|
|
184
254
|
}
|
|
185
255
|
return cloneAndFreeze(values.map(value => String(value)));
|
|
186
256
|
}
|
package/src/grammar/timeUnit.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
3
3
|
|
|
4
|
+
export function utcTimestamp(
|
|
5
|
+
year,
|
|
6
|
+
month = 0,
|
|
7
|
+
day = 1,
|
|
8
|
+
hour = 0,
|
|
9
|
+
minute = 0
|
|
10
|
+
) {
|
|
11
|
+
const date = new Date(0);
|
|
12
|
+
date.setUTCFullYear(year, month, day);
|
|
13
|
+
date.setUTCHours(hour, minute, 0, 0);
|
|
14
|
+
return date.getTime();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validTimestamp(value) {
|
|
18
|
+
return Number.isFinite(value) && Number.isFinite(new Date(value).getTime());
|
|
19
|
+
}
|
|
20
|
+
|
|
4
21
|
export const TIME_UNITS = cloneAndFreeze([
|
|
5
22
|
"year",
|
|
6
23
|
"quarter",
|
|
@@ -52,13 +69,6 @@ export function normalizeTimeUnitTransform({ field, unit, as } = {}) {
|
|
|
52
69
|
return cloneAndFreeze(transform);
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
function utcTimestamp(year, month = 0, day = 1, hour = 0, minute = 0) {
|
|
56
|
-
const date = new Date(0);
|
|
57
|
-
date.setUTCFullYear(year, month, day);
|
|
58
|
-
date.setUTCHours(hour, minute, 0, 0);
|
|
59
|
-
return date.getTime();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
72
|
export function floorUtcTimeUnit(timestamp, unit) {
|
|
63
73
|
if (!Number.isFinite(timestamp)) {
|
|
64
74
|
throw new TypeError("Time-unit timestamp must be finite.");
|
|
@@ -67,7 +77,7 @@ export function floorUtcTimeUnit(timestamp, unit) {
|
|
|
67
77
|
throw new Error(`Unsupported time unit "${unit}".`);
|
|
68
78
|
}
|
|
69
79
|
const date = new Date(timestamp);
|
|
70
|
-
if (!
|
|
80
|
+
if (!validTimestamp(timestamp)) {
|
|
71
81
|
throw new TypeError("Time-unit timestamp must represent a valid date.");
|
|
72
82
|
}
|
|
73
83
|
const year = date.getUTCFullYear();
|
|
@@ -32,7 +32,10 @@ function facetHorizonTransform(transform, { scales = {} } = {}) {
|
|
|
32
32
|
(scales.y ?? "shared") !== "shared" ||
|
|
33
33
|
!Array.isArray(transform.resolved?.extents)
|
|
34
34
|
) return transform;
|
|
35
|
-
|
|
35
|
+
let extent = -Infinity;
|
|
36
|
+
for (const item of transform.resolved.extents) {
|
|
37
|
+
extent = Math.max(extent, item.extent);
|
|
38
|
+
}
|
|
36
39
|
if (!(extent > 0)) return transform;
|
|
37
40
|
return {
|
|
38
41
|
...requestedHorizonTransform(transform),
|
package/src/grammar/window.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
stableFiniteMean,
|
|
4
|
+
stableFinitePrefixSums,
|
|
5
|
+
stableFiniteSum
|
|
6
|
+
} from "./numeric.js";
|
|
2
7
|
|
|
3
|
-
const TRANSFORM_KEYS =
|
|
8
|
+
const TRANSFORM_KEYS = [
|
|
4
9
|
"type", "partitionBy", "sortBy", "operations"
|
|
5
|
-
]
|
|
6
|
-
const SORT_KEYS =
|
|
7
|
-
const ORDER_VALUES =
|
|
8
|
-
const OPERATION_VALUES =
|
|
10
|
+
];
|
|
11
|
+
const SORT_KEYS = ["field", "order"];
|
|
12
|
+
const ORDER_VALUES = ["ascending", "descending"];
|
|
13
|
+
const OPERATION_VALUES = [
|
|
9
14
|
"rowNumber", "rank", "denseRank", "cumulativeSum", "lag", "lead",
|
|
10
15
|
"movingMean", "movingSum"
|
|
11
|
-
]
|
|
12
|
-
const POSITION_OPERATIONS = new Set(
|
|
13
|
-
const OFFSET_OPERATIONS = new Set(
|
|
14
|
-
const MOVING_OPERATIONS = new Set(
|
|
15
|
-
const FRAME_KEYS =
|
|
16
|
+
];
|
|
17
|
+
const POSITION_OPERATIONS = new Set(OPERATION_VALUES.slice(0, 3));
|
|
18
|
+
const OFFSET_OPERATIONS = new Set(OPERATION_VALUES.slice(4, 6));
|
|
19
|
+
const MOVING_OPERATIONS = new Set(OPERATION_VALUES.slice(-2));
|
|
20
|
+
const FRAME_KEYS = ["preceding", "following"];
|
|
16
21
|
|
|
17
22
|
function requireField(value, label) {
|
|
18
23
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -77,15 +82,12 @@ function validateFrame(frame, operation) {
|
|
|
77
82
|
throw new TypeError(`Window ${operation} frame must be a plain object.`);
|
|
78
83
|
}
|
|
79
84
|
rejectUnknownKeys(frame, FRAME_KEYS, `window ${operation} frame`);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
throw new RangeError(
|
|
87
|
-
`Window ${operation} frame following must be a non-negative integer.`
|
|
88
|
-
);
|
|
85
|
+
for (const field of FRAME_KEYS) {
|
|
86
|
+
if (!Number.isInteger(frame[field]) || frame[field] < 0) {
|
|
87
|
+
throw new RangeError(
|
|
88
|
+
`Window ${operation} frame ${field} must be a non-negative integer.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -264,12 +266,6 @@ function validateSourceFields(rows, transform) {
|
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
function validatePartitionSortValues(partition, transform) {
|
|
267
|
-
for (const field of transform.partitionBy) {
|
|
268
|
-
partition.forEach(entry => scalarKey(
|
|
269
|
-
entry.row[field],
|
|
270
|
-
`Window partition field "${field}"`
|
|
271
|
-
));
|
|
272
|
-
}
|
|
273
269
|
for (const sort of transform.sortBy) {
|
|
274
270
|
const types = new Set(partition.flatMap(({ row }) => {
|
|
275
271
|
const value = row[sort.field];
|
|
@@ -301,12 +297,45 @@ function partitionRows(entries, partitionBy) {
|
|
|
301
297
|
entry.row[field],
|
|
302
298
|
`Window partition field "${field}"`
|
|
303
299
|
)).join("\0");
|
|
304
|
-
|
|
305
|
-
|
|
300
|
+
let partition = partitions.get(key);
|
|
301
|
+
if (partition === undefined) {
|
|
302
|
+
partition = [];
|
|
303
|
+
partitions.set(key, partition);
|
|
304
|
+
}
|
|
305
|
+
partition.push(entry);
|
|
306
306
|
}
|
|
307
307
|
return [...partitions.values()];
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
function operationValues(
|
|
311
|
+
partition,
|
|
312
|
+
operation,
|
|
313
|
+
first = 0,
|
|
314
|
+
last = partition.length - 1
|
|
315
|
+
) {
|
|
316
|
+
const values = [];
|
|
317
|
+
for (let index = first; index <= last; index += 1) {
|
|
318
|
+
const value = partition[index].row[operation.field];
|
|
319
|
+
if (!Number.isFinite(value)) {
|
|
320
|
+
throw new TypeError(
|
|
321
|
+
`Window ${operation.op} field "${operation.field}" must contain finite numbers.`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
values.push(value);
|
|
325
|
+
}
|
|
326
|
+
return values;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function stableWindowOutput(operation, calculate) {
|
|
330
|
+
const label = `Window ${operation.op} output "${operation.as}"`;
|
|
331
|
+
try {
|
|
332
|
+
return calculate(label);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (!(error instanceof RangeError)) throw error;
|
|
335
|
+
throw new RangeError(`${label} must be finite.`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
310
339
|
function applyOperation(partition, operation, sortBy) {
|
|
311
340
|
if (operation.op === "rowNumber") {
|
|
312
341
|
partition.forEach((entry, index) => {
|
|
@@ -330,44 +359,29 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
330
359
|
return;
|
|
331
360
|
}
|
|
332
361
|
if (operation.op === "cumulativeSum") {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
total += value;
|
|
342
|
-
entry.row[operation.as] = total;
|
|
343
|
-
}
|
|
362
|
+
const values = operationValues(partition, operation);
|
|
363
|
+
const totals = stableWindowOutput(operation, label =>
|
|
364
|
+
stableFinitePrefixSums(values, label)
|
|
365
|
+
);
|
|
366
|
+
partition.forEach((entry, index) => {
|
|
367
|
+
entry.row[operation.as] = totals[index];
|
|
368
|
+
});
|
|
344
369
|
return;
|
|
345
370
|
}
|
|
346
371
|
if (MOVING_OPERATIONS.has(operation.op)) {
|
|
372
|
+
const calculate = operation.op === "movingMean"
|
|
373
|
+
? stableFiniteMean
|
|
374
|
+
: stableFiniteSum;
|
|
347
375
|
partition.forEach((entry, index) => {
|
|
348
376
|
const first = Math.max(0, index - operation.frame.preceding);
|
|
349
377
|
const last = Math.min(
|
|
350
378
|
partition.length - 1,
|
|
351
379
|
index + operation.frame.following
|
|
352
380
|
);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
throw new TypeError(
|
|
358
|
-
`Window ${operation.op} field "${operation.field}" must contain finite numbers.`
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
total += value;
|
|
362
|
-
if (!Number.isFinite(total)) {
|
|
363
|
-
throw new RangeError(
|
|
364
|
-
`Window ${operation.op} output "${operation.as}" must be finite.`
|
|
365
|
-
);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
entry.row[operation.as] = operation.op === "movingMean"
|
|
369
|
-
? total / (last - first + 1)
|
|
370
|
-
: total;
|
|
381
|
+
const values = operationValues(partition, operation, first, last);
|
|
382
|
+
entry.row[operation.as] = stableWindowOutput(operation, label =>
|
|
383
|
+
calculate(values, label)
|
|
384
|
+
);
|
|
371
385
|
});
|
|
372
386
|
return;
|
|
373
387
|
}
|
|
@@ -400,6 +414,5 @@ export function deriveWindowRows(rows, transform) {
|
|
|
400
414
|
applyOperation(partition, operation, transform.sortBy);
|
|
401
415
|
}
|
|
402
416
|
}
|
|
403
|
-
entries.sort((left, right) => left.index - right.index);
|
|
404
417
|
return cloneAndFreeze(entries.map(entry => entry.row));
|
|
405
418
|
}
|
package/src/index.js
CHANGED
package/src/layout/canvas.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import {
|
|
3
3
|
validateNonEmptyString,
|
|
4
|
-
validateNonNegativeFinite
|
|
4
|
+
validateNonNegativeFinite,
|
|
5
|
+
validatePositiveFinite
|
|
5
6
|
} from "../core/validation.js";
|
|
6
7
|
|
|
7
8
|
const MARGIN_KEYS = Object.freeze(["top", "right", "bottom", "left"]);
|
|
@@ -63,13 +64,8 @@ export function normalizeMargin(margin, base = DEFAULT_MARGIN) {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export function validateCanvasState({ width, height, background, margin }) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!Number.isInteger(height) || height <= 0) {
|
|
71
|
-
throw new RangeError("Canvas height must be a positive integer.");
|
|
72
|
-
}
|
|
67
|
+
validatePositiveFinite(width, "Canvas width");
|
|
68
|
+
validatePositiveFinite(height, "Canvas height");
|
|
73
69
|
|
|
74
70
|
validateNonEmptyString(background, "Canvas background");
|
|
75
71
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
|
|
3
|
-
const DIRECTIONS =
|
|
4
|
-
const ALIGNMENTS =
|
|
5
|
-
const SIZE_MODES =
|
|
6
|
-
const PADDING_KEYS =
|
|
3
|
+
const DIRECTIONS = ["horizontal", "vertical"];
|
|
4
|
+
const ALIGNMENTS = ["start", "center", "end"];
|
|
5
|
+
const SIZE_MODES = ["auto", "explicit"];
|
|
6
|
+
const PADDING_KEYS = ["top", "right", "bottom", "left"];
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_COMPOSITION_LAYOUT = cloneAndFreeze({
|
|
9
9
|
gap: 16,
|
|
@@ -124,21 +124,16 @@ export function normalizeCompositionChildren(children) {
|
|
|
124
124
|
function normalizeAutoCrossSize(children, horizontal) {
|
|
125
125
|
const dimension = horizontal ? "height" : "width";
|
|
126
126
|
const mode = `${dimension}Mode`;
|
|
127
|
-
const sharedSize =
|
|
127
|
+
const sharedSize = children.reduce(
|
|
128
|
+
(maximum, child) => Math.max(maximum, child[dimension]),
|
|
129
|
+
-Infinity
|
|
130
|
+
);
|
|
128
131
|
return children.map(child => cloneAndFreeze({
|
|
129
132
|
...child,
|
|
130
133
|
[dimension]: child[mode] === "auto" ? sharedSize : child[dimension]
|
|
131
134
|
}));
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
function placementSize(child) {
|
|
135
|
-
return {
|
|
136
|
-
id: child.id,
|
|
137
|
-
width: child.width,
|
|
138
|
-
height: child.height
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
137
|
function crossOffset(remaining, align) {
|
|
143
138
|
if (align === "start") return 0;
|
|
144
139
|
if (align === "end") return remaining;
|
|
@@ -162,30 +157,36 @@ export function resolveCompositionLayout({
|
|
|
162
157
|
const contentWidth = horizontal
|
|
163
158
|
? sizedChildren.reduce((sum, child) => sum + child.width, 0) +
|
|
164
159
|
resolvedGap * (sizedChildren.length - 1)
|
|
165
|
-
:
|
|
160
|
+
: sizedChildren.reduce(
|
|
161
|
+
(maximum, child) => Math.max(maximum, child.width),
|
|
162
|
+
-Infinity
|
|
163
|
+
);
|
|
166
164
|
const contentHeight = horizontal
|
|
167
|
-
?
|
|
165
|
+
? sizedChildren.reduce(
|
|
166
|
+
(maximum, child) => Math.max(maximum, child.height),
|
|
167
|
+
-Infinity
|
|
168
|
+
)
|
|
168
169
|
: sizedChildren.reduce((sum, child) => sum + child.height, 0) +
|
|
169
170
|
resolvedGap * (sizedChildren.length - 1);
|
|
170
171
|
let cursor = horizontal ? resolvedPadding.left : resolvedPadding.top;
|
|
171
172
|
const placements = sizedChildren.map(child => {
|
|
172
|
-
const placement =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
: {
|
|
182
|
-
...placementSize(child),
|
|
183
|
-
x: resolvedPadding.left + crossOffset(
|
|
173
|
+
const placement = {
|
|
174
|
+
id: child.id,
|
|
175
|
+
width: child.width,
|
|
176
|
+
height: child.height,
|
|
177
|
+
x: horizontal
|
|
178
|
+
? cursor
|
|
179
|
+
: resolvedPadding.left + crossOffset(
|
|
184
180
|
contentWidth - child.width,
|
|
185
181
|
resolvedAlign
|
|
186
182
|
),
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
y: horizontal
|
|
184
|
+
? resolvedPadding.top + crossOffset(
|
|
185
|
+
contentHeight - child.height,
|
|
186
|
+
resolvedAlign
|
|
187
|
+
)
|
|
188
|
+
: cursor
|
|
189
|
+
};
|
|
189
190
|
cursor += (horizontal ? child.width : child.height) + resolvedGap;
|
|
190
191
|
return placement;
|
|
191
192
|
});
|
|
@@ -264,7 +265,11 @@ export function resolvePlacedPlotBounds({ placements, plots } = {}) {
|
|
|
264
265
|
}
|
|
265
266
|
plotById.set(plot.id, validatePlacedBounds(plot, `Placed plot "${plot.id}"`));
|
|
266
267
|
}
|
|
267
|
-
|
|
268
|
+
let left = Infinity;
|
|
269
|
+
let top = Infinity;
|
|
270
|
+
let right = -Infinity;
|
|
271
|
+
let bottom = -Infinity;
|
|
272
|
+
placements.forEach((placement, index) => {
|
|
268
273
|
if (
|
|
269
274
|
!isPlainObject(placement) ||
|
|
270
275
|
typeof placement.id !== "string" ||
|
|
@@ -287,20 +292,14 @@ export function resolvePlacedPlotBounds({ placements, plots } = {}) {
|
|
|
287
292
|
);
|
|
288
293
|
}
|
|
289
294
|
plotById.delete(placement.id);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
bottom: placement.y + plot.y + plot.height
|
|
295
|
-
};
|
|
295
|
+
left = Math.min(left, placement.x + plot.x);
|
|
296
|
+
top = Math.min(top, placement.y + plot.y);
|
|
297
|
+
right = Math.max(right, placement.x + plot.x + plot.width);
|
|
298
|
+
bottom = Math.max(bottom, placement.y + plot.y + plot.height);
|
|
296
299
|
});
|
|
297
300
|
if (plotById.size > 0) {
|
|
298
301
|
throw new Error(`Unknown placed plot id "${plotById.keys().next().value}".`);
|
|
299
302
|
}
|
|
300
|
-
const left = Math.min(...translated.map(bounds => bounds.left));
|
|
301
|
-
const top = Math.min(...translated.map(bounds => bounds.top));
|
|
302
|
-
const right = Math.max(...translated.map(bounds => bounds.right));
|
|
303
|
-
const bottom = Math.max(...translated.map(bounds => bounds.bottom));
|
|
304
303
|
return cloneAndFreeze({
|
|
305
304
|
x: left,
|
|
306
305
|
y: top,
|