ggaction 0.0.12 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/README.md +20 -11
- 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 +10 -3
- package/src/actions/basic.js +4 -2
- package/src/actions/boxPlots/components.js +14 -4
- package/src/actions/boxPlots/edit.js +31 -5
- package/src/actions/boxPlots/materialize.js +13 -4
- package/src/actions/boxPlots/options.js +48 -16
- 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 +18 -6
- package/src/actions/charts/area.js +51 -0
- package/src/actions/charts/bar.js +13 -6
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +48 -0
- package/src/actions/charts/ecdf.js +186 -0
- package/src/actions/charts/endpoints.js +473 -0
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/charts/histogram.js +5 -4
- package/src/actions/charts/horizon.js +73 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +172 -0
- package/src/actions/charts/line.js +8 -6
- package/src/actions/charts/parallel.js +8 -2
- package/src/actions/charts/pie.js +52 -0
- package/src/actions/charts/polar.js +134 -0
- package/src/actions/charts/radar.js +305 -0
- package/src/actions/charts/radial.js +50 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +283 -0
- package/src/actions/charts/scatter.js +11 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +129 -3
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/bin2d.js +57 -185
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/create.js +2 -2
- package/src/actions/data/density.js +12 -20
- package/src/actions/data/derived.js +111 -15
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/edit.js +539 -0
- package/src/actions/data/filter.js +226 -30
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +38 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +19 -21
- package/src/actions/data/revisionBindings.js +145 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +4 -1
- package/src/actions/data/window.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/channels.js +415 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +30 -23
- 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 +124 -34
- package/src/actions/encodings/histogram.js +3 -2
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +6 -0
- package/src/actions/encodings/offset.js +30 -29
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +63 -11
- package/src/actions/encodings/position/index.js +52 -6
- package/src/actions/encodings/position/policies/arc.js +14 -0
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +57 -13
- package/src/actions/encodings/position/resolve.js +88 -26
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +44 -34
- package/src/actions/encodings/ruleAppearance.js +24 -32
- package/src/actions/encodings/seriesLayout.js +105 -0
- package/src/actions/encodings/shared.js +107 -37
- package/src/actions/encodings/stroke.js +226 -0
- 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 +41 -8
- package/src/actions/errorBands/edit.js +360 -25
- package/src/actions/errorBars/create.js +56 -17
- package/src/actions/errorBars/edit.js +372 -21
- package/src/actions/facets/actions.js +619 -41
- package/src/actions/facets/derive.js +139 -25
- package/src/actions/gradientPlots/components.js +43 -13
- package/src/actions/gradientPlots/edit.js +14 -4
- package/src/actions/gradientPlots/materialize.js +8 -8
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/guides/applicability.js +28 -19
- 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 +184 -28
- package/src/actions/guides/axes/lines.js +13 -7
- 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 +50 -25
- 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 +15 -8
- package/src/actions/guides/axes/ticks.js +11 -6
- package/src/actions/guides/axes/titles.js +32 -11
- 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 +17 -4
- package/src/actions/guides/guides.js +54 -31
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +309 -279
- package/src/actions/guides/legends/categorical/components.js +21 -17
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +123 -354
- package/src/actions/guides/legends/categorical/options.js +32 -16
- package/src/actions/guides/legends/categorical/recipes.js +114 -9
- package/src/actions/guides/legends/categorical/resolve.js +30 -15
- package/src/actions/guides/legends/categorical/symbols.js +87 -33
- package/src/actions/guides/legends/continuous/common.js +99 -26
- package/src/actions/guides/legends/continuous/gradient.js +120 -89
- package/src/actions/guides/legends/continuous/index.js +8 -2
- package/src/actions/guides/legends/continuous/interval.js +79 -95
- package/src/actions/guides/legends/continuous/opacity.js +113 -89
- package/src/actions/guides/legends/continuous/stroke.js +525 -0
- package/src/actions/guides/legends/creation.js +40 -0
- package/src/actions/guides/legends/edit.js +321 -90
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +149 -102
- package/src/actions/guides/legends/lifecycle.js +143 -0
- package/src/actions/guides/legends/remove.js +43 -78
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +213 -116
- package/src/actions/guides/legends/strokeWidth.js +121 -73
- package/src/actions/guides/legends/target.js +123 -0
- package/src/actions/guides/legends/transition.js +192 -0
- package/src/actions/guides/polar/axes/facade.js +81 -66
- package/src/actions/guides/polar/axes/labels.js +28 -8
- package/src/actions/guides/polar/axes/lines.js +12 -5
- package/src/actions/guides/polar/axes/shared.js +22 -0
- package/src/actions/guides/polar/axes/ticks.js +6 -5
- package/src/actions/guides/polar/axes/titles.js +6 -5
- package/src/actions/guides/polar/grids.js +8 -8
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +15 -2
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +78 -20
- package/src/actions/marks/area/actions.js +51 -12
- package/src/actions/marks/area/materialize.js +22 -3
- package/src/actions/marks/bar/create.js +11 -2
- package/src/actions/marks/bar/edit.js +20 -3
- package/src/actions/marks/bar/materialize.js +33 -9
- package/src/actions/marks/index.js +14 -0
- package/src/actions/marks/line/actions.js +71 -113
- package/src/actions/marks/line/materialize.js +57 -56
- package/src/actions/marks/point/create.js +8 -2
- package/src/actions/marks/point/edit.js +29 -21
- 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 +154 -16
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/rect/actions.js +33 -12
- package/src/actions/marks/references.js +658 -0
- package/src/actions/marks/remove.js +87 -4
- package/src/actions/marks/rule/actions.js +77 -9
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +13 -13
- package/src/actions/marks/text/actions.js +296 -13
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +156 -8
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +73 -0
- package/src/actions/marks/tick/actions.js +22 -5
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/editGraphics.js +26 -0
- package/src/actions/primitives/semantic.js +3 -2
- package/src/actions/primitives/semanticAction.js +120 -10
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +23 -1
- package/src/actions/primitives/semanticValidation/scale.js +38 -8
- package/src/actions/regression/components.js +26 -11
- package/src/actions/regression/create.js +11 -3
- package/src/actions/regression/edit.js +40 -13
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +172 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +21 -54
- package/src/actions/scales/consumers/families.js +28 -14
- package/src/actions/scales/consumers/index.js +42 -4
- package/src/actions/scales/consumers/seriesLayout.js +57 -12
- package/src/actions/scales/create.js +58 -26
- package/src/actions/scales/definitions.js +109 -39
- package/src/actions/scales/edit.js +101 -34
- package/src/actions/scales/editPolicy.js +95 -35
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +14 -49
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +140 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/selection/actions.js +154 -30
- package/src/actions/selection/index.js +3 -0
- package/src/actions/theme/actions.js +98 -0
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +16 -0
- package/src/actions/theme/reconcile.js +1130 -0
- package/src/actions/theme/state.js +160 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +90 -43
- package/src/actions/violinPlots/edit.js +192 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +32 -2
- package/src/core/compositionState.js +141 -8
- package/src/core/programState.js +20 -0
- package/src/core/resourceReferences.js +332 -0
- package/src/core/vocabulary.js +16 -3
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +58 -1
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +37 -13
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +226 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +351 -0
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +123 -52
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/impute.js +340 -0
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +110 -0
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +109 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- 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 +8 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +63 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +8 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +57 -0
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +42 -5
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +179 -30
- package/src/grammar/schemas/semanticPath.js +23 -4
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +245 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +234 -13
- package/src/grammar/transformTopology.js +10 -0
- package/src/grammar/transforms.js +418 -16
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +232 -21
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +100 -12
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/labels.js +343 -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 +56 -147
- package/src/materialization/bars/histogram.js +104 -20
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/composition.js +3 -1
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +93 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +8 -2
- package/src/materialization/facetGuides/preparation.js +12 -7
- package/src/materialization/facetHeaders.js +346 -0
- package/src/materialization/facets.js +23 -66
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +28 -2
- package/src/materialization/legends.js +11 -2
- package/src/materialization/marks/capabilities.js +20 -5
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +68 -27
- package/src/materialization/marks/policies.js +4 -1
- package/src/materialization/rect.js +55 -42
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/rowEncoding.js +13 -13
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +6 -3
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +5 -1
- package/src/materialization/scales/resolve.js +65 -17
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +42 -71
- package/src/materialization/selection/items/common.js +27 -6
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +35 -8
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +324 -14
- package/src/mcp/adapter.js +2 -2
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/line.js +2 -3
- package/src/renderers/canvas/native.js +4 -4
- package/src/renderers/canvas/path.js +2 -3
- package/src/renderers/canvas/rect.js +2 -3
- package/src/renderers/canvas/stroke.js +11 -0
- package/src/renderers/svg.js +19 -0
- package/src/selectors/datasets.js +44 -0
- package/src/selectors/layers.js +7 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +140 -0
- package/types/basic.d.ts +17 -2
- package/types/index.d.ts +211 -0
- package/types/program.d.ts +2182 -204
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -1,6 +1,137 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
2
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
3
|
import { findTransformPolicy } from "../transforms.js";
|
|
4
|
+
import { BAR_GRAINS, resolveBarGrain } from "../bars/policy.js";
|
|
5
|
+
import { resolveRectMode } from "../rects.js";
|
|
6
|
+
import { resolveRuleMode } from "../rules.js";
|
|
7
|
+
|
|
8
|
+
const CARTESIAN_MARKS = new Set([
|
|
9
|
+
"point", "line", "area", "bar", "rule", "tick", "rect"
|
|
10
|
+
]);
|
|
11
|
+
const FACET_BAR_GRAINS = new Set([
|
|
12
|
+
BAR_GRAINS.histogram,
|
|
13
|
+
BAR_GRAINS.aggregate,
|
|
14
|
+
BAR_GRAINS.ranged
|
|
15
|
+
]);
|
|
16
|
+
const ORDINARY_SCALE_CHANNELS = Object.freeze([
|
|
17
|
+
["x", "x"], ["y", "y"], ["xOffset", "xOffset"], ["yOffset", "yOffset"],
|
|
18
|
+
["theta", "theta"], ["r", "radius"], ["color", "color"],
|
|
19
|
+
["stroke", "stroke"], ["size", "size"], ["shape", "shape"],
|
|
20
|
+
["opacity", "opacity"], ["strokeDash", "strokeDash"]
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function coordinateType(semanticSpec, layer) {
|
|
24
|
+
if (layer.coordinate === undefined) return undefined;
|
|
25
|
+
return semanticSpec.coordinates?.find(value => value.id === layer.coordinate)?.type;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function classifyPrimaryLayer(semanticSpec, layer) {
|
|
29
|
+
const type = layer.mark?.type;
|
|
30
|
+
const parallel = layer.encoding?.parallel;
|
|
31
|
+
if (parallel !== undefined || coordinateType(semanticSpec, layer) === "parallel") {
|
|
32
|
+
const complete = type === "line" && Array.isArray(parallel?.dimensions) &&
|
|
33
|
+
parallel.dimensions.length >= 2 &&
|
|
34
|
+
parallel.dimensions.every(dimension =>
|
|
35
|
+
typeof dimension?.scale === "string" && dimension.scale.length > 0
|
|
36
|
+
);
|
|
37
|
+
if (!complete) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Facet layer "${layer.id}" must be a complete materializable Parallel mark.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return "parallel";
|
|
43
|
+
}
|
|
44
|
+
const polar = layer.encoding?.theta !== undefined ||
|
|
45
|
+
layer.encoding?.radius !== undefined || coordinateType(semanticSpec, layer) === "polar";
|
|
46
|
+
if (polar) {
|
|
47
|
+
const theta = layer.encoding?.theta?.scale !== undefined;
|
|
48
|
+
const radius = layer.encoding?.radius?.scale !== undefined;
|
|
49
|
+
const complete = (type === "point" || type === "line")
|
|
50
|
+
? theta && radius
|
|
51
|
+
: type === "arc" && theta;
|
|
52
|
+
if (!complete) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Facet layer "${layer.id}" must be a complete materializable Polar mark.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return "polar";
|
|
58
|
+
}
|
|
59
|
+
if (!CARTESIAN_MARKS.has(type)) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`facet does not support mark "${layer.id}" of type ${type ?? "incomplete"}.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (type === "bar" && !FACET_BAR_GRAINS.has(resolveBarGrain(layer))) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const complete = (
|
|
70
|
+
layer.encoding?.x?.scale !== undefined &&
|
|
71
|
+
layer.encoding?.y?.scale !== undefined
|
|
72
|
+
) || (type === "rule" && resolveRuleMode(layer) !== undefined) ||
|
|
73
|
+
(type === "rect" && resolveRectMode(layer) !== undefined);
|
|
74
|
+
if (!complete) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Facet layer "${layer.id}" must be a complete materializable Cartesian mark.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return "cartesian";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function collectFacetScaleBindings(semanticSpec) {
|
|
83
|
+
if (!isPlainObject(semanticSpec) || !Array.isArray(semanticSpec.layers)) {
|
|
84
|
+
throw new TypeError("Facet scale bindings require a semantic spec with layers.");
|
|
85
|
+
}
|
|
86
|
+
const bindings = [];
|
|
87
|
+
for (const layer of semanticSpec.layers) {
|
|
88
|
+
for (const [policyKey, semanticChannel] of ORDINARY_SCALE_CHANNELS) {
|
|
89
|
+
const scaleId = layer.encoding?.[semanticChannel]?.scale;
|
|
90
|
+
if (scaleId === undefined) continue;
|
|
91
|
+
bindings.push({ layerId: layer.id, scaleId, policyKey, semanticChannel });
|
|
92
|
+
}
|
|
93
|
+
for (const [dimensionIndex, dimension] of
|
|
94
|
+
(layer.encoding?.parallel?.dimensions ?? []).entries()) {
|
|
95
|
+
if (dimension?.scale === undefined) continue;
|
|
96
|
+
bindings.push({
|
|
97
|
+
layerId: layer.id,
|
|
98
|
+
scaleId: dimension.scale,
|
|
99
|
+
policyKey: "parallelDimensions",
|
|
100
|
+
semanticChannel: "parallel",
|
|
101
|
+
dimensionField: dimension.field,
|
|
102
|
+
dimensionIndex
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return cloneAndFreeze(bindings);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolveFacetFamily(semanticSpec) {
|
|
110
|
+
if (!isPlainObject(semanticSpec) || !Array.isArray(semanticSpec.layers) ||
|
|
111
|
+
semanticSpec.layers.length === 0) {
|
|
112
|
+
throw new Error("facet requires at least one materializable layer.");
|
|
113
|
+
}
|
|
114
|
+
const primary = semanticSpec.layers.filter(layer =>
|
|
115
|
+
!(layer.mark?.type === "text" && layer.source !== undefined)
|
|
116
|
+
);
|
|
117
|
+
if (primary.length === 0) {
|
|
118
|
+
throw new Error("facet requires at least one primary materializable layer.");
|
|
119
|
+
}
|
|
120
|
+
const families = primary.map(layer => classifyPrimaryLayer(semanticSpec, layer));
|
|
121
|
+
if (new Set(families).size !== 1) {
|
|
122
|
+
throw new Error("facet requires every primary layer to use one coordinate family.");
|
|
123
|
+
}
|
|
124
|
+
const family = families[0];
|
|
125
|
+
return cloneAndFreeze({
|
|
126
|
+
family,
|
|
127
|
+
primaryLayers: primary.map(layer => layer.id),
|
|
128
|
+
dependentLayers: semanticSpec.layers
|
|
129
|
+
.filter(layer => !primary.includes(layer))
|
|
130
|
+
.map(layer => layer.id),
|
|
131
|
+
scaleBindings: collectFacetScaleBindings(semanticSpec),
|
|
132
|
+
coordinates: [...new Set(primary.map(layer => layer.coordinate).filter(Boolean))]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
4
135
|
|
|
5
136
|
function requireCollection(value, label) {
|
|
6
137
|
if (!Array.isArray(value) || value.length === 0) {
|
|
@@ -172,6 +303,7 @@ export function planFacetDependencies(semanticSpec, options = {}) {
|
|
|
172
303
|
field,
|
|
173
304
|
anchor,
|
|
174
305
|
replay,
|
|
175
|
-
layers: layerPaths.map(({ layerId, data }) => ({ id: layerId, data }))
|
|
306
|
+
layers: layerPaths.map(({ layerId, data }) => ({ id: layerId, data })),
|
|
307
|
+
...(options.family === undefined ? {} : options.family)
|
|
176
308
|
});
|
|
177
309
|
}
|
|
@@ -3,8 +3,11 @@ import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
|
3
3
|
const LEGEND_FAMILIES = Object.freeze({
|
|
4
4
|
series: "categorical",
|
|
5
5
|
color: "categorical",
|
|
6
|
+
stroke: "categorical",
|
|
6
7
|
gradient: "gradient",
|
|
7
8
|
interval: "discretized",
|
|
9
|
+
strokeGradient: "gradient",
|
|
10
|
+
strokeInterval: "discretized",
|
|
8
11
|
size: "size",
|
|
9
12
|
opacity: "opacity"
|
|
10
13
|
});
|
|
@@ -4,61 +4,17 @@ import {
|
|
|
4
4
|
validateGeneratedItemLimit,
|
|
5
5
|
validateWorkLimit
|
|
6
6
|
} from "../../core/validation.js";
|
|
7
|
-
import {
|
|
8
|
-
import { planFacetDependencies } from "./dependencies.js";
|
|
7
|
+
import { planFacetDependencies, resolveFacetFamily } from "./dependencies.js";
|
|
9
8
|
import { readNominalField } from "../scales/index.js";
|
|
10
|
-
|
|
11
|
-
const SUPPORTED_MARKS = new Set([
|
|
12
|
-
"point", "line", "area", "bar", "rule", "tick", "rect"
|
|
13
|
-
]);
|
|
14
|
-
const SUPPORTED_BAR_GRAINS = new Set([
|
|
15
|
-
BAR_GRAINS.histogram,
|
|
16
|
-
BAR_GRAINS.aggregate,
|
|
17
|
-
BAR_GRAINS.ranged
|
|
18
|
-
]);
|
|
19
9
|
const MAX_FACET_CHILDREN = 100;
|
|
20
10
|
|
|
21
|
-
function requireFacetField(field) {
|
|
11
|
+
function requireFacetField(field, label = "facet") {
|
|
22
12
|
if (typeof field !== "string" || field.length === 0) {
|
|
23
|
-
throw new TypeError(
|
|
13
|
+
throw new TypeError(`${label} requires a non-empty field.`);
|
|
24
14
|
}
|
|
25
15
|
return field;
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
function requireSupportedLayer(layer) {
|
|
29
|
-
if (!SUPPORTED_MARKS.has(layer.mark?.type)) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`facet does not support mark "${layer.id}" of type ${layer.mark?.type ?? "incomplete"}.`
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
if (
|
|
35
|
-
layer.mark.type === "bar" &&
|
|
36
|
-
!SUPPORTED_BAR_GRAINS.has(resolveBarGrain(layer))
|
|
37
|
-
) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`facet requires bar mark "${layer.id}" to be a complete histogram, aggregate, or ranged bar.`
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
if (
|
|
43
|
-
layer.encoding?.x?.scale === undefined ||
|
|
44
|
-
layer.encoding?.y?.scale === undefined
|
|
45
|
-
) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Facet layer "${layer.id}" must be a complete materializable Cartesian mark.`
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (typeof layer.data !== "string" || layer.data.length === 0) {
|
|
51
|
-
throw new Error(`Facet layer "${layer.id}" requires a dataset.`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function requireSupportedLayers(semanticSpec) {
|
|
56
|
-
if (!Array.isArray(semanticSpec.layers) || semanticSpec.layers.length === 0) {
|
|
57
|
-
throw new Error("facet requires at least one materializable layer.");
|
|
58
|
-
}
|
|
59
|
-
for (const layer of semanticSpec.layers) requireSupportedLayer(layer);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
18
|
function requirePartitionDataset(semanticSpec, id) {
|
|
63
19
|
const dataset = semanticSpec.datasets?.find(candidate => candidate.id === id);
|
|
64
20
|
if (dataset === undefined) {
|
|
@@ -74,25 +30,52 @@ function uniqueInOrder(values) {
|
|
|
74
30
|
return [...new Set(values)];
|
|
75
31
|
}
|
|
76
32
|
|
|
77
|
-
function resolveValues(observed, requested) {
|
|
33
|
+
function resolveValues(observed, requested, label = "facet values") {
|
|
78
34
|
if (requested === undefined) return observed;
|
|
79
35
|
if (!Array.isArray(requested) || requested.length === 0) {
|
|
80
|
-
throw new TypeError(
|
|
36
|
+
throw new TypeError(`${label} must be a non-empty array when provided.`);
|
|
81
37
|
}
|
|
82
38
|
const normalized = readNominalField(
|
|
83
39
|
requested.map(value => ({ value })),
|
|
84
40
|
"value"
|
|
85
41
|
);
|
|
86
42
|
if (new Set(normalized).size !== normalized.length) {
|
|
87
|
-
throw new Error(
|
|
43
|
+
throw new Error(`${label} must be unique.`);
|
|
88
44
|
}
|
|
89
45
|
const missing = normalized.find(value => !observed.includes(value));
|
|
90
46
|
if (missing !== undefined) {
|
|
91
|
-
throw new Error(
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${label} value ${JSON.stringify(missing)} is not present in the source field.`
|
|
49
|
+
);
|
|
92
50
|
}
|
|
93
51
|
return normalized;
|
|
94
52
|
}
|
|
95
53
|
|
|
54
|
+
function requireGridRole(value, label) {
|
|
55
|
+
if (!isPlainObject(value)) {
|
|
56
|
+
throw new TypeError(`facetGrid ${label} must be a plain object.`);
|
|
57
|
+
}
|
|
58
|
+
const unknown = Object.keys(value).find(key => !["field", "values"].includes(key));
|
|
59
|
+
if (unknown !== undefined) {
|
|
60
|
+
throw new Error(`Unknown facetGrid ${label} option "${unknown}".`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
field: requireFacetField(value.field, `facetGrid ${label}`),
|
|
64
|
+
...(value.values === undefined ? {} : { values: value.values })
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function pairKey(row, column) {
|
|
69
|
+
return JSON.stringify([
|
|
70
|
+
[typeof row, row],
|
|
71
|
+
[typeof column, column]
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function displayValue(value) {
|
|
76
|
+
return value === "" ? "(empty)" : String(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
96
79
|
export function resolveFacetDefinition(semanticSpec, options = {}) {
|
|
97
80
|
if (!isPlainObject(semanticSpec)) {
|
|
98
81
|
throw new TypeError("resolveFacetDefinition requires a semantic spec.");
|
|
@@ -102,9 +85,10 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
|
|
|
102
85
|
}
|
|
103
86
|
const field = requireFacetField(options.field);
|
|
104
87
|
const id = validateUserId(options.id ?? "facet", "Facet id");
|
|
105
|
-
|
|
88
|
+
const family = resolveFacetFamily(semanticSpec);
|
|
106
89
|
const dependencies = planFacetDependencies(semanticSpec, {
|
|
107
90
|
field,
|
|
91
|
+
family,
|
|
108
92
|
...(options.data === undefined ? {} : { data: options.data })
|
|
109
93
|
});
|
|
110
94
|
const data = dependencies.anchor;
|
|
@@ -125,6 +109,7 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
|
|
|
125
109
|
);
|
|
126
110
|
return cloneAndFreeze({
|
|
127
111
|
id,
|
|
112
|
+
family: family.family,
|
|
128
113
|
data,
|
|
129
114
|
field,
|
|
130
115
|
values,
|
|
@@ -136,3 +121,89 @@ export function resolveFacetDefinition(semanticSpec, options = {}) {
|
|
|
136
121
|
}))
|
|
137
122
|
});
|
|
138
123
|
}
|
|
124
|
+
|
|
125
|
+
export function resolveFacetGridDefinition(semanticSpec, options = {}) {
|
|
126
|
+
if (!isPlainObject(semanticSpec)) {
|
|
127
|
+
throw new TypeError("resolveFacetGridDefinition requires a semantic spec.");
|
|
128
|
+
}
|
|
129
|
+
if (!isPlainObject(options)) {
|
|
130
|
+
throw new TypeError("facetGrid options must be a plain object.");
|
|
131
|
+
}
|
|
132
|
+
const rows = requireGridRole(options.rows, "rows");
|
|
133
|
+
const columns = requireGridRole(options.columns, "columns");
|
|
134
|
+
if (rows.field === columns.field) {
|
|
135
|
+
throw new Error("facetGrid rows.field and columns.field must be different.");
|
|
136
|
+
}
|
|
137
|
+
const combinations = options.combinations ?? "observed";
|
|
138
|
+
if (!["observed", "full"].includes(combinations)) {
|
|
139
|
+
throw new Error('facetGrid combinations must be "observed" or "full".');
|
|
140
|
+
}
|
|
141
|
+
const id = validateUserId(options.id ?? "facetGrid", "Facet grid id");
|
|
142
|
+
const family = resolveFacetFamily(semanticSpec);
|
|
143
|
+
const dependencies = planFacetDependencies(semanticSpec, {
|
|
144
|
+
field: rows.field,
|
|
145
|
+
family,
|
|
146
|
+
...(options.data === undefined ? {} : { data: options.data })
|
|
147
|
+
});
|
|
148
|
+
const data = dependencies.anchor;
|
|
149
|
+
const dataset = requirePartitionDataset(semanticSpec, data);
|
|
150
|
+
const observedRows = readNominalField(dataset.values, rows.field);
|
|
151
|
+
const observedColumns = readNominalField(dataset.values, columns.field);
|
|
152
|
+
const rowValues = resolveValues(
|
|
153
|
+
uniqueInOrder(observedRows), rows.values, "facetGrid rows.values"
|
|
154
|
+
);
|
|
155
|
+
const columnValues = resolveValues(
|
|
156
|
+
uniqueInOrder(observedColumns), columns.values, "facetGrid columns.values"
|
|
157
|
+
);
|
|
158
|
+
const observedPairs = new Set(dataset.values.map((_, index) =>
|
|
159
|
+
pairKey(observedRows[index], observedColumns[index])
|
|
160
|
+
));
|
|
161
|
+
const candidates = rowValues.flatMap((rowValue, row) =>
|
|
162
|
+
columnValues.map((columnValue, column) => ({
|
|
163
|
+
row,
|
|
164
|
+
column,
|
|
165
|
+
rowValue,
|
|
166
|
+
columnValue,
|
|
167
|
+
empty: !observedPairs.has(pairKey(rowValue, columnValue))
|
|
168
|
+
}))
|
|
169
|
+
);
|
|
170
|
+
const selected = combinations === "full"
|
|
171
|
+
? candidates
|
|
172
|
+
: candidates.filter(cell => !cell.empty);
|
|
173
|
+
if (selected.length === 0) {
|
|
174
|
+
throw new Error("facetGrid has no observed row and column combinations.");
|
|
175
|
+
}
|
|
176
|
+
validateGeneratedItemLimit(
|
|
177
|
+
selected.length,
|
|
178
|
+
"Facet grid child count",
|
|
179
|
+
MAX_FACET_CHILDREN
|
|
180
|
+
);
|
|
181
|
+
validateWorkLimit(
|
|
182
|
+
dataset.values.length * selected.length,
|
|
183
|
+
"Facet grid partition work"
|
|
184
|
+
);
|
|
185
|
+
return cloneAndFreeze({
|
|
186
|
+
id,
|
|
187
|
+
family: family.family,
|
|
188
|
+
data,
|
|
189
|
+
dependencies,
|
|
190
|
+
grid: {
|
|
191
|
+
rows: { field: rows.field, values: rowValues },
|
|
192
|
+
columns: { field: columns.field, values: columnValues },
|
|
193
|
+
combinations
|
|
194
|
+
},
|
|
195
|
+
cells: selected.map(cell => {
|
|
196
|
+
const cellId = `${id}-row-${cell.row + 1}-column-${cell.column + 1}`;
|
|
197
|
+
return {
|
|
198
|
+
...cell,
|
|
199
|
+
id: cellId,
|
|
200
|
+
data: `${cellId}-data`,
|
|
201
|
+
value: `${displayValue(cell.rowValue)} · ${displayValue(cell.columnValue)}`,
|
|
202
|
+
filters: [
|
|
203
|
+
{ field: rows.field, value: cell.rowValue },
|
|
204
|
+
{ field: columns.field, value: cell.columnValue }
|
|
205
|
+
]
|
|
206
|
+
};
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
FACET_SCALE_CHANNELS,
|
|
4
4
|
FACET_SCALE_RESOLUTIONS
|
|
5
5
|
} from "../../core/vocabulary.js";
|
|
6
|
+
import { collectFacetScaleBindings } from "./dependencies.js";
|
|
6
7
|
import { resolveHistogramBins } from "../histogram.js";
|
|
7
8
|
import {
|
|
8
9
|
hasOrdinalDomain,
|
|
@@ -63,30 +64,25 @@ function normalizeRequestedPolicies(requested) {
|
|
|
63
64
|
|
|
64
65
|
function usedScaleChannels(semanticSpec, scales) {
|
|
65
66
|
const used = new Map();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
`Facet layer "${layer.id}" references missing scale "${id}".`
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
if (!used.has(id)) used.set(id, new Set());
|
|
76
|
-
used.get(id).add(channel);
|
|
67
|
+
const bindings = collectFacetScaleBindings(semanticSpec);
|
|
68
|
+
for (const binding of bindings) {
|
|
69
|
+
if (!scales.has(binding.scaleId)) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Facet layer "${binding.layerId}" references missing scale "${binding.scaleId}".`
|
|
72
|
+
);
|
|
77
73
|
}
|
|
74
|
+
if (!used.has(binding.scaleId)) used.set(binding.scaleId, new Set());
|
|
75
|
+
used.get(binding.scaleId).add(binding.policyKey);
|
|
78
76
|
}
|
|
79
|
-
return used;
|
|
77
|
+
return { used, bindings };
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
export function normalizeFacetScalePolicies(semanticSpec, requested = {}) {
|
|
83
81
|
const scales = requireScaleDefinitions(semanticSpec);
|
|
84
82
|
const channels = normalizeRequestedPolicies(requested);
|
|
85
|
-
const used = usedScaleChannels(semanticSpec, scales);
|
|
83
|
+
const { used, bindings } = usedScaleChannels(semanticSpec, scales);
|
|
86
84
|
for (const channel of Object.keys(requested)) {
|
|
87
|
-
const applicable =
|
|
88
|
-
layer => layer.encoding?.[channel]?.scale !== undefined
|
|
89
|
-
);
|
|
85
|
+
const applicable = bindings.some(binding => binding.policyKey === channel);
|
|
90
86
|
if (!applicable) {
|
|
91
87
|
throw new Error(
|
|
92
88
|
`Facet scale channel "${channel}" is not used by an affected layer.`
|
|
@@ -107,7 +103,7 @@ export function normalizeFacetScalePolicies(semanticSpec, requested = {}) {
|
|
|
107
103
|
channels: attached
|
|
108
104
|
};
|
|
109
105
|
}
|
|
110
|
-
return cloneAndFreeze({ channels, scales: scalePolicies });
|
|
106
|
+
return cloneAndFreeze({ channels, scales: scalePolicies, bindings });
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
function requireChildDomains(resolvedByChild, scaleId) {
|
|
@@ -210,7 +206,8 @@ export function resolveFacetScaleDomains(
|
|
|
210
206
|
}
|
|
211
207
|
return cloneAndFreeze({
|
|
212
208
|
channels: normalized.channels,
|
|
213
|
-
scales: resolved
|
|
209
|
+
scales: resolved,
|
|
210
|
+
bindings: normalized.bindings
|
|
214
211
|
});
|
|
215
212
|
}
|
|
216
213
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const TRANSFORM_KEYS = Object.freeze(["type", "fields", "as"]);
|
|
4
|
+
const AS_KEYS = Object.freeze(["key", "value"]);
|
|
5
|
+
const MAX_FIELDS = 64;
|
|
6
|
+
const MAX_OUTPUT_ROWS = 10_000;
|
|
7
|
+
|
|
8
|
+
function rejectUnknownKeys(value, supported, label) {
|
|
9
|
+
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
10
|
+
if (unknown !== undefined) {
|
|
11
|
+
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requireField(value, label) {
|
|
16
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
17
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeAs(value) {
|
|
23
|
+
const requested = value ?? {};
|
|
24
|
+
if (!isPlainObject(requested)) return requested;
|
|
25
|
+
return {
|
|
26
|
+
...requested,
|
|
27
|
+
key: requested.key ?? "key",
|
|
28
|
+
value: requested.value ?? "value"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeFoldTransform({ fields, as } = {}) {
|
|
33
|
+
const transform = {
|
|
34
|
+
type: "fold",
|
|
35
|
+
fields: Array.isArray(fields) ? [...fields] : fields,
|
|
36
|
+
as: normalizeAs(as)
|
|
37
|
+
};
|
|
38
|
+
validateFoldTransform(transform);
|
|
39
|
+
return cloneAndFreeze(transform);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateFoldTransform(transform) {
|
|
43
|
+
if (!isPlainObject(transform)) {
|
|
44
|
+
throw new TypeError("Fold transform must be a plain object.");
|
|
45
|
+
}
|
|
46
|
+
rejectUnknownKeys(transform, TRANSFORM_KEYS, "fold transform");
|
|
47
|
+
if (transform.type !== "fold") {
|
|
48
|
+
throw new Error(`Unsupported fold transform "${transform.type}".`);
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(transform.fields) || transform.fields.length === 0) {
|
|
51
|
+
throw new TypeError("Fold fields must be a non-empty array.");
|
|
52
|
+
}
|
|
53
|
+
if (transform.fields.length > MAX_FIELDS) {
|
|
54
|
+
throw new RangeError(`Fold fields cannot contain more than ${MAX_FIELDS} entries.`);
|
|
55
|
+
}
|
|
56
|
+
transform.fields.forEach((field, index) => {
|
|
57
|
+
requireField(field, `Fold fields[${index}]`);
|
|
58
|
+
});
|
|
59
|
+
if (new Set(transform.fields).size !== transform.fields.length) {
|
|
60
|
+
throw new Error("Fold fields must be unique.");
|
|
61
|
+
}
|
|
62
|
+
if (!isPlainObject(transform.as)) {
|
|
63
|
+
throw new TypeError("Fold as must be a plain object.");
|
|
64
|
+
}
|
|
65
|
+
rejectUnknownKeys(transform.as, AS_KEYS, "fold as");
|
|
66
|
+
const key = requireField(transform.as.key, "Fold as.key");
|
|
67
|
+
const value = requireField(transform.as.value, "Fold as.value");
|
|
68
|
+
if (key === value) {
|
|
69
|
+
throw new Error("Fold output fields must be unique.");
|
|
70
|
+
}
|
|
71
|
+
return transform;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function valueType(value, field, row) {
|
|
75
|
+
if (value === null || value === undefined) {
|
|
76
|
+
throw new TypeError(`Fold field "${field}" is missing at row ${row}.`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof value === "number") {
|
|
79
|
+
if (!Number.isFinite(value)) {
|
|
80
|
+
throw new TypeError(`Fold field "${field}" must contain finite numbers.`);
|
|
81
|
+
}
|
|
82
|
+
return "number";
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
85
|
+
return typeof value;
|
|
86
|
+
}
|
|
87
|
+
throw new TypeError(
|
|
88
|
+
`Fold field "${field}" must contain finite numbers, strings, or booleans.`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function deriveFoldRows(rows, transform) {
|
|
93
|
+
validateFoldTransform(transform);
|
|
94
|
+
if (rows.length * transform.fields.length > MAX_OUTPUT_ROWS) {
|
|
95
|
+
throw new RangeError(`Fold output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
|
|
96
|
+
}
|
|
97
|
+
const sourceFields = new Set(rows.flatMap(row => Object.keys(row)));
|
|
98
|
+
for (const output of [transform.as.key, transform.as.value]) {
|
|
99
|
+
if (sourceFields.has(output)) {
|
|
100
|
+
throw new Error(`Fold output field "${output}" already exists.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
let expectedType;
|
|
104
|
+
const output = [];
|
|
105
|
+
rows.forEach((row, rowIndex) => {
|
|
106
|
+
for (const field of transform.fields) {
|
|
107
|
+
if (!Object.hasOwn(row, field)) {
|
|
108
|
+
throw new Error(`Fold source does not contain field "${field}" at row ${rowIndex}.`);
|
|
109
|
+
}
|
|
110
|
+
const type = valueType(row[field], field, rowIndex);
|
|
111
|
+
if (expectedType === undefined) expectedType = type;
|
|
112
|
+
else if (type !== expectedType) {
|
|
113
|
+
throw new TypeError("Fold fields must contain one common primitive type.");
|
|
114
|
+
}
|
|
115
|
+
output.push({
|
|
116
|
+
...row,
|
|
117
|
+
[transform.as.key]: field,
|
|
118
|
+
[transform.as.value]: row[field]
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return output;
|
|
123
|
+
}
|
package/src/grammar/horizon.js
CHANGED
|
@@ -8,14 +8,15 @@ import { interpolateNumber, inverseLerp } from "./numeric.js";
|
|
|
8
8
|
import { requireFiniteResult } from "./numeric.js";
|
|
9
9
|
import {
|
|
10
10
|
isNominalValue,
|
|
11
|
-
normalizeTemporalValue
|
|
11
|
+
normalizeTemporalValue,
|
|
12
|
+
resolveTemporalUnit
|
|
12
13
|
} from "./scales/index.js";
|
|
13
14
|
|
|
14
15
|
export const HORIZON_RESOLUTIONS = Object.freeze(["shared", "independent"]);
|
|
15
16
|
export const HORIZON_MISSING_POLICIES = Object.freeze(["break", "error"]);
|
|
16
17
|
export const HORIZON_OVERFLOW_POLICIES = Object.freeze(["clip", "error"]);
|
|
17
18
|
|
|
18
|
-
const ENCODING_PROPERTIES = Object.freeze(["field", "fieldType"]);
|
|
19
|
+
const ENCODING_PROPERTIES = Object.freeze(["field", "fieldType", "temporalUnit"]);
|
|
19
20
|
const OUTPUT_PROPERTIES = Object.freeze([
|
|
20
21
|
"x", "lower", "upper", "group", "color", "sign", "band", "segment"
|
|
21
22
|
]);
|
|
@@ -41,7 +42,8 @@ function validateEncoding(value, role) {
|
|
|
41
42
|
`Horizon ${role} fieldType must be quantitative${supportsTemporal ? " or temporal" : ""}.`
|
|
42
43
|
);
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
const temporalUnit = resolveTemporalUnit(value, fieldType);
|
|
46
|
+
return { field, fieldType, ...(temporalUnit === undefined ? {} : { temporalUnit }) };
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function validateOutputFields(value) {
|
|
@@ -183,7 +185,7 @@ export function validateHorizonTransform(transform) {
|
|
|
183
185
|
|
|
184
186
|
function orderedX(value, encoding, rowIndex) {
|
|
185
187
|
if (encoding.fieldType === "temporal") {
|
|
186
|
-
return normalizeTemporalValue(value, encoding.field, rowIndex);
|
|
188
|
+
return normalizeTemporalValue(value, encoding.field, rowIndex, encoding.temporalUnit);
|
|
187
189
|
}
|
|
188
190
|
if (!Number.isFinite(value)) {
|
|
189
191
|
throw new TypeError(
|