ggaction 0.0.15 → 0.0.17
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 +88 -0
- package/README.md +23 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-packet.schema.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +40 -7
- package/src/BasicChartProgram.js +3 -1
- package/src/ChartProgram.js +2 -0
- package/src/accessibility.js +157 -0
- package/src/actions/boxPlots/components.js +6 -8
- package/src/actions/boxPlots/create.js +4 -5
- package/src/actions/boxPlots/edit.js +3 -4
- package/src/actions/boxPlots/materialize.js +1 -1
- package/src/actions/canvas/actions.js +3 -3
- package/src/actions/canvas/fitting.js +1 -1
- package/src/actions/categoryOrder/index.js +6 -8
- package/src/actions/charts/area.js +1 -1
- package/src/actions/charts/bar.js +1 -1
- package/src/actions/charts/beeswarm.js +1 -1
- package/src/actions/charts/density.js +1 -1
- package/src/actions/charts/ecdf.js +2 -2
- package/src/actions/charts/endpoints.js +4 -4
- package/src/actions/charts/heatmap.js +1 -1
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +1 -1
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +1 -1
- package/src/actions/charts/polar.js +2 -2
- package/src/actions/charts/radar.js +1 -1
- package/src/actions/charts/radial.js +2 -2
- package/src/actions/charts/raincloud.js +2 -2
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/scatter.js +1 -1
- package/src/actions/composition/actions.js +20 -24
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +1 -1
- package/src/actions/data/basicBin2d.js +4 -5
- package/src/actions/data/bin.js +3 -3
- package/src/actions/data/bin2d.js +6 -8
- package/src/actions/data/bin2dMaterialize.js +4 -5
- package/src/actions/data/box.js +4 -4
- package/src/actions/data/complete.js +2 -2
- package/src/actions/data/computed.js +2 -2
- package/src/actions/data/create.js +23 -17
- package/src/actions/data/density.js +4 -4
- package/src/actions/data/derived.js +25 -15
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +54 -29
- package/src/actions/data/filter.js +29 -24
- package/src/actions/data/fold.js +2 -2
- package/src/actions/data/gradientProfile.js +2 -2
- package/src/actions/data/horizon.js +2 -2
- package/src/actions/data/impute.js +2 -2
- package/src/actions/data/index.js +58 -54
- package/src/actions/data/interval.js +4 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +2 -2
- package/src/actions/data/regression.js +9 -7
- package/src/actions/data/revise.js +80 -0
- package/src/actions/data/revisionBindings.js +13 -33
- package/src/actions/data/shared.js +30 -2
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +3 -3
- package/src/actions/data/timeUnit.js +2 -2
- package/src/actions/data/window.js +2 -2
- package/src/actions/encodings/angle.js +5 -2
- package/src/actions/encodings/appearance.js +10 -6
- package/src/actions/encodings/barWidth.js +1 -1
- package/src/actions/encodings/channels.js +2 -2
- package/src/actions/encodings/color/continuous.js +4 -1
- package/src/actions/encodings/color/index.js +5 -2
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/histogram.js +1 -1
- package/src/actions/encodings/horizon.js +6 -8
- package/src/actions/encodings/offset.js +6 -3
- package/src/actions/encodings/parallel.js +4 -5
- package/src/actions/encodings/pathOrder.js +6 -11
- package/src/actions/encodings/position/index.js +4 -4
- package/src/actions/encodings/position/resolve.js +6 -1
- package/src/actions/encodings/ranged.js +11 -6
- package/src/actions/encodings/remove.js +4 -5
- package/src/actions/encodings/ruleAppearance.js +5 -2
- package/src/actions/encodings/seriesLayout.js +1 -1
- package/src/actions/encodings/stroke.js +5 -2
- package/src/actions/encodings/strokeDash.js +8 -3
- package/src/actions/encodings/text.js +7 -2
- package/src/actions/errorBands/create.js +11 -17
- package/src/actions/errorBands/edit.js +13 -131
- package/src/actions/errorBars/create.js +8 -13
- package/src/actions/errorBars/edit.js +10 -121
- package/src/actions/facets/actions.js +30 -16
- package/src/actions/facets/derive.js +8 -2
- package/src/actions/facets/guides.js +4 -5
- package/src/actions/facets/replay.js +4 -5
- package/src/actions/gradientPlots/components.js +5 -6
- package/src/actions/gradientPlots/create.js +4 -5
- package/src/actions/gradientPlots/edit.js +4 -5
- package/src/actions/gradientPlots/materialize.js +2 -2
- package/src/actions/gradientPlots/rebind.js +4 -5
- package/src/actions/guides/axes/axes.js +23 -4
- package/src/actions/guides/axes/axis.js +2 -2
- package/src/actions/guides/axes/edit.js +2 -2
- package/src/actions/guides/axes/labels.js +9 -9
- package/src/actions/guides/axes/lines.js +9 -7
- package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
- package/src/actions/guides/axes/parallel.js +5 -5
- package/src/actions/guides/axes/remove.js +4 -4
- package/src/actions/guides/axes/tickGroups.js +4 -4
- package/src/actions/guides/axes/ticks.js +13 -4
- package/src/actions/guides/axes/titles.js +10 -10
- package/src/actions/guides/grids/grid.js +18 -29
- package/src/actions/guides/guides.js +1 -1
- package/src/actions/guides/legends/blocks.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +4 -4
- package/src/actions/guides/legends/categorical/components.js +38 -33
- package/src/actions/guides/legends/categorical/layout.js +1 -1
- package/src/actions/guides/legends/categorical/symbols.js +27 -30
- package/src/actions/guides/legends/continuous/common.js +46 -8
- package/src/actions/guides/legends/continuous/gradient.js +89 -155
- package/src/actions/guides/legends/continuous/interval.js +13 -13
- package/src/actions/guides/legends/continuous/opacity.js +30 -79
- package/src/actions/guides/legends/continuous/stroke.js +22 -322
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +5 -5
- package/src/actions/guides/legends/lane.js +6 -6
- package/src/actions/guides/legends/remove.js +4 -5
- package/src/actions/guides/legends/size.js +21 -82
- package/src/actions/guides/legends/strokeWidth.js +17 -77
- package/src/actions/guides/polar/axes/facade.js +4 -4
- package/src/actions/guides/polar/axes/labels.js +5 -5
- package/src/actions/guides/polar/axes/lines.js +9 -15
- package/src/actions/guides/polar/axes/ticks.js +9 -15
- package/src/actions/guides/polar/axes/titles.js +5 -5
- package/src/actions/guides/polar/grids.js +11 -13
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/revision.js +126 -0
- package/src/actions/marks/arc/actions.js +3 -3
- package/src/actions/marks/area/actions.js +4 -4
- package/src/actions/marks/bar/create.js +9 -2
- package/src/actions/marks/bar/edit.js +13 -6
- package/src/actions/marks/bar/materialize.js +12 -38
- package/src/actions/marks/line/actions.js +3 -3
- package/src/actions/marks/point/create.js +9 -2
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +20 -26
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/references.js +8 -10
- package/src/actions/marks/remove.js +5 -49
- package/src/actions/marks/rule/actions.js +28 -12
- package/src/actions/marks/text/actions.js +30 -11
- package/src/actions/marks/text/layout.js +12 -20
- package/src/actions/marks/text/removal.js +1 -1
- package/src/actions/marks/text/selection.js +4 -5
- package/src/actions/marks/tick/actions.js +26 -11
- package/src/actions/primitives/createGraphics.js +5 -4
- package/src/actions/primitives/editGraphics.js +5 -4
- package/src/actions/primitives/graphicProperties.js +6 -0
- package/src/actions/primitives/semantic.js +10 -3
- package/src/actions/primitives/semanticAction.js +44 -7
- package/src/actions/primitives/semanticValidation/dataset.js +14 -4
- package/src/actions/primitives/semanticValidation/layer.js +22 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/components.js +10 -14
- package/src/actions/regression/create.js +28 -8
- package/src/actions/regression/edit.js +50 -16
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/resources/remove.js +12 -11
- package/src/actions/scales/channels.js +11 -11
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/create.js +3 -2
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +2 -2
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/offset.js +2 -2
- package/src/actions/scales/parallel.js +2 -2
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +28 -1
- package/src/actions/scales/quantitativeColor.js +4 -5
- package/src/actions/selection/actions.js +25 -36
- package/src/actions/textMetrics/index.js +56 -0
- package/src/actions/theme/actions.js +2 -2
- package/src/actions/theme/reconcile.js +67 -121
- package/src/actions/titles/actions.js +8 -8
- package/src/actions/titles/resolve.js +3 -3
- package/src/actions/violinPlots/create.js +1 -1
- package/src/actions/violinPlots/edit.js +1 -1
- package/src/core/action.js +109 -36
- package/src/core/diagnostics.js +16 -0
- package/src/core/extensionRegistry.js +27 -0
- package/src/core/font.js +18 -0
- package/src/core/identifiers.js +5 -4
- package/src/core/immutable.js +10 -3
- package/src/core/optionRequirements.js +8 -0
- package/src/core/programState.js +6 -2
- package/src/core/resourceReferences.js +15 -6
- package/src/core/textMetricProfile.js +28 -0
- package/src/core/textMetrics.js +24 -3
- package/src/core/validation.js +22 -10
- package/src/diagnostics.js +1 -0
- package/src/grammar/bin.js +39 -10
- package/src/grammar/datasetSchema.js +386 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/impute.js +3 -2
- package/src/grammar/interval.js +32 -1
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/regression/derive.js +57 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +51 -22
- package/src/grammar/regression/parameters.js +56 -4
- package/src/grammar/scales/definition.js +7 -0
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/graphicBounds.js +11 -11
- package/src/grammar/schemas/semanticPath.js +3 -2
- package/src/grammar/sort.js +88 -0
- package/src/grammar/summary.js +117 -19
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +21 -11
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/layout/labels.js +11 -10
- package/src/layout/legend.js +4 -4
- package/src/layout/legendItems.js +6 -6
- package/src/layout/text.js +10 -9
- package/src/layout/title.js +9 -9
- package/src/materialization/bars/resolve.js +4 -2
- package/src/materialization/facetGuides/placement.js +1 -1
- package/src/materialization/facetGuides/preparation.js +1 -1
- package/src/materialization/facetHeaders.js +11 -10
- package/src/materialization/guides/layout.js +3 -3
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/scales/resolve.js +67 -4
- package/src/materialization/selection/items/common.js +1 -1
- package/src/materialization/text.js +6 -4
- package/src/materialization/typography.js +31 -0
- package/src/mcp/cli.js +5 -3
- package/src/persistence/codec.js +65 -0
- package/src/persistence/validation.js +185 -0
- package/src/persistence.js +147 -0
- package/src/renderers/canvas/index.js +5 -4
- package/src/renderers/nativeBackend.js +10 -0
- package/src/renderers/options.js +13 -0
- package/src/renderers/pdf.js +25 -54
- package/src/renderers/png.js +22 -17
- package/src/renderers/svg.js +10 -14
- package/src/renderers/text.js +1 -11
- package/src/selectors/coordinates.js +3 -1
- package/src/selectors/datasets.js +8 -6
- package/src/selectors/layers.js +6 -4
- package/src/selectors/markOwners.js +46 -0
- package/src/selectors/scales.js +6 -4
- package/src/theme/defaults.js +2 -1
- package/src/version.js +2 -0
- package/types/accessibility.d.ts +37 -0
- package/types/basic.d.ts +4 -2
- package/types/diagnostics.d.ts +15 -0
- package/types/index.d.ts +5 -0
- package/types/inspection.d.ts +17 -0
- package/types/pdf.d.ts +9 -0
- package/types/persistence.d.ts +7 -0
- package/types/png.d.ts +9 -0
- package/types/program.d.ts +181 -28
|
@@ -58,14 +58,14 @@ function linearPrediction(model, xValue) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function fitLinearGroup(rows, {
|
|
61
|
-
x, y, group, confidenceMethod, level
|
|
61
|
+
x, y, group, confidenceMethod, level, interval
|
|
62
62
|
}) {
|
|
63
63
|
const count = rows.length;
|
|
64
64
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
65
65
|
const label = `Regression group "${groupLabel}"`;
|
|
66
|
-
if (count < 3) {
|
|
66
|
+
if (count < (interval === false ? 2 : 3)) {
|
|
67
67
|
throw new Error(
|
|
68
|
-
`${label} requires at least three rows.`
|
|
68
|
+
`${label} requires at least ${interval === false ? "two" : "three"} rows.`
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
const xValues = rows.map(row => row[x]);
|
|
@@ -148,6 +148,7 @@ function fitLinearGroup(rows, {
|
|
|
148
148
|
normalizedSxx
|
|
149
149
|
});
|
|
150
150
|
if (!normalizedXByDifference) model.normalizedXByDifference = false;
|
|
151
|
+
if (interval === false) return model;
|
|
151
152
|
const residualSumSquares = stableFiniteSquareSum(
|
|
152
153
|
rows.map(row => row[y] - linearPrediction(model, row[x])),
|
|
153
154
|
`${label} residual sum of squares`
|
|
@@ -238,19 +239,19 @@ function polynomialResponse(design, values, size, scale = 1) {
|
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
function fitPolynomialGroup(rows, {
|
|
241
|
-
x, y, group, confidenceMethod, level, degree
|
|
242
|
+
x, y, group, confidenceMethod, level, degree, interval
|
|
242
243
|
}) {
|
|
243
244
|
const count = rows.length;
|
|
244
245
|
const parameterCount = degree + 1;
|
|
245
246
|
const groupLabel = group === undefined ? "all" : String(group);
|
|
246
247
|
const label = `Polynomial regression group "${groupLabel}"`;
|
|
247
248
|
if (
|
|
248
|
-
count < degree + 2 ||
|
|
249
|
+
count < (interval === false ? parameterCount : degree + 2) ||
|
|
249
250
|
new Set(rows.map(row => row[x])).size < parameterCount
|
|
250
251
|
) {
|
|
251
252
|
throw new Error(
|
|
252
253
|
`${label} requires at least ` +
|
|
253
|
-
`${degree + 2} rows and ${parameterCount} distinct x values.`
|
|
254
|
+
`${interval === false ? parameterCount : degree + 2} rows and ${parameterCount} distinct x values.`
|
|
254
255
|
);
|
|
255
256
|
}
|
|
256
257
|
const xValues = rows.map(row => row[x]);
|
|
@@ -289,7 +290,7 @@ function fitPolynomialGroup(rows, {
|
|
|
289
290
|
`${label} coefficient ${index}`
|
|
290
291
|
))
|
|
291
292
|
: scaledCoefficients;
|
|
292
|
-
const inverse = invertSymmetricMatrix(normal);
|
|
293
|
+
const inverse = interval === false ? undefined : invertSymmetricMatrix(normal);
|
|
293
294
|
const fitted = design.map(basis => stableResponse
|
|
294
295
|
? restoreFiniteScale(
|
|
295
296
|
dot(basis, scaledCoefficients),
|
|
@@ -298,12 +299,12 @@ function fitPolynomialGroup(rows, {
|
|
|
298
299
|
)
|
|
299
300
|
: dot(basis, normalizedCoefficients)
|
|
300
301
|
);
|
|
301
|
-
const residualSumSquares = stableFiniteSquareSum(
|
|
302
|
+
const residualSumSquares = interval === false ? undefined : stableFiniteSquareSum(
|
|
302
303
|
fitted.map((value, index) => rows[index][y] - value),
|
|
303
304
|
`${label} residual sum of squares`
|
|
304
305
|
);
|
|
305
306
|
const degreesOfFreedom = count - parameterCount;
|
|
306
|
-
const residualVariance = residualSumSquares / degreesOfFreedom;
|
|
307
|
+
const residualVariance = interval === false ? undefined : residualSumSquares / degreesOfFreedom;
|
|
307
308
|
const model = {
|
|
308
309
|
count,
|
|
309
310
|
degreesOfFreedom,
|
|
@@ -316,13 +317,15 @@ function fitPolynomialGroup(rows, {
|
|
|
316
317
|
normalizedCoefficients,
|
|
317
318
|
center,
|
|
318
319
|
scale,
|
|
319
|
-
inverse,
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
320
|
+
...(inverse === undefined ? {} : { inverse }),
|
|
321
|
+
...(interval === false ? {} : {
|
|
322
|
+
residualSumSquares,
|
|
323
|
+
residualStandardError: Math.sqrt(residualVariance),
|
|
324
|
+
critical: confidenceCriticalValue({
|
|
325
|
+
method: confidenceMethod,
|
|
326
|
+
level,
|
|
327
|
+
degreesOfFreedom
|
|
328
|
+
})
|
|
326
329
|
})
|
|
327
330
|
};
|
|
328
331
|
if (!normalizedXByDifference) model.normalizedXByDifference = false;
|
|
@@ -345,7 +348,9 @@ function evaluatePolynomial(model, xValue) {
|
|
|
345
348
|
model.responseScale,
|
|
346
349
|
"Polynomial regression prediction"
|
|
347
350
|
),
|
|
348
|
-
|
|
351
|
+
...(model.inverse === undefined ? {} : {
|
|
352
|
+
leverage: dot(basis, model.inverse.map(row => dot(row, basis)))
|
|
353
|
+
})
|
|
349
354
|
};
|
|
350
355
|
}
|
|
351
356
|
|
|
@@ -420,7 +425,29 @@ function fitLoessGroup(rows, { x, y, group, span }) {
|
|
|
420
425
|
neighborIndices: weighted.map(item => item.index)
|
|
421
426
|
};
|
|
422
427
|
});
|
|
423
|
-
return { count: rows.length, span, neighborCount, fits };
|
|
428
|
+
return { count: rows.length, span, neighborCount, fits, rows, x, y, xScale, yScale };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function predictLoessAt(model, xValue) {
|
|
432
|
+
const found = model.fits.find(fit => fit.x === xValue);
|
|
433
|
+
if (found !== undefined) return found.prediction;
|
|
434
|
+
const neighbors = model.rows.map((row, index) => ({
|
|
435
|
+
row,
|
|
436
|
+
index,
|
|
437
|
+
distance: Math.abs(row[model.x] / model.xScale - xValue / model.xScale)
|
|
438
|
+
})).sort((left, right) => left.distance - right.distance || left.index - right.index)
|
|
439
|
+
.slice(0, model.neighborCount);
|
|
440
|
+
const radius = neighbors.at(-1).distance;
|
|
441
|
+
const weighted = neighbors.map(neighbor => ({
|
|
442
|
+
...neighbor,
|
|
443
|
+
weight: radius === 0 ? 1 : (1 - (neighbor.distance / radius) ** 3) ** 3
|
|
444
|
+
}));
|
|
445
|
+
const ordinary = weightedPrediction(weighted, model.x, model.y, xValue);
|
|
446
|
+
return Number.isFinite(ordinary) ? ordinary : restoreFiniteScale(
|
|
447
|
+
weightedPrediction(weighted, model.x, model.y, xValue, model.xScale, model.yScale),
|
|
448
|
+
model.yScale,
|
|
449
|
+
"LOESS regression prediction"
|
|
450
|
+
);
|
|
424
451
|
}
|
|
425
452
|
|
|
426
453
|
export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
@@ -430,7 +457,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
|
430
457
|
y,
|
|
431
458
|
group,
|
|
432
459
|
confidenceMethod: parameters.confidenceMethod,
|
|
433
|
-
level: parameters.level
|
|
460
|
+
level: parameters.level,
|
|
461
|
+
interval: parameters.interval
|
|
434
462
|
});
|
|
435
463
|
}
|
|
436
464
|
if (parameters.method === "polynomial") {
|
|
@@ -440,7 +468,8 @@ export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
|
440
468
|
group,
|
|
441
469
|
confidenceMethod: parameters.confidenceMethod,
|
|
442
470
|
level: parameters.level,
|
|
443
|
-
degree: parameters.degree
|
|
471
|
+
degree: parameters.degree,
|
|
472
|
+
interval: parameters.interval
|
|
444
473
|
});
|
|
445
474
|
}
|
|
446
475
|
return fitLoessGroup(rows, { x, y, group, span: parameters.span });
|
|
@@ -454,8 +483,8 @@ export function predictRegressionAt(model, xValue, parameters) {
|
|
|
454
483
|
? linearPrediction(model, xValue)
|
|
455
484
|
: parameters.method === "polynomial"
|
|
456
485
|
? polynomial.prediction
|
|
457
|
-
: model
|
|
458
|
-
if (parameters.method === "loess") return { prediction };
|
|
486
|
+
: predictLoessAt(model, xValue);
|
|
487
|
+
if (parameters.method === "loess" || parameters.interval === false) return { prediction };
|
|
459
488
|
const leverage = parameters.method === "linear"
|
|
460
489
|
? model.normalizedSxx !== undefined
|
|
461
490
|
? 1 / model.count + (
|
|
@@ -1,10 +1,39 @@
|
|
|
1
|
-
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
2
|
import { validateGeneratedItemLimit } from "../../core/validation.js";
|
|
3
3
|
import { normalizeConfidenceInterval } from
|
|
4
4
|
"../statistics/confidenceInterval.js";
|
|
5
5
|
|
|
6
6
|
const MAX_POLYNOMIAL_DEGREE = 32;
|
|
7
7
|
|
|
8
|
+
export function normalizeRegressionPredict(predict) {
|
|
9
|
+
if (predict === undefined) return undefined;
|
|
10
|
+
if (predict === false) throw new TypeError("Regression predict false is only valid in edits.");
|
|
11
|
+
if (!isPlainObject(predict)) {
|
|
12
|
+
throw new TypeError("Regression predict must be a plain object.");
|
|
13
|
+
}
|
|
14
|
+
const unknown = Object.keys(predict).find(key => !["values", "domain", "steps"].includes(key));
|
|
15
|
+
if (unknown !== undefined) throw new Error(`Unknown regression predict property "${unknown}".`);
|
|
16
|
+
const usesValues = Object.hasOwn(predict, "values");
|
|
17
|
+
const usesDomain = Object.hasOwn(predict, "domain") || Object.hasOwn(predict, "steps");
|
|
18
|
+
if (usesValues === usesDomain) throw new Error("Regression predict requires values or domain with steps.");
|
|
19
|
+
if (usesValues) {
|
|
20
|
+
const values = predict.values;
|
|
21
|
+
if (!Array.isArray(values) || values.length === 0 || values.some((value, index) =>
|
|
22
|
+
!Number.isFinite(value) || (index > 0 && !(value > values[index - 1]))
|
|
23
|
+
)) throw new TypeError("Regression predict values must be finite, unique, and strictly ascending.");
|
|
24
|
+
return cloneAndFreeze({ values: [...values] });
|
|
25
|
+
}
|
|
26
|
+
if (!Array.isArray(predict.domain) || predict.domain.length !== 2 ||
|
|
27
|
+
!predict.domain.every(Number.isFinite) || !(predict.domain[0] < predict.domain[1])) {
|
|
28
|
+
throw new TypeError("Regression predict domain must be two increasing finite numbers.");
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isInteger(predict.steps) || predict.steps < 2) {
|
|
31
|
+
throw new RangeError("Regression predict steps must be an integer of at least two.");
|
|
32
|
+
}
|
|
33
|
+
validateGeneratedItemLimit(predict.steps, "Regression predict steps");
|
|
34
|
+
return cloneAndFreeze({ domain: [...predict.domain], steps: predict.steps });
|
|
35
|
+
}
|
|
36
|
+
|
|
8
37
|
export function requireRegressionField(field, label) {
|
|
9
38
|
if (typeof field !== "string" || field.length === 0) {
|
|
10
39
|
throw new TypeError(`${label} must be a non-empty string.`);
|
|
@@ -45,6 +74,21 @@ export function normalizeRegressionParameters({
|
|
|
45
74
|
if (span !== undefined) {
|
|
46
75
|
throw new Error("Regression span requires the loess method.");
|
|
47
76
|
}
|
|
77
|
+
if (interval === false) {
|
|
78
|
+
if (confidenceMethod !== undefined || level !== undefined || confidence !== undefined) {
|
|
79
|
+
throw new Error("Regression interval false cannot include confidence options.");
|
|
80
|
+
}
|
|
81
|
+
if (method === "polynomial") {
|
|
82
|
+
const resolvedDegree = degree ?? 2;
|
|
83
|
+
if (!Number.isInteger(resolvedDegree) || resolvedDegree < 1) {
|
|
84
|
+
throw new RangeError("Regression polynomial degree must be a positive integer.");
|
|
85
|
+
}
|
|
86
|
+
validateGeneratedItemLimit(resolvedDegree, "Regression polynomial degree", MAX_POLYNOMIAL_DEGREE);
|
|
87
|
+
return cloneAndFreeze({ method, degree: resolvedDegree, interval: false });
|
|
88
|
+
}
|
|
89
|
+
if (degree !== undefined) throw new Error("Regression degree requires the polynomial method.");
|
|
90
|
+
return cloneAndFreeze({ method, interval: false });
|
|
91
|
+
}
|
|
48
92
|
if (level !== undefined && confidence !== undefined && level !== confidence) {
|
|
49
93
|
throw new Error(
|
|
50
94
|
"Regression level and confidence alias must match when both are provided."
|
|
@@ -96,7 +140,7 @@ export function validateRegressionTransform(transform) {
|
|
|
96
140
|
const supported = [
|
|
97
141
|
"type", "method", "x", "y", "groupBy", "confidenceMethod", "level",
|
|
98
142
|
"confidence", "interval",
|
|
99
|
-
"degree", "span"
|
|
143
|
+
"degree", "span", "predict", "missing"
|
|
100
144
|
];
|
|
101
145
|
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
102
146
|
if (unknown !== undefined) {
|
|
@@ -127,9 +171,14 @@ export function validateRegressionTransform(transform) {
|
|
|
127
171
|
confidence: transform.confidence,
|
|
128
172
|
interval: transform.interval
|
|
129
173
|
});
|
|
174
|
+
normalizeRegressionPredict(transform.predict);
|
|
175
|
+
if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
|
|
176
|
+
throw new Error('Regression missing must be "error" or "drop".');
|
|
177
|
+
}
|
|
130
178
|
if (normalized.method === "loess") return transform;
|
|
131
179
|
if (
|
|
132
180
|
transform.confidence === undefined &&
|
|
181
|
+
transform.interval !== false &&
|
|
133
182
|
(transform.confidenceMethod === undefined || transform.level === undefined)
|
|
134
183
|
) {
|
|
135
184
|
throw new Error("Regression confidence provenance requires method and level.");
|
|
@@ -142,6 +191,7 @@ export function validateRegressionTransform(transform) {
|
|
|
142
191
|
|
|
143
192
|
export function normalizeRegressionTransform(args = {}) {
|
|
144
193
|
const parameters = normalizeRegressionParameters(args);
|
|
194
|
+
const predict = normalizeRegressionPredict(args.predict);
|
|
145
195
|
const transform = {
|
|
146
196
|
type: "regression",
|
|
147
197
|
method: parameters.method,
|
|
@@ -153,11 +203,13 @@ export function normalizeRegressionTransform(args = {}) {
|
|
|
153
203
|
: {}),
|
|
154
204
|
...(parameters.method === "loess"
|
|
155
205
|
? { span: parameters.span }
|
|
156
|
-
: {
|
|
206
|
+
: parameters.interval === false ? { interval: false } : {
|
|
157
207
|
confidenceMethod: parameters.confidenceMethod,
|
|
158
208
|
level: parameters.level,
|
|
159
209
|
interval: parameters.interval
|
|
160
|
-
})
|
|
210
|
+
}),
|
|
211
|
+
...(predict === undefined ? {} : { predict }),
|
|
212
|
+
...(args.missing === undefined ? {} : { missing: args.missing })
|
|
161
213
|
};
|
|
162
214
|
validateRegressionTransform(transform);
|
|
163
215
|
return cloneAndFreeze(transform);
|
|
@@ -108,6 +108,13 @@ export function normalizeScaleDefinition({
|
|
|
108
108
|
domain: validateDomain(type, rawDomain),
|
|
109
109
|
range: validateRange(type, rawRange)
|
|
110
110
|
};
|
|
111
|
+
const emptyDomain = retainedValue(previous, patch, defaults, "emptyDomain", typeChanged);
|
|
112
|
+
if (emptyDomain !== undefined) {
|
|
113
|
+
if (!["preserve", "require-explicit"].includes(emptyDomain)) {
|
|
114
|
+
throw new Error('Scale emptyDomain must be "preserve" or "require-explicit".');
|
|
115
|
+
}
|
|
116
|
+
definition.emptyDomain = emptyDomain;
|
|
117
|
+
}
|
|
111
118
|
|
|
112
119
|
for (const property of BOOLEAN_PROPERTIES) {
|
|
113
120
|
let value = retainedValue(previous, patch, defaults, property, typeChanged);
|
|
@@ -26,6 +26,14 @@ export const COMPLETE_SCALE_TYPES = Object.freeze([
|
|
|
26
26
|
...new Set(Object.values(SCALE_TYPES_BY_ROLE).flat())
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
+
// Dimensionless scale spacing is shared by generic and focused editors.
|
|
30
|
+
export const SCALE_OPTION_UNITS = Object.freeze({
|
|
31
|
+
padding: "band-fraction",
|
|
32
|
+
paddingInner: "band-fraction",
|
|
33
|
+
paddingOuter: "band-fraction",
|
|
34
|
+
align: "ratio"
|
|
35
|
+
});
|
|
36
|
+
|
|
29
37
|
const SCALE_PROPERTY_TYPES = Object.freeze({
|
|
30
38
|
midpoint: Object.freeze(["sequential"]),
|
|
31
39
|
nice: Object.freeze(["linear", "log", "pow", "sqrt", "symlog", "time"]),
|
|
@@ -376,7 +376,7 @@ function hasActiveStroke(properties) {
|
|
|
376
376
|
(properties.strokeWidth ?? 0) > 0;
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
function primitiveBounds(type, properties = {}) {
|
|
379
|
+
function primitiveBounds(type, properties = {}, profile) {
|
|
380
380
|
const strokeExtent = hasActiveStroke(properties)
|
|
381
381
|
? properties.strokeWidth / 2
|
|
382
382
|
: 0;
|
|
@@ -420,7 +420,7 @@ function primitiveBounds(type, properties = {}) {
|
|
|
420
420
|
!Number.isFinite(properties.fontSize) ||
|
|
421
421
|
typeof properties.text !== "string"
|
|
422
422
|
) return undefined;
|
|
423
|
-
return resolveTextBounds(properties);
|
|
423
|
+
return resolveTextBounds(properties, profile);
|
|
424
424
|
}
|
|
425
425
|
if (type === "path") {
|
|
426
426
|
const commands = properties.commands;
|
|
@@ -469,15 +469,15 @@ function ancestorOffset(graphicSpec, target) {
|
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
-
function objectBounds(graphicSpec, id, object, ancestors, x, y) {
|
|
472
|
+
function objectBounds(graphicSpec, id, object, ancestors, x, y, profile) {
|
|
473
473
|
if (ancestors.has(id)) {
|
|
474
474
|
throw new Error(`Graphic attachment cycle includes "${id}".`);
|
|
475
475
|
}
|
|
476
476
|
ancestors.add(id);
|
|
477
477
|
let result = object.items === undefined
|
|
478
|
-
? translateBounds(primitiveBounds(object.type, object.properties), x, y)
|
|
478
|
+
? translateBounds(primitiveBounds(object.type, object.properties, profile), x, y)
|
|
479
479
|
: unionBounds(object.items.map(item => translateBounds(
|
|
480
|
-
primitiveBounds(item.type ?? object.type, item.properties),
|
|
480
|
+
primitiveBounds(item.type ?? object.type, item.properties, profile),
|
|
481
481
|
x,
|
|
482
482
|
y
|
|
483
483
|
)));
|
|
@@ -489,7 +489,7 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y) {
|
|
|
489
489
|
throw new Error(`Unknown attached graphic "${childId}".`);
|
|
490
490
|
}
|
|
491
491
|
const bounds = objectBounds(
|
|
492
|
-
graphicSpec, childId, child.object, ancestors, childX, childY
|
|
492
|
+
graphicSpec, childId, child.object, ancestors, childX, childY, profile
|
|
493
493
|
);
|
|
494
494
|
if (bounds !== undefined) {
|
|
495
495
|
result = includeExtent(
|
|
@@ -501,23 +501,23 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y) {
|
|
|
501
501
|
return result;
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
export function resolveConcreteGraphicBounds(graphicSpec, target) {
|
|
504
|
+
export function resolveConcreteGraphicBounds(graphicSpec, target, profile) {
|
|
505
505
|
const found = requireGraphic(graphicSpec, target);
|
|
506
506
|
const [x, y] = ancestorOffset(graphicSpec, target);
|
|
507
507
|
return requireFiniteBounds(found.kind === "item"
|
|
508
508
|
? translateBounds(
|
|
509
|
-
primitiveBounds(found.object.type ?? found.owner.type, found.object.properties),
|
|
509
|
+
primitiveBounds(found.object.type ?? found.owner.type, found.object.properties, profile),
|
|
510
510
|
x,
|
|
511
511
|
y
|
|
512
512
|
)
|
|
513
|
-
: objectBounds(graphicSpec, found.id, found.object, new Set(), x, y));
|
|
513
|
+
: objectBounds(graphicSpec, found.id, found.object, new Set(), x, y, profile));
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
export function unionConcreteGraphicBounds(graphicSpec, targets) {
|
|
516
|
+
export function unionConcreteGraphicBounds(graphicSpec, targets, profile) {
|
|
517
517
|
if (!Array.isArray(targets)) {
|
|
518
518
|
throw new TypeError("Graphic bounds targets must be an array.");
|
|
519
519
|
}
|
|
520
520
|
return unionBounds(targets.map(target =>
|
|
521
|
-
resolveConcreteGraphicBounds(graphicSpec, target)
|
|
521
|
+
resolveConcreteGraphicBounds(graphicSpec, target, profile)
|
|
522
522
|
));
|
|
523
523
|
}
|
|
@@ -49,7 +49,7 @@ const ENCODING_PATHS = Object.freeze([
|
|
|
49
49
|
const ENTITY_PATHS = Object.freeze({
|
|
50
50
|
dataset: {
|
|
51
51
|
collection: "datasets",
|
|
52
|
-
properties: new Set(["source", "transform", "values"])
|
|
52
|
+
properties: new Set(["source", "transform", "schema", "values"])
|
|
53
53
|
},
|
|
54
54
|
layer: {
|
|
55
55
|
collection: "layers",
|
|
@@ -64,6 +64,7 @@ const ENTITY_PATHS = Object.freeze({
|
|
|
64
64
|
"transform",
|
|
65
65
|
"mark.type",
|
|
66
66
|
"mark.missing",
|
|
67
|
+
"derivedBindings.regression",
|
|
67
68
|
"layout.mode",
|
|
68
69
|
...ENCODING_PATHS
|
|
69
70
|
])
|
|
@@ -71,7 +72,7 @@ const ENTITY_PATHS = Object.freeze({
|
|
|
71
72
|
scale: {
|
|
72
73
|
collection: "scales",
|
|
73
74
|
properties: new Set([
|
|
74
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
75
|
+
"type", "domain", "range", "emptyDomain", "nice", "zero", "clamp", "reverse",
|
|
75
76
|
"unknown", "base", "exponent", "constant", "interpolate", "radialMapping", "midpoint",
|
|
76
77
|
"paddingInner", "paddingOuter", "padding", "align"
|
|
77
78
|
])
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
3
|
+
|
|
4
|
+
const ORDERS = new Set(["ascending", "descending"]);
|
|
5
|
+
const NULLS = new Set(["first", "last"]);
|
|
6
|
+
const TEMPORAL_UNITS = new Set(["year", "timestamp"]);
|
|
7
|
+
|
|
8
|
+
export function normalizeSortTransform({ sortBy }) {
|
|
9
|
+
if (!Array.isArray(sortBy) || sortBy.length === 0 ||
|
|
10
|
+
Array.from({ length: sortBy.length }, (_, index) => index).some(index => !Object.hasOwn(sortBy, index))) {
|
|
11
|
+
throw new TypeError("Sorted data sortBy must be a non-empty dense array.");
|
|
12
|
+
}
|
|
13
|
+
const keys = sortBy.map((key, index) => {
|
|
14
|
+
if (!isPlainObject(key)) throw new TypeError(`Sort key ${index} must be a plain object.`);
|
|
15
|
+
const unknown = Object.keys(key).find(name => !["field", "order", "nulls", "temporalUnit"].includes(name));
|
|
16
|
+
if (unknown !== undefined) throw new Error(`Unknown sort key property "${unknown}".`);
|
|
17
|
+
if (typeof key.field !== "string" || key.field.length === 0) {
|
|
18
|
+
throw new TypeError(`Sort key ${index} field must be a non-empty string.`);
|
|
19
|
+
}
|
|
20
|
+
const order = key.order ?? "ascending";
|
|
21
|
+
const nulls = key.nulls ?? "last";
|
|
22
|
+
if (!ORDERS.has(order)) throw new Error(`Unsupported sort order "${order}".`);
|
|
23
|
+
if (!NULLS.has(nulls)) throw new Error(`Unsupported sort null placement "${nulls}".`);
|
|
24
|
+
if (key.temporalUnit !== undefined && !TEMPORAL_UNITS.has(key.temporalUnit)) {
|
|
25
|
+
throw new Error(`Unsupported sort temporalUnit "${key.temporalUnit}".`);
|
|
26
|
+
}
|
|
27
|
+
return { field: key.field, order, nulls, ...(key.temporalUnit === undefined ? {} : { temporalUnit: key.temporalUnit }) };
|
|
28
|
+
});
|
|
29
|
+
if (new Set(keys.map(key => key.field)).size !== keys.length) {
|
|
30
|
+
throw new Error("Sorted data sortBy fields must be unique.");
|
|
31
|
+
}
|
|
32
|
+
return cloneAndFreeze({ type: "sort", sortBy: keys });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateSortTransform(transform) {
|
|
36
|
+
if (!isPlainObject(transform)) throw new TypeError("Sort transform must be a plain object.");
|
|
37
|
+
const unknown = Object.keys(transform).find(key => !["type", "sortBy"].includes(key));
|
|
38
|
+
if (unknown !== undefined) throw new Error(`Unknown sort transform property "${unknown}".`);
|
|
39
|
+
if (transform.type !== "sort") throw new Error('Sort transform type must be "sort".');
|
|
40
|
+
normalizeSortTransform(transform);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function compareValues(left, right) {
|
|
44
|
+
if (left === right) return 0;
|
|
45
|
+
return left < right ? -1 : 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizedKey(value, key, rowIndex) {
|
|
49
|
+
if (value === null || value === undefined) return { missing: true };
|
|
50
|
+
if (key.temporalUnit !== undefined) {
|
|
51
|
+
return { missing: false, value: normalizeTemporalValue(value, key.field, rowIndex, key.temporalUnit) };
|
|
52
|
+
}
|
|
53
|
+
if (!["number", "string", "boolean"].includes(typeof value) ||
|
|
54
|
+
(typeof value === "number" && !Number.isFinite(value))) {
|
|
55
|
+
throw new TypeError(`Sort field "${key.field}" requires finite number, string, boolean, or missing values at row ${rowIndex}.`);
|
|
56
|
+
}
|
|
57
|
+
return { missing: false, value, type: typeof value };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function deriveSortedRows(values, transform) {
|
|
61
|
+
if (!Array.isArray(values)) throw new TypeError("Sort source values must be an array.");
|
|
62
|
+
validateSortTransform(transform);
|
|
63
|
+
const decorated = values.map((row, index) => ({
|
|
64
|
+
row,
|
|
65
|
+
index,
|
|
66
|
+
keys: transform.sortBy.map(key => normalizedKey(row[key.field], key, index))
|
|
67
|
+
}));
|
|
68
|
+
for (let keyIndex = 0; keyIndex < transform.sortBy.length; keyIndex += 1) {
|
|
69
|
+
const key = transform.sortBy[keyIndex];
|
|
70
|
+
const types = new Set(decorated.map(entry => entry.keys[keyIndex]).filter(value => !value.missing).map(value => value.type ?? "number"));
|
|
71
|
+
if (types.size > 1) throw new TypeError(`Sort field "${key.field}" contains mixed non-missing types.`);
|
|
72
|
+
}
|
|
73
|
+
decorated.sort((left, right) => {
|
|
74
|
+
for (let index = 0; index < transform.sortBy.length; index += 1) {
|
|
75
|
+
const key = transform.sortBy[index];
|
|
76
|
+
const a = left.keys[index];
|
|
77
|
+
const b = right.keys[index];
|
|
78
|
+
if (a.missing || b.missing) {
|
|
79
|
+
if (a.missing !== b.missing) return a.missing === (key.nulls === "first") ? -1 : 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const compared = compareValues(a.value, b.value);
|
|
83
|
+
if (compared !== 0) return key.order === "ascending" ? compared : -compared;
|
|
84
|
+
}
|
|
85
|
+
return left.index - right.index;
|
|
86
|
+
});
|
|
87
|
+
return decorated.map(entry => entry.row);
|
|
88
|
+
}
|