ggaction 0.0.24 → 0.0.26
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 +47 -0
- package/README.md +2 -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-resolver.js +1 -1
- package/package.json +1 -1
- package/src/accessibility.js +13 -0
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/fitting.js +68 -13
- package/src/actions/charts/beeswarm.js +2 -1
- package/src/actions/charts/ecdf.js +2 -1
- package/src/actions/charts/endpoints.js +25 -16
- package/src/actions/charts/heatmap.js +5 -3
- package/src/actions/charts/missing.js +17 -0
- package/src/actions/charts/radar.js +70 -33
- package/src/actions/charts/raincloud.js +18 -4
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/shared.js +17 -0
- package/src/actions/composition/actions.js +24 -7
- package/src/actions/composition/adjacent.js +90 -0
- package/src/actions/composition/index.js +11 -1
- package/src/actions/composition/layer.js +42 -0
- package/src/actions/composition/layerPlan.js +176 -0
- package/src/actions/composition/targets.js +105 -0
- package/src/actions/data/derived.js +3 -0
- package/src/actions/data/filter.js +1 -1
- package/src/actions/data/raincloudFilter.js +3 -1
- package/src/actions/encodings/appearance.js +26 -7
- package/src/actions/encodings/channels.js +1 -0
- package/src/actions/encodings/color/continuous.js +37 -34
- package/src/actions/encodings/color/index.js +57 -43
- package/src/actions/encodings/color/policy.js +15 -0
- package/src/actions/encodings/density/resolve.js +3 -2
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/offset.js +2 -0
- package/src/actions/encodings/pathOrder.js +2 -0
- package/src/actions/encodings/position/areaAlignment.js +42 -0
- package/src/actions/encodings/position/index.js +37 -3
- package/src/actions/encodings/position/policies/line.js +1 -1
- package/src/actions/encodings/ranged.js +2 -0
- package/src/actions/encodings/remove.js +13 -10
- package/src/actions/encodings/ruleAppearance.js +14 -2
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/guides/axes/axes.js +2 -1
- package/src/actions/guides/axes/edit.js +1 -1
- package/src/actions/guides/axes/labels.js +2 -1
- package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
- package/src/actions/guides/axes/parallel/policy.js +5 -2
- package/src/actions/guides/axes/parallel/resolve.js +3 -1
- package/src/actions/guides/axes/parallel.js +9 -4
- package/src/actions/guides/axes/titles.js +1 -1
- package/src/actions/guides/facadeLegend.js +3 -2
- package/src/actions/guides/grids/grid.js +3 -2
- package/src/actions/guides/grids/resolve.js +2 -1
- package/src/actions/guides/guides.js +3 -2
- package/src/actions/guides/legends/automatic.js +28 -0
- package/src/actions/guides/legends/blocks.js +15 -21
- package/src/actions/guides/legends/categorical/actions.js +1 -1
- package/src/actions/guides/legends/categorical/components.js +4 -3
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +9 -14
- package/src/actions/guides/legends/categorical/recipes.js +3 -0
- package/src/actions/guides/legends/categorical/resolve.js +7 -5
- package/src/actions/guides/legends/colorSize.js +51 -14
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -2
- package/src/actions/guides/legends/remove.js +18 -4
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/transition.js +15 -6
- package/src/actions/guides/polar/axes/facade.js +4 -2
- package/src/actions/guides/polar/axes/labels.js +6 -1
- package/src/actions/guides/polar/axes/shared.js +4 -4
- package/src/actions/guides/polar/axes/titles.js +13 -2
- package/src/actions/guides/polar/grids.js +6 -3
- package/src/actions/guides/tickValues.js +7 -1
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +9 -2
- package/src/actions/marks/area/materialize.js +1 -1
- package/src/actions/marks/bar/materialize.js +4 -4
- package/src/actions/marks/lifecycle.js +14 -0
- package/src/actions/marks/line/actions.js +3 -0
- package/src/actions/marks/line/materialize.js +4 -2
- package/src/actions/marks/point/edit.js +2 -0
- package/src/actions/marks/point/jitter.js +2 -0
- package/src/actions/marks/point/materialize.js +5 -4
- package/src/actions/marks/point/packing.js +2 -0
- package/src/actions/marks/references.js +5 -3
- package/src/actions/marks/remove.js +16 -0
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +5 -2
- package/src/actions/overlays/index.js +168 -0
- package/src/actions/overlays/snapshot.js +45 -0
- package/src/actions/primitives/editGraphics.js +21 -27
- package/src/actions/primitives/semanticAction.js +4 -5
- package/src/actions/primitives/semanticValidation/guide.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +7 -0
- package/src/actions/scales/consumers/common.js +10 -0
- package/src/actions/scales/consumers/families.js +1 -1
- package/src/actions/scales/preview.js +1 -1
- package/src/actions/selection/actions.js +63 -11
- package/src/actions/titles/actions.js +3 -14
- package/src/actions/titles/resolve.js +12 -9
- package/src/core/action.js +41 -25
- package/src/core/compositionScales.js +12 -0
- package/src/core/compositionState.js +16 -2
- package/src/core/immutable.js +11 -6
- package/src/core/programState.js +16 -0
- package/src/core/resourceReferences.js +19 -3
- package/src/core/validation.js +7 -0
- package/src/core/vocabulary.js +8 -2
- package/src/grammar/actionTargets.js +24 -0
- package/src/grammar/bin2d.js +7 -0
- package/src/grammar/colorSizeLegend.js +56 -5
- package/src/grammar/displayLabels.js +10 -0
- package/src/grammar/facetHeaders.js +1 -1
- package/src/grammar/facets/dependencies.js +10 -3
- package/src/grammar/facets/guides.js +2 -1
- package/src/grammar/facets/legendIntent.js +7 -5
- package/src/grammar/legendBlocks.js +1 -1
- package/src/grammar/legendCombinations.js +1 -1
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendItemsOffset.js +12 -0
- package/src/grammar/legendOverflow.js +25 -0
- package/src/grammar/lineSeries.js +1 -1
- package/src/grammar/markSelection.js +23 -0
- package/src/grammar/overlay.js +51 -0
- package/src/grammar/parallelCoordinates.js +1 -208
- package/src/grammar/pathCommands.js +8 -2
- package/src/grammar/pathSeries.js +1 -1
- package/src/grammar/plotComposition.js +152 -0
- package/src/grammar/pointShapes.js +18 -0
- package/src/grammar/scales/transformed.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +8 -1
- package/src/grammar/schemas/graphicBounds.js +8 -1
- package/src/grammar/schemas/graphicTree.js +18 -2
- package/src/grammar/schemas/semanticPath.js +3 -0
- package/src/grammar/strokeStyle.js +14 -8
- package/src/grammar/text.js +72 -0
- package/src/inspection.js +1 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/composition.js +30 -4
- package/src/layout/compositionLegend.js +91 -0
- package/src/layout/facetHeaderTitles.js +1 -1
- package/src/layout/labels.js +55 -27
- package/src/layout/legend.js +3 -2
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +7 -6
- package/src/layout/legendLane.js +1 -1
- package/src/layout/legendText.js +1 -1
- package/src/materialization/adjacentSource.js +27 -0
- package/src/materialization/composition.js +165 -52
- package/src/materialization/compositionLegends.js +120 -0
- package/src/materialization/compositionScales.js +125 -0
- package/src/materialization/compositionTitle.js +96 -0
- package/src/materialization/facetGuides/edit.js +22 -2
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +4 -3
- package/src/materialization/facetGuides/ownerLayout.js +12 -6
- package/src/materialization/facetGuides/placement.js +19 -22
- package/src/materialization/facetGuides/preparation.js +17 -5
- package/src/materialization/facetHeaders.js +5 -1
- package/src/materialization/facets.js +1 -1
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/guides/visibility.js +105 -0
- package/src/materialization/legendClip.js +1 -1
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legendItemsOffset.js +68 -0
- package/src/materialization/legendSuppression.js +22 -0
- package/src/materialization/legends.js +3 -2
- package/src/materialization/marks/capabilities.js +4 -0
- package/src/materialization/marks/index.js +5 -1
- package/src/materialization/marks/pathOrder.js +1 -1
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/overlayItems.js +154 -0
- package/src/materialization/scales/policies/binnedPosition.js +43 -5
- package/src/materialization/scales/policies/series.js +12 -4
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/bar.js +21 -3
- package/src/materialization/selection/items/common.js +18 -5
- package/src/materialization/selection/items/overlay.js +15 -0
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/items/point.js +2 -3
- package/src/materialization/selection/items/tick.js +3 -12
- package/src/materialization/selection/path.js +2 -2
- package/src/materialization/selection/policies/bar.js +2 -0
- package/src/materialization/selection/policies/index.js +35 -1
- package/src/materialization/selection/policies/point.js +2 -0
- package/src/materialization/selection/policies/text.js +17 -0
- package/src/materialization/selection/rows.js +103 -0
- package/src/materialization/selection/state.js +19 -3
- package/src/materialization/selection/styles.js +8 -0
- package/src/materialization/text.js +6 -489
- package/src/materialization/textGeometry.js +503 -0
- package/src/materialization/textLines.js +20 -0
- package/src/persistence/validation.js +97 -24
- package/src/renderers/canvas/index.js +12 -7
- package/src/renderers/canvas/text.js +5 -5
- package/src/renderers/nativeBackend.js +25 -0
- package/src/renderers/pdf.js +3 -3
- package/src/renderers/png.js +4 -3
- package/src/renderers/svgSerialization.js +1 -643
- package/src/scene.js +5 -3
- package/src/selectors/plotComposition.js +115 -0
- package/src/selectors/selectionTargets.js +19 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -2
- package/types/index.d.ts +11 -0
- package/types/inspection.d.ts +34 -0
- package/types/program.d.ts +553 -365
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { patchLegendItemsOffset } from "../legendItemsOffset.js";
|
|
2
3
|
import {validateLegendSpacing} from "../legendSpacing.js";
|
|
3
4
|
import { validateOptionObject, validateNonEmptyString } from "../../core/validation.js";
|
|
4
5
|
import { validateUserId } from "../../core/identifiers.js";
|
|
@@ -8,12 +9,12 @@ import { validateLegendGroupState } from "../legendGroups.js";
|
|
|
8
9
|
import { resolveSharedFacetLegends, sameFacetGuideValue as same } from "./guides.js";
|
|
9
10
|
import { normalizeDisplayLabelMap } from "../displayLabels.js";
|
|
10
11
|
|
|
11
|
-
const CONFIG_KEYS = Object.freeze([
|
|
12
|
+
const CONFIG_KEYS = Object.freeze(["visibility",
|
|
12
13
|
"kind", "target", "channels", "scales", "scale", "field", "fieldType", "domain", "title", "titleVisible",
|
|
13
14
|
"inferredTitle", "inferredSymbol", "inferredStrokeWidth", "inferredOffset", "position", "align", "offset", "direction", "columns",
|
|
14
15
|
"titlePosition", "symbol", "labels", "titleStyle", "itemGap", "rowPadding", "titlePadding", "layout", "overflow", "border", "gradient",
|
|
15
16
|
"count", "sampling", "representation", "gradientDirection", "inheritAppearance", "inheritPosition", "blockOverrides",
|
|
16
|
-
"combine", "sizeAreas", "clipHeight", "defaultLabelMap"
|
|
17
|
+
"combine", "sizeAreas", "clipHeight", "defaultLabelMap", "itemsOffset"
|
|
17
18
|
]);
|
|
18
19
|
|
|
19
20
|
export function isAuthoredFacetLegend(value) {
|
|
@@ -31,9 +32,10 @@ export function validateAuthoredFacetLegend(program, intent) {
|
|
|
31
32
|
for (const [kind, block] of Object.entries(intent.blocks)) {
|
|
32
33
|
if (!LEGEND_CONFIG_KINDS.includes(kind)) throw new Error("Stored parent legend has an unknown block kind.");
|
|
33
34
|
validateOptionObject(block, ["config", "guide"], "Stored parent legend block");
|
|
34
|
-
validateOptionObject(block.config, kind === "colorSize" ? [...CONFIG_KEYS, "colorScale", "sizeScale"] : CONFIG_KEYS, "Stored parent legend config");
|
|
35
|
+
validateOptionObject(block.config, kind === "colorSize" ? [...CONFIG_KEYS, "colorScale", "sizeScale", "shapeScale", "colorChannel"] : CONFIG_KEYS, "Stored parent legend config");
|
|
35
36
|
const config = block.config;
|
|
36
37
|
validateLegendSpacing(kind,config);
|
|
38
|
+
if (config.itemsOffset !== undefined) patchLegendItemsOffset({}, config.itemsOffset);
|
|
37
39
|
if (config.defaultLabelMap !== undefined) normalizeDisplayLabelMap(config.defaultLabelMap, "Stored inferred legend labels");
|
|
38
40
|
validateUserId(config.target, "Parent legend target");
|
|
39
41
|
if (!LEGEND_POSITIONS.includes(config.position)) throw new Error("Stored parent legend requires a native position.");
|
|
@@ -42,9 +44,9 @@ export function validateAuthoredFacetLegend(program, intent) {
|
|
|
42
44
|
}
|
|
43
45
|
const descriptor = describeLegendBlock(program, kind, config);
|
|
44
46
|
if (block.guide !== undefined) {
|
|
45
|
-
validateOptionObject(block.guide, kind === "colorSize" ? ["colorScale", "sizeScale", "title"]
|
|
47
|
+
validateOptionObject(block.guide, kind === "colorSize" ? ["colorScale", "sizeScale", "shapeScale", "colorChannel", "title"]
|
|
46
48
|
: ["scale", "scales", "channels", "title", "order"], "Stored parent legend meaning");
|
|
47
|
-
if (kind === "colorSize" && (block.guide.colorScale !== config.colorScale || block.guide.sizeScale !== config.sizeScale)) {
|
|
49
|
+
if (kind === "colorSize" && (block.guide.colorScale !== config.colorScale || block.guide.sizeScale !== config.sizeScale || block.guide.shapeScale !== config.shapeScale || block.guide.colorChannel !== config.colorChannel)) {
|
|
48
50
|
throw new Error("Stored parent color-size meaning must match both block scale bindings.");
|
|
49
51
|
}
|
|
50
52
|
if (block.guide.title !== undefined) validateNonEmptyString(block.guide.title, "Parent legend title");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{colorSizeChannels as d,colorSizeScaleIds as h}from"./colorSizeLegend.js";import{LEGEND_CHANNELS as u,LEGEND_CONFIG_KINDS as y}from"../core/vocabulary.js";import{isEnumeratedSizeScaleType as p}from"./scales/size.js";function w(r,e){if(!Array.isArray(r))throw new TypeError(`${e} channels must be an array.`);if(r.length===0)throw new Error(`${e} channels must select at least one channel.`);const t=new Set;for(const s of r){if(!u.includes(s))throw new Error(`Unsupported legend channel "${s}".`);if(t.has(s))throw new Error(`${e} channels contains duplicate "${s}".`);t.add(s)}}const o=Object.freeze(["series","color","stroke"]),a=Object.freeze({size:"size",opacity:"opacity",strokeWidth:"strokeWidth",gradient:"color",interval:"color",strokeGradient:"stroke",strokeInterval:"stroke"}),c=(r,e)=>r<e?-1:r>e?1:0;function S(r,e){const t=r==="colorSize"?d(e):o.includes(r)?e?.channels:a[r]===void 0?void 0:[a[r]];if(!Array.isArray(t)||t.length===0)throw new Error(`Legend block ${r} requires at least one channel.`);return w(t,`Legend block ${r}`),Object.freeze([...t].sort(c))}function m(r){if(!Array.isArray(r)||r.length===0)throw new TypeError("Legend block key requires a non-empty channel array.");return JSON.stringify([...r].sort(c))}function E(r,e,t){if(!y.includes(e))throw new Error(`Unknown legend block kind "${e}".`);const s=S(e,t),n=e==="colorSize"?h(t):o.includes(e)?t?.scales:[t?.scale];if(!Array.isArray(n)||n.length===0||n.some(l=>typeof l!="string"||r.resolvedScales[l]===void 0))throw new Error(`Legend block ${e} requires resolved scales.`);const i=o.includes(e)?"categorical":e==="colorSize"?"sampled":e==="size"?p(r.resolvedScales[n[0]].type)?"discrete-size":"sampled":["opacity","strokeWidth"].includes(e)||e==="gradient"&&t.representation==="symbols"?"sampled":["gradient","strokeGradient"].includes(e)?"gradient":"interval";return Object.freeze({target:t.target,kind:e,family:i,key:m(s),channels:s,scaleIds:Object.freeze([...n]),config:t})}function L(r,e){return r.symbol.layers.length===1&&(r.kind==="series"&&e==="line"||["color","stroke"].includes(r.kind)&&e==="swatch")?`${r.kind}LegendSymbols`:`${r.kind}LegendSymbol${{line:"Lines",point:"Points",swatch:"Swatches"}[e]}`}export{L as categoricalLegendSymbolId,E as describeLegendBlock,S as legendBlockChannels,m as legendBlockKey,w as validateLegendChannels};
|
|
@@ -9,7 +9,7 @@ export function isSizeShapeChannels(channels) {
|
|
|
9
9
|
}
|
|
10
10
|
export function validateLegendCombine(value) {
|
|
11
11
|
if(value===undefined)return;
|
|
12
|
-
if (!['separate','size-shape','color-size'].includes(value)) throw new Error('Legend combine must be separate, size-shape, or color-size.');
|
|
12
|
+
if (!['separate','size-shape','color-size','color-size-shape'].includes(value)) throw new Error('Legend combine must be separate, size-shape, color-size, or color-size-shape.');
|
|
13
13
|
}
|
|
14
14
|
export function validateMappedSizeShapeSymbol(symbol) {
|
|
15
15
|
if(symbol===undefined||symbol==='auto')return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateOptionObject as o,validateNonNegativeFinite as
|
|
1
|
+
import{validateOptionObject as o,validateNonNegativeFinite as l}from"../core/validation.js";import{validateUserId as d}from"../core/identifiers.js";import{PLOT_LEGEND_POSITIONS as i,LEGEND_POSITIONS as f,LEGEND_BLOCK_LAYOUTS as a}from"../core/vocabulary.js";function c(e){o(e,void 0,"Legend groups");for(const[r,n]of Object.entries(e)){if(d(r,"Legend group target"),o(n,["placement","blockLayout","inferredOffset"],"Legend group"),!a.includes(n.blockLayout))throw new Error("Legend group blockLayout must be row, column or wrap.");if(n.inferredOffset!==void 0&&typeof n.inferredOffset!="boolean")throw new TypeError("Legend group inferredOffset must be boolean.");const t=n.placement;if(o(t,["at","position","align","offset","alongOffset"],"Legend group placement"),Object.hasOwn(t,"at")){if(Object.keys(t).length!==1)throw new Error("Legend group at cannot coexist with an edge anchor.");if(o(t.at,["x","y"],"Legend group at"),!Number.isFinite(t.at.x)||!Number.isFinite(t.at.y))throw new Error("Legend group at requires finite x and y.")}else{if(![3,4].includes(Object.keys(t).length)||!f.includes(t.position))throw new Error("Legend group requires a complete edge placement.");if(!["left","center","right"].includes(t.align))throw new Error("Unsupported legend group alignment.");if((i.includes(t.position)||["left","right"].includes(t.position))&&t.align!=="center")throw new Error("Side and in-plot legend groups require center alignment.");if(l(t.offset,"Legend group offset"),t.alongOffset!==void 0&&!Number.isFinite(t.alongOffset))throw new TypeError("Legend alongOffset must be finite.")}}return e}function s(e){for(const n of Object.values(e?.blockOverrides??{}))if(n.alongOffset!==void 0&&!Number.isFinite(n.alongOffset))throw new TypeError("Legend alongOffset must be finite.");const r=Object.values(e?.blockOverrides??{}).map(n=>n.position??(n.alongOffset===void 0?void 0:e.position)).filter(n=>n!==void 0);if(r.length>1)throw new Error("Legend family has ambiguous local positions.");if(r.length&&!f.includes(r[0]))throw new Error("Invalid local legend position.");return r[0]}function O(e){const r=s(e);if(r===void 0)return;const n=Object.values(e.blockOverrides??{}).flatMap(t=>t.alongOffset===void 0?[]:[t.alongOffset]);if(n.length>1||n.some(t=>!Number.isFinite(t)))throw new TypeError("Legend alongOffset requires one finite block value.");return{position:r,...n.length?{alongOffset:n[0]}:{},align:["top","bottom"].includes(r)?e.align??"center":"center",offset:i.includes(r)&&r!==e.position&&e.inferredOffset===!0?8:e.offset??(i.includes(r)?8:e.channels===void 0?30:8),inferredOffset:e.inferredOffset??!0}}function m(e,r=e.position){return s(e)===r&&Object.values(e.blockOverrides??{}).some(n=>n.alongOffset!==void 0)}export{O as localLegendPlacement,s as localLegendPosition,m as retainsLegendSlot,c as validateLegendGroupState};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export function patchLegendItemsOffset(config, value) {
|
|
4
|
+
const { itemsOffset: previous, ...rest } = config;
|
|
5
|
+
if (value === "auto") return rest;
|
|
6
|
+
if (!isPlainObject(value) || !Object.keys(value).length ||
|
|
7
|
+
Object.keys(value).some(key => !["x", "y"].includes(key) ||
|
|
8
|
+
typeof value[key] !== "number" || !Number.isFinite(value[key]))) {
|
|
9
|
+
throw new TypeError('Legend itemsOffset must be a nonempty object with finite x/y coordinates, or "auto".');
|
|
10
|
+
}
|
|
11
|
+
return { ...rest, itemsOffset: { x: 0, y: 0, ...previous, ...value } };
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { validateOptionObject } from "../core/validation.js";
|
|
2
|
+
|
|
3
|
+
export function normalizeLegendOverflow(value, { summarySymbol = false } = {}) {
|
|
4
|
+
if (value === undefined || value === false) return value;
|
|
5
|
+
validateOptionObject(value, ["maxItems", "summary", "summarySymbol"], "Legend overflow");
|
|
6
|
+
if (!Number.isInteger(value.maxItems) || value.maxItems < 1) {
|
|
7
|
+
throw new RangeError("Legend overflow.maxItems must be a positive integer.");
|
|
8
|
+
}
|
|
9
|
+
if (value.summary !== undefined && value.summary !== "ellipsis-count") {
|
|
10
|
+
throw new Error("Legend overflow.summary must be ellipsis-count.");
|
|
11
|
+
}
|
|
12
|
+
if (value.summarySymbol !== undefined && (!summarySymbol || value.summarySymbol !== "first-hidden")) {
|
|
13
|
+
throw new Error("Legend overflow.summarySymbol requires first-hidden on categorical line samples.");
|
|
14
|
+
}
|
|
15
|
+
return { maxItems: value.maxItems, summary: "ellipsis-count",
|
|
16
|
+
...(value.summarySymbol === undefined ? {} : { summarySymbol: value.summarySymbol }) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function validateLegendOverflow(kind, config) {
|
|
20
|
+
normalizeLegendOverflow(config.overflow, { summarySymbol: ["series", "color", "stroke"].includes(kind) });
|
|
21
|
+
if (config.overflow?.summarySymbol !== undefined &&
|
|
22
|
+
(!config.symbol?.layers?.length || config.symbol.layers.some(layer => layer.type !== "line"))) {
|
|
23
|
+
throw new Error("Legend overflow.summarySymbol requires a categorical line-only sample recipe.");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cloneAndFreeze as
|
|
1
|
+
import{cloneAndFreeze as M}from"../core/immutable.js";import{readLinePosition as D,validateLineMissingScope as N,orderLineGapValues as X,splitLineGapValues as j}from"./lineMissing.js";import{readNominalField as H,readQuantitativeField as k,readTemporalField as P}from"./scales/index.js";import{aggregateRows as Q,isAggregate as R,validateAggregateFieldType as Y,validateAggregateFieldValues as _}from"./aggregate.js";import{stableOrderPathValues as J}from"./pathOrder.js";import{finiteMidpoint as W}from"./numeric.js";import{findHistogramBinIndex as Z,resolveHistogramBins as ee}from"./histogram.js";import{validatePathSeriesAppearance as A,pathSeriesKey as U}from"./pathSeries.js";function ne(e){return["quantitative","temporal","nominal","ordinal"].includes(e?.fieldType)}function ie(e){if(e?.mark?.type!=="line")throw new Error("Line series derivation requires a semantic line mark.");const n=e.encoding?.x,i=e.encoding?.y;if(n===void 0)throw new Error(`Line mark "${e.id}" requires an x encoding.`);if(n.aggregate!==void 0)throw new Error("A Line requires one independent position; both axes cannot aggregate.");const o=n?.fieldType==="quantitative"&&n.bin!==void 0&&R(i?.aggregate),t=ne(n)&&R(i?.aggregate),r=["quantitative","nominal","ordinal"].includes(n?.fieldType)&&n.bin===void 0&&i?.fieldType==="quantitative"&&i.aggregate===void 0,u=i?.aggregate===void 0&&(n?.fieldType==="temporal"&&i?.fieldType==="quantitative"||n?.fieldType==="quantitative"&&i?.fieldType==="temporal");if(!t&&!r&&!u)throw n?.fieldType==="temporal"?new Error(`Line mark "${e.id}" requires a supported aggregate y encoding.`):new Error(`Line mark "${e.id}" requires an aggregate value at compatible x positions or compatible direct x/y encodings.`);return t&&Y(i.aggregate,i.fieldType),{x:n,y:i,isAggregate:t,binnedAggregate:o,directRows:u||r,directTemporal:u}}function L(e,n,i){const o=U(i);let t=e.get(o);return t===void 0&&(t={key:Object.fromEntries(n.map((r,u)=>[r,i[u]])),values:[]},e.set(o,t)),t}function B(e,n){const i=e.flatMap(t=>t.values.map(r=>r.x)),o=new Set(i);if(n!==void 0){const t=new Set(n);if(i.some(r=>!t.has(r)))throw new Error("Line categorical x domain must contain every observed vertex.")}return M({xValues:n===void 0?i:n.filter(t=>o.has(t)),yValues:e.flatMap(t=>t.values.map(r=>r.y)),series:e})}function te(e,n,i={}){const o=n?.encoding?.x;if(!(n?.mark?.type!=="line"||o?.fieldType!=="quantitative"||o.bin===void 0))return ee({values:k(e,o.field),bin:o.bin,domain:i.domain??"auto",nice:i.nice??!0,zero:i.zero??!1})}function C(e,n,i={}){const{x:o,y:t,isAggregate:r,binnedAggregate:u,directRows:v,directTemporal:q}=ie(n);if(n.mark.missing==="break"){const a=D(e,o),d=D(e,t),f=A(e,n,i.scales),s=n.encoding?.pathOrder,g=s===void 0?q&&t.fieldType==="temporal"?d:a:k(e,s.field),m=new Map;for(let c=0;c<e.length;c+=1){const b=L(m,f,f.map(z=>e[c][z]));b.values.push({x:a[c],y:d[c],sourceIndex:c}),(b.orderValues??=[]).push(g[c])}const x=[...m.values()].flatMap(c=>{const b=X(c.values,c.orderValues,s?.order);return j(b).map(O=>({key:c.key,values:O.map(({sourceIndex:G,...K})=>K),sourceIndices:O.map(G=>G.sourceIndex)}))});return M({xValues:a.filter(c=>c!==null),yValues:d.filter(c=>c!==null),series:x})}r&&_(e,t.field,t.fieldType);const w=["nominal","ordinal"].includes(o.fieldType),h=w?H(e,o.field):o.fieldType==="temporal"?P(e,o.field,o.temporalUnit):k(e,o.field),l=r?void 0:t.fieldType==="temporal"?P(e,t.field,t.temporalUnit):k(e,t.field),p=w?i.xDomain??[...new Set(h)]:void 0,T=new Map(p?.map((a,d)=>[a,d])),y=w?(a,d)=>T.get(a.x)-T.get(d.x):(a,d)=>a.x-d.x,V=A(e,n,i.scales),S=u?i.xBinBoundaries??te(e,n).boundaries:void 0,$=n.encoding?.pathOrder;if($!==void 0){if(r)throw new Error(`Line mark "${n.id}" cannot combine aggregate y and path order.`);const a=k(e,$.field),d=new Map;for(let s=0;s<e.length;s+=1){const g=V.map(x=>e[s][x]),m=L(d,V,g);m.values.push({x:h[s],y:l[s]}),m.orderValues??=[],m.orderValues.push(a[s])}const f=[...d.values()].flatMap(s=>{const g=J(s.values,s.orderValues,$.order);return[{key:s.key,values:g}]});if(f.length===0)throw new Error(`Line series on mark "${n.id}" requires at least two ordered points.`);return B(f,p)}if(v){const a=new Map;for(let s=0;s<e.length;s+=1){const g=V.map(x=>e[s][x]);L(a,V,g).values.push({x:h[s],y:l[s]})}const d=q&&t.fieldType==="temporal"?"y":"x",f=[...a.values()].flatMap(s=>{const g=s.values.sort(d==="x"?y:(m,x)=>m.y-x.y);return[{key:s.key,values:g}]});if(f.length===0)throw new Error(`Line series on mark "${n.id}" requires at least two direct points.`);return B(f,p)}const E=new Map;for(let a=0;a<e.length;a+=1){const d=V.map(x=>e[a][x]),f=u?Z(h[a],S):void 0;if(f===-1)continue;const s=u?W(S[f],S[f+1]):h[a],g=U([u?f:s,...d]),m=E.get(g)??{x:s,dimensions:d,rows:[]};m.rows.push(e[a]),E.set(g,m)}if(E.size===0)throw new Error(`Line mark "${n.id}" has no values to aggregate.`);const I=new Map;for(const a of E.values()){const d=Q(a.rows,t.field,t.aggregate);if(d===void 0)continue;L(I,V,a.dimensions).values.push({x:a.x,y:d})}const F=[...I.values()].flatMap(a=>{const d=a.values.sort(y);return d.length<(i.allowSingleton===!0?1:2)?[]:[{key:a.key,values:d}]});if(F.length===0)throw new Error(`Line series on mark "${n.id}" requires at least two aggregate points.`);return B(F,p)}function re(e){if(e?.mark?.type!=="line")throw new Error("Polar line series derivation requires a semantic line mark.");const n=e.encoding?.theta,i=e.encoding?.radius;if(n===void 0||i===void 0)throw new Error(`Polar line mark "${e.id}" requires theta and radius encodings.`);if(!["quantitative","temporal","ordinal","nominal"].includes(n.fieldType))throw new Error(`Polar line mark "${e.id}" has unsupported theta field type.`);if(i.fieldType!=="quantitative")throw new Error(`Polar line mark "${e.id}" requires a quantitative radius encoding.`);return{theta:n,radius:i}}function ae(e,n){return["nominal","ordinal"].includes(n.fieldType)?H(e,n.field):n.fieldType==="temporal"?P(e,n.field,n.temporalUnit):k(e,n.field)}function oe(e,n,i){if(!["nominal","ordinal"].includes(n))return e;const o=i??[...new Set(e)],t=new Map(o.map((r,u)=>[r,u]));for(const r of e)if(!t.has(r))throw new Error(`Polar line theta domain does not contain "${r}".`);return e.map(r=>t.get(r))}function se(e,n,{thetaDomain:i}={}){if(n.encoding?.pathOrder!==void 0)throw new Error(`Polar line mark "${n.id}" does not support path order.`);const{theta:o,radius:t}=re(n),r=ae(e,o),u=k(e,t.field),v=oe(r,o.fieldType,i),q=A(e,n),w=new Map;for(let l=0;l<e.length;l+=1){const p=q.map(y=>e[l][y]);L(w,q,p).values.push({theta:r[l],radius:u[l],order:v[l],sourceIndex:l})}const h=[...w.values()].flatMap(l=>{const p=l.values.sort((T,y)=>T.order-y.order||T.sourceIndex-y.sourceIndex);return p.length<2?[]:[{key:l.key,values:p}]});if(h.length===0)throw new Error(`Polar line series on mark "${n.id}" requires at least two points.`);return M({thetaFieldType:o.fieldType,thetaValues:h.flatMap(l=>l.values.map(p=>p.theta)),radiusValues:h.flatMap(l=>l.values.map(p=>p.radius)),series:h})}function de(e){return e?.encoding?.x?.aggregate!==void 0||e?.encoding?.x?.fieldType==="quantitative"&&["nominal","ordinal"].includes(e?.encoding?.y?.fieldType)}function xe(e,n,i={}){if(N(n),n?.encoding?.theta!==void 0||n?.encoding?.radius!==void 0)return se(e,n,i);if(!de(n))return C(e,n,i);const t={...n,encoding:{...n.encoding,x:n.encoding.y,y:n.encoding.x}},r=C(e,t,{...i,xDomain:i.yDomain,xBinBoundaries:i.yBinBoundaries});return M({...r,xValues:r.yValues,yValues:r.xValues,series:r.series.map(u=>({...u,values:u.values.map(v=>({...v,x:v.y,y:v.x}))}))})}export{xe as deriveLineSeries,se as derivePolarLineSeries,ne as isAggregateLinePosition,de as isHorizontalLine,te as resolveLineBins};
|
|
@@ -43,6 +43,7 @@ const COMPARISON_OPERATORS = new Set([
|
|
|
43
43
|
const ORDERED_COMPARISON_OPERATORS = new Set(["gt", "gte", "lt", "lte"]);
|
|
44
44
|
const RANK_OPERATORS = new Set(["min", "max"]);
|
|
45
45
|
const SELECTOR_KEYS = Object.freeze([
|
|
46
|
+
"rows",
|
|
46
47
|
"grain",
|
|
47
48
|
"field",
|
|
48
49
|
"channel",
|
|
@@ -98,6 +99,16 @@ export function normalizeMarkSelector(selector) {
|
|
|
98
99
|
throw new Error(`Unknown mark selector option "${unknown}".`);
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
if (selector.rows !== undefined) {
|
|
103
|
+
if (Object.keys(selector).some(key => !["rows", "grain"].includes(key)) || !isPlainObject(selector.rows) ||
|
|
104
|
+
Object.keys(selector.rows).some(key => !["data", "indices"].includes(key)) ||
|
|
105
|
+
typeof selector.rows.data !== "string" || !selector.rows.data || !Array.isArray(selector.rows.indices) ||
|
|
106
|
+
!Array.from(selector.rows.indices).every(i => Number.isInteger(i) && i >= 0)) throw new TypeError("Row selection requires data and non-negative integer indices.");
|
|
107
|
+
const grain = selector.grain ?? "item";
|
|
108
|
+
if (!MARK_SELECTOR_GRAINS.includes(grain)) throw new Error(`Unknown mark selector grain "${grain}".`);
|
|
109
|
+
return cloneAndFreeze({ grain, rows: { data: selector.rows.data, indices: [...new Set(selector.rows.indices)] } });
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
const sources = ["field", "channel", "property"].filter(key =>
|
|
102
113
|
Object.hasOwn(selector, key)
|
|
103
114
|
);
|
|
@@ -298,6 +309,10 @@ function rankGroup(items, selector) {
|
|
|
298
309
|
export function selectMarkItemKeys(items, selector) {
|
|
299
310
|
validateItems(items);
|
|
300
311
|
const normalized = normalizeMarkSelector(selector);
|
|
312
|
+
if (normalized.rows) {
|
|
313
|
+
const indices = new Set(normalized.rows.indices);
|
|
314
|
+
return cloneAndFreeze(items.filter(item => item.rows?.some(ref => ref.data === normalized.rows.data && indices.has(ref.index))).map(item => item.key));
|
|
315
|
+
}
|
|
301
316
|
if (!RANK_OPERATORS.has(normalized.op)) {
|
|
302
317
|
return cloneAndFreeze(
|
|
303
318
|
items
|
|
@@ -324,3 +339,11 @@ export function selectMarkItemKeys(items, selector) {
|
|
|
324
339
|
)
|
|
325
340
|
);
|
|
326
341
|
}
|
|
342
|
+
|
|
343
|
+
// Stored selectors and live materialization share dataset identity and bounds checks.
|
|
344
|
+
export function validateRowSelectionDataset(rows, dataset) {
|
|
345
|
+
if (!dataset) throw new Error(`Unknown row dataset "${rows.data}".`);
|
|
346
|
+
if (rows.indices.some(index => index >= dataset.values.length)) {
|
|
347
|
+
throw new RangeError("Selected row index exceeds the dataset length.");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isPlainObject, cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import { validateUserId } from "../core/identifiers.js";
|
|
3
|
+
|
|
4
|
+
// Inspection and materialization share the same topology capability. A
|
|
5
|
+
// categorical density is an entire silhouette, not one endpoint per category.
|
|
6
|
+
export function overlaySourceReason(layer, coordinateType, density) {
|
|
7
|
+
if (coordinateType !== 'cartesian' || !['bar', 'point', 'line', 'area'].includes(layer?.mark?.type))
|
|
8
|
+
return 'Overlay source requires a Cartesian bar, point, line, or area mark.';
|
|
9
|
+
if (layer.derivedBindings?.overlay) return 'Overlay source must be an independent mark.';
|
|
10
|
+
if (['x', 'y'].some(channel => !layer.encoding?.[channel]?.scale)) return 'Overlay source requires complete x and y positions.';
|
|
11
|
+
if (density?.placement?.type === 'category') return 'Categorically placed density areas do not have a single result endpoint per category.';
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeOverlayBinding(input) {
|
|
16
|
+
if (!isPlainObject(input) || Object.keys(input).some(key =>
|
|
17
|
+
!["source", "basis", "mode", "series", "missing", "orderBy", "inheritColor"].includes(key))) {
|
|
18
|
+
throw new TypeError("Overlay binding requires source, basis, mode, series, missing, and optional orderBy.");
|
|
19
|
+
}
|
|
20
|
+
validateUserId(input.source, "Overlay source");
|
|
21
|
+
if (input.basis !== "visibleItems" || input.mode !== "follow") {
|
|
22
|
+
throw new Error('Overlay binding requires basis "visibleItems" and mode "follow".');
|
|
23
|
+
}
|
|
24
|
+
if (!["source", "total"].includes(input.series)) throw new Error("Overlay series must be source or total.");
|
|
25
|
+
if (!["break", "connect"].includes(input.missing)) throw new Error("Overlay missing must be break or connect.");
|
|
26
|
+
if (typeof input.inheritColor !== "boolean") throw new TypeError("Overlay inheritColor must be boolean.");
|
|
27
|
+
if (input.orderBy !== undefined && !["x", "y"].includes(input.orderBy)) {
|
|
28
|
+
throw new Error("Overlay orderBy must be x or y.");
|
|
29
|
+
}
|
|
30
|
+
return cloneAndFreeze(input);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function overlayGroupFields(layer) {
|
|
34
|
+
const group = layer.encoding?.group;
|
|
35
|
+
if (group?.fields) return group.fields;
|
|
36
|
+
if (group?.field) return [group.field];
|
|
37
|
+
const color = layer.encoding?.color;
|
|
38
|
+
return ["nominal", "ordinal"].includes(color?.fieldType) &&
|
|
39
|
+
![layer.encoding?.x?.field, layer.encoding?.y?.field].includes(color.field)
|
|
40
|
+
? [color.field] : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function overlayPositionEncoding(source, channel) {
|
|
44
|
+
const encoding = source.encoding[channel];
|
|
45
|
+
return encoding?.field === undefined && source.encoding[`${channel}2`]?.field !== undefined
|
|
46
|
+
? source.encoding[`${channel}2`] : encoding;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function assertIndependentOverlayGeometry(layer, operation) {
|
|
50
|
+
if (layer.derivedBindings?.overlay) throw new Error(`${operation}: overlay geometry follows its source; edit the source or detach the overlay first.`);
|
|
51
|
+
}
|
|
@@ -1,208 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
3
|
-
import { validateUserId } from "../core/identifiers.js";
|
|
4
|
-
import { validateOptionObject } from "../core/validation.js";
|
|
5
|
-
import {
|
|
6
|
-
mapContinuousScaleValues,
|
|
7
|
-
mapOrdinalPositionValues,
|
|
8
|
-
validateSemanticFieldType
|
|
9
|
-
} from "./scales/index.js";
|
|
10
|
-
|
|
11
|
-
const DIMENSION_OPTIONS = Object.freeze([
|
|
12
|
-
"field", "fieldType", "title", "scale"
|
|
13
|
-
]);
|
|
14
|
-
const FIELD_TYPES = new Set(["quantitative", "ordinal"]);
|
|
15
|
-
const MISSING_POLICIES = new Set(["break", "drop-row", "error"]);
|
|
16
|
-
const DIMENSION_SCALE_OPTIONS = Object.freeze([
|
|
17
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
18
|
-
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
19
|
-
"padding", "align", "unknown"
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export function validateParallelKeyField(value) {
|
|
24
|
-
return nonEmptyString(value, "Parallel key field");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function inferFieldType(rows, field) {
|
|
28
|
-
const values = rows
|
|
29
|
-
.map(row => row[field])
|
|
30
|
-
.filter(value => value !== undefined && value !== null);
|
|
31
|
-
if (values.length === 0) {
|
|
32
|
-
throw new Error(`Cannot infer Parallel field type for "${field}".`);
|
|
33
|
-
}
|
|
34
|
-
if (values.every(Number.isFinite)) return "quantitative";
|
|
35
|
-
if (values.every(value => typeof value === "string")) return "ordinal";
|
|
36
|
-
throw new Error(
|
|
37
|
-
`Parallel dimension "${field}" requires an explicit compatible fieldType.`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function validateScaleOptions(value, index) {
|
|
42
|
-
if (value === undefined) return {};
|
|
43
|
-
if (!isPlainObject(value)) {
|
|
44
|
-
throw new TypeError(`Parallel dimension ${index} scale must be a plain object.`);
|
|
45
|
-
}
|
|
46
|
-
validateOptionObject(
|
|
47
|
-
value,
|
|
48
|
-
DIMENSION_SCALE_OPTIONS,
|
|
49
|
-
`Parallel dimension ${index} scale`
|
|
50
|
-
);
|
|
51
|
-
return { ...value };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function validateParallelMissingPolicy(value) {
|
|
55
|
-
if (!MISSING_POLICIES.has(value)) {
|
|
56
|
-
throw new Error(`Unsupported Parallel missing policy "${value}".`);
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function validateParallelDimensions(dimensions, { normalized = false } = {}) {
|
|
62
|
-
if (!Array.isArray(dimensions) || dimensions.length < 2) {
|
|
63
|
-
throw new RangeError("Parallel coordinates require at least two dimensions.");
|
|
64
|
-
}
|
|
65
|
-
const fields = new Set();
|
|
66
|
-
for (const [index, dimension] of dimensions.entries()) {
|
|
67
|
-
if (!isPlainObject(dimension)) {
|
|
68
|
-
throw new TypeError(`Parallel dimension ${index} must be an object.`);
|
|
69
|
-
}
|
|
70
|
-
const supported = normalized
|
|
71
|
-
? ["field", "fieldType", "title", "scale"]
|
|
72
|
-
: DIMENSION_OPTIONS;
|
|
73
|
-
validateOptionObject(dimension, supported, `Parallel dimension ${index}`);
|
|
74
|
-
const field = nonEmptyString(
|
|
75
|
-
dimension.field,
|
|
76
|
-
`Parallel dimension ${index} field`
|
|
77
|
-
);
|
|
78
|
-
if (fields.has(field)) {
|
|
79
|
-
throw new Error("Parallel dimensions must use unique fields.");
|
|
80
|
-
}
|
|
81
|
-
fields.add(field);
|
|
82
|
-
validateSemanticFieldType(dimension.fieldType);
|
|
83
|
-
if (!FIELD_TYPES.has(dimension.fieldType)) {
|
|
84
|
-
throw new Error(`Unsupported Parallel field type "${dimension.fieldType}".`);
|
|
85
|
-
}
|
|
86
|
-
nonEmptyString(dimension.title, `Parallel dimension ${index} title`);
|
|
87
|
-
validateUserId(dimension.scale, `Parallel dimension ${index} scale id`);
|
|
88
|
-
}
|
|
89
|
-
return dimensions;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function normalizeParallelDimensions(rows, dimensions, target) {
|
|
93
|
-
if (!Array.isArray(dimensions) || dimensions.length < 2) {
|
|
94
|
-
throw new RangeError("Parallel coordinates require at least two dimensions.");
|
|
95
|
-
}
|
|
96
|
-
const normalized = dimensions.map((input, index) => {
|
|
97
|
-
const dimension = typeof input === "string" ? { field: input } : input;
|
|
98
|
-
if (!isPlainObject(dimension)) {
|
|
99
|
-
throw new TypeError(
|
|
100
|
-
`Parallel dimension ${index} must be a field string or plain object.`
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
validateOptionObject(
|
|
104
|
-
dimension,
|
|
105
|
-
DIMENSION_OPTIONS,
|
|
106
|
-
`Parallel dimension ${index}`
|
|
107
|
-
);
|
|
108
|
-
const field = nonEmptyString(
|
|
109
|
-
dimension.field,
|
|
110
|
-
`Parallel dimension ${index} field`
|
|
111
|
-
);
|
|
112
|
-
const fieldType = dimension.fieldType ?? inferFieldType(rows, field);
|
|
113
|
-
if (!FIELD_TYPES.has(fieldType)) {
|
|
114
|
-
throw new Error(`Unsupported Parallel field type "${fieldType}".`);
|
|
115
|
-
}
|
|
116
|
-
const scale = validateScaleOptions(dimension.scale, index);
|
|
117
|
-
return {
|
|
118
|
-
field,
|
|
119
|
-
fieldType,
|
|
120
|
-
title: dimension.title ?? field,
|
|
121
|
-
scale: `${target}-parallel-${index}`,
|
|
122
|
-
scaleOptions: scale
|
|
123
|
-
};
|
|
124
|
-
});
|
|
125
|
-
const fields = normalized.map(dimension => dimension.field);
|
|
126
|
-
if (new Set(fields).size !== fields.length) {
|
|
127
|
-
throw new Error("Parallel dimensions must use unique fields.");
|
|
128
|
-
}
|
|
129
|
-
return cloneAndFreeze(normalized);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function validDimensionValue(value, fieldType) {
|
|
133
|
-
return fieldType === "quantitative"
|
|
134
|
-
? Number.isFinite(value)
|
|
135
|
-
: typeof value === "string" || Number.isFinite(value);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function commandsFromVertices(vertices) {
|
|
139
|
-
const commands = [];
|
|
140
|
-
let fragment = [];
|
|
141
|
-
const flush = () => {
|
|
142
|
-
if (fragment.length >= 2) {
|
|
143
|
-
fragment.forEach((point, index) => commands.push({
|
|
144
|
-
op: index === 0 ? "M" : "L",
|
|
145
|
-
x: point.x,
|
|
146
|
-
y: point.y
|
|
147
|
-
}));
|
|
148
|
-
}
|
|
149
|
-
fragment = [];
|
|
150
|
-
};
|
|
151
|
-
for (const vertex of vertices) {
|
|
152
|
-
if (vertex === undefined) flush();
|
|
153
|
-
else fragment.push(vertex);
|
|
154
|
-
}
|
|
155
|
-
flush();
|
|
156
|
-
return commands;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function validateParallelRows(rows, dimensions, { key, missing }) {
|
|
160
|
-
const seen = new Set();
|
|
161
|
-
rows.forEach((row, index) => {
|
|
162
|
-
if (key !== undefined) {
|
|
163
|
-
const value = row[key];
|
|
164
|
-
if (value === undefined || value === null || value === "") {
|
|
165
|
-
throw new Error(`Parallel key is missing at row ${index}.`);
|
|
166
|
-
}
|
|
167
|
-
const token = `${typeof value}:${String(value)}`;
|
|
168
|
-
if (seen.has(token)) throw new Error(`Duplicate Parallel key "${value}".`);
|
|
169
|
-
seen.add(token);
|
|
170
|
-
}
|
|
171
|
-
const incomplete = dimensions.some(dimension =>
|
|
172
|
-
!validDimensionValue(row[dimension.field], dimension.fieldType)
|
|
173
|
-
);
|
|
174
|
-
if (incomplete && missing === "error") {
|
|
175
|
-
throw new Error(`Parallel row ${index} has a missing dimension.`);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function materializeParallelRows(rows, dimensions, resolvedScales, bounds, {
|
|
181
|
-
key,
|
|
182
|
-
missing
|
|
183
|
-
}) {
|
|
184
|
-
validateParallelRows(rows, dimensions, { key, missing });
|
|
185
|
-
const step = bounds.width / (dimensions.length - 1);
|
|
186
|
-
const items = [];
|
|
187
|
-
rows.forEach((row, sourceRowIndex) => {
|
|
188
|
-
const vertices = dimensions.map((dimension, index) => {
|
|
189
|
-
const value = row[dimension.field];
|
|
190
|
-
if (!validDimensionValue(value, dimension.fieldType)) return undefined;
|
|
191
|
-
const scale = resolvedScales[dimension.scale];
|
|
192
|
-
const mapped = dimension.fieldType === "quantitative"
|
|
193
|
-
? mapContinuousScaleValues([value], scale)[0]
|
|
194
|
-
: mapOrdinalPositionValues([value], scale)[0];
|
|
195
|
-
return Number.isFinite(mapped)
|
|
196
|
-
? { x: bounds.x + step * index, y: mapped, value, dimension: dimension.field }
|
|
197
|
-
: undefined;
|
|
198
|
-
});
|
|
199
|
-
const incomplete = vertices.some(vertex => vertex === undefined);
|
|
200
|
-
if (incomplete && missing === "drop-row") return;
|
|
201
|
-
items.push({
|
|
202
|
-
key: key === undefined ? `source:${sourceRowIndex}` : row[key],
|
|
203
|
-
sourceRowIndex,
|
|
204
|
-
commands: commandsFromVertices(vertices)
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
return cloneAndFreeze(items);
|
|
208
|
-
}
|
|
1
|
+
import{requireStringValue as P}from"../core/validation.js";import{cloneAndFreeze as N,isOwned as E,isPlainObject as $}from"../core/immutable.js";import{validateUserId as I}from"../core/identifiers.js";import{validateOptionObject as S}from"../core/validation.js";import{mapContinuousScaleValues as A,mapOrdinalPositionValues as D,validateSemanticFieldType as M}from"./scales/index.js";const q=Object.freeze(["field","fieldType","title","scale"]),F=new Set(["quantitative","ordinal"]),k=new Set(["break","drop-row","error"]),z=new WeakMap,x=Object.freeze(["id","type","domain","range","nice","zero","clamp","reverse","base","exponent","constant","paddingInner","paddingOuter","padding","align","unknown"]);function W(e){return P(e,"Parallel key field")}function V(e,t){const i=e.map(n=>n[t]).filter(n=>n!=null);if(i.length===0)throw new Error(`Cannot infer Parallel field type for "${t}".`);if(i.every(Number.isFinite))return"quantitative";if(i.every(n=>typeof n=="string"))return"ordinal";throw new Error(`Parallel dimension "${t}" requires an explicit compatible fieldType.`)}function _(e,t){if(e===void 0)return{};if(!$(e))throw new TypeError(`Parallel dimension ${t} scale must be a plain object.`);return S(e,x,`Parallel dimension ${t} scale`),{...e}}function Y(e){if(!k.has(e))throw new Error(`Unsupported Parallel missing policy "${e}".`);return e}function B(e,{normalized:t=!1}={}){if(!Array.isArray(e)||e.length<2)throw new RangeError("Parallel coordinates require at least two dimensions.");const i=new Set;for(const[n,r]of e.entries()){if(!$(r))throw new TypeError(`Parallel dimension ${n} must be an object.`);S(r,t?["field","fieldType","title","scale"]:q,`Parallel dimension ${n}`);const o=P(r.field,`Parallel dimension ${n} field`);if(i.has(o))throw new Error("Parallel dimensions must use unique fields.");if(i.add(o),M(r.fieldType),!F.has(r.fieldType))throw new Error(`Unsupported Parallel field type "${r.fieldType}".`);P(r.title,`Parallel dimension ${n} title`),I(r.scale,`Parallel dimension ${n} scale id`)}return e}function H(e,t,i){if(!Array.isArray(t)||t.length<2)throw new RangeError("Parallel coordinates require at least two dimensions.");const n=t.map((s,o)=>{const f=typeof s=="string"?{field:s}:s;if(!$(f))throw new TypeError(`Parallel dimension ${o} must be a field string or plain object.`);S(f,q,`Parallel dimension ${o}`);const l=P(f.field,`Parallel dimension ${o} field`),a=f.fieldType??V(e,l);if(!F.has(a))throw new Error(`Unsupported Parallel field type "${a}".`);const y=_(f.scale,o);return{field:l,fieldType:a,title:f.title??l,scale:y.id===void 0?`${i}-parallel-${o}`:I(y.id,"Parallel dimension scale id"),scaleOptions:y}}),r=n.map(s=>s.field);if(new Set(r).size!==r.length)throw new Error("Parallel dimensions must use unique fields.");return N(n)}function T(e,t){return t==="quantitative"?Number.isFinite(e):typeof e=="string"||Number.isFinite(e)}function C(e){const t=[];let i=[];const n=()=>{i.length>=2&&i.forEach((r,s)=>t.push({op:s===0?"M":"L",x:r.x,y:r.y})),i=[]};for(const r of e)r===void 0?n():i.push(r);return n(),t}function L(e,t,{key:i,missing:n}){const r=new Set;e.forEach((s,o)=>{if(i!==void 0){const l=s[i];if(l==null||l==="")throw new Error(`Parallel key is missing at row ${o}.`);const a=`${typeof l}:${String(l)}`;if(r.has(a))throw new Error(`Duplicate Parallel key "${l}".`);r.add(a)}if(t.some(l=>!T(s[l.field],l.fieldType))&&n==="error")throw new Error(`Parallel row ${o} has a missing dimension.`)})}function Q(e,t,i,n,{key:r,missing:s}){const o=t.map(c=>i[c.scale]),f=E(e)&&E(t)&&o.every(E),l=f?JSON.stringify({dimensions:t,scales:o,bounds:n,key:r,missing:s}):void 0;let a=f?z.get(e):void 0;if(a?.has(l)){const c=a.get(l);return a.delete(l),a.set(l,c),c}L(e,t,{key:r,missing:s});const y=n.width/(t.length-1),j=t.map((c,u)=>{const d=[],g=[];e.forEach((m,h)=>{const b=m[c.field];T(b,c.fieldType)&&(d.push(b),g.push(h))});const p=d.length===0?[]:c.fieldType==="quantitative"?A(d,o[u]):D(d,o[u]),w=new Array(e.length);return g.forEach((m,h)=>{w[m]=p[h]}),w}),O=[];e.forEach((c,u)=>{const d=t.map((p,w)=>{const m=c[p.field];if(!T(m,p.fieldType))return;const h=j[w][u];return Number.isFinite(h)?{x:n.x+y*w,y:h,value:m,dimension:p.field}:void 0});d.some(p=>p===void 0)&&s==="drop-row"||O.push({key:r===void 0?`source:${u}`:c[r],sourceRowIndex:u,commands:C(d)})});const v=N(O);return f&&(a||z.set(e,a=new Map),a.set(l,v),a.size>4&&a.delete(a.keys().next().value)),v}export{Q as materializeParallelRows,H as normalizeParallelDimensions,B as validateParallelDimensions,W as validateParallelKeyField,Y as validateParallelMissingPolicy,L as validateParallelRows};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { freezeOwned, isPlainObject } from "../core/immutable.js";
|
|
1
|
+
import { freezeOwned, isOwned, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { validateGeneratedItemLimit } from "../core/validation.js";
|
|
3
3
|
|
|
4
4
|
export const MAX_PATH_COMMANDS = 30_000;
|
|
@@ -13,6 +13,7 @@ const COMMAND_KEYS = Object.freeze({
|
|
|
13
13
|
C: Object.freeze(["op", "x1", "y1", "x2", "y2", "x", "y"]),
|
|
14
14
|
Z: Object.freeze(["op"])
|
|
15
15
|
});
|
|
16
|
+
const validatedCommands = new WeakSet();
|
|
16
17
|
|
|
17
18
|
function validateCommand(command, index) {
|
|
18
19
|
if (!isPlainObject(command) || !Object.hasOwn(COMMAND_KEYS, command.op)) {
|
|
@@ -41,6 +42,7 @@ function validateCommand(command, index) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function validatePathCommands(commands) {
|
|
45
|
+
if (validatedCommands.has(commands)) return commands;
|
|
44
46
|
if (!Array.isArray(commands) || commands.length < 1) {
|
|
45
47
|
throw new TypeError("path.commands must contain at least one command.");
|
|
46
48
|
}
|
|
@@ -49,7 +51,10 @@ export function validatePathCommands(commands) {
|
|
|
49
51
|
if (commands[0].op !== "M") {
|
|
50
52
|
throw new Error("path.commands must start with M.");
|
|
51
53
|
}
|
|
52
|
-
if (commands.length === 1)
|
|
54
|
+
if (commands.length === 1) {
|
|
55
|
+
if (isOwned(commands)) validatedCommands.add(commands);
|
|
56
|
+
return commands;
|
|
57
|
+
}
|
|
53
58
|
if (commands.slice(1).some(command => command.op === "M")) {
|
|
54
59
|
throw new Error("path.commands supports one subpath and only one initial M.");
|
|
55
60
|
}
|
|
@@ -60,6 +65,7 @@ export function validatePathCommands(commands) {
|
|
|
60
65
|
if (closeIndex !== -1 && closeIndex !== commands.length - 1) {
|
|
61
66
|
throw new Error("path.commands Z must be the final command.");
|
|
62
67
|
}
|
|
68
|
+
if (isOwned(commands)) validatedCommands.add(commands);
|
|
63
69
|
return commands;
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateNonEmptyString as
|
|
1
|
+
import{validateNonEmptyString as y}from"../core/validation.js";import{readCategoricalLineColor as p,validateCategoricalColorConsumer as S}from"./scales/colorConsumers.js";import{readNominalField as f,readQuantitativeField as O,readScaleField as x}from"./scales/fields.js";function m(n){const e=Array.isArray(n)?[...n]:[n];for(const i of e)y(i,"Path group field");if(e.length===0||new Set(e).size!==e.length)throw new Error("Path group fields must be non-empty and unique.");return e}function E(n){const e=n.encoding?.group;if(e!==void 0){if(e.fieldType!=="nominal")throw new Error(`Path group on mark "${n.id}" must be nominal.`);if(Object.hasOwn(e,"field")&&Object.hasOwn(e,"fields"))throw new Error("Path group requires exactly one of field or fields.");return m(e.fields??e.field)}if(n.mark?.type!=="line")return[];const i=["nominal","ordinal"].includes(n.encoding?.color?.fieldType)?n.encoding.color.field:void 0,o=n.encoding?.strokeDash?.field;if(i!==void 0&&o!==void 0&&i!==o)throw new Error("Line color field must match strokeDash field unless encodeGroup defines the series.");return i===void 0&&o===void 0?[]:[i??o]}function h(n){return JSON.stringify(n.map(e=>e===void 0?{}:e))}function g(n,e){const i=n.encoding?.color,o=Array.isArray(e)?e.find(t=>t.id===i?.scale):e?.[i?.scale];return o!==void 0&&S(n,i,o)?o:void 0}function P(n,e,i){const o=E(e),t=g(e,i);for(const r of o)t&&e.encoding?.group===void 0&&r===e.encoding.color.field?p(n,r):f(n,r);return o}function k(n,e,i,o,t={},r){const l=o==="strokeWidth"||o==="opacity",d=o==="color"&&r!==void 0?p(n,i):o==="stroke"||o==="color"&&["quantitative","temporal"].includes(t.fieldType)?x(n,i,t.fieldType??"nominal",{temporalUnit:t.temporalUnit}):l?O(n,i):f(n,i),a=new Map;return n.forEach((u,v)=>{const c=h(e.map(w=>u[w])),s=d[v];if(o==="strokeWidth"&&s<0)throw new RangeError(`Line strokeWidth field "${i}" cannot contain negative values.`);if(a.has(c)&&a.get(c)!==s)throw new Error(`Path ${o} field "${i}" must have one value within each series.`);a.set(c,s)}),a}function F(n,e,i,o,t,r){if(e.length===0)return[];const l=Object.keys(e[0]?.key??{}),d=k(n,l,i,o,t,Object.hasOwn(r??{},"unknown")?r:void 0);return e.map(a=>d.get(h(Object.values(a.key))))}function j(n,e,i,o){if(!["line","area"].includes(e.mark?.type)||e.encoding?.parallel!==void 0)return;const t=o??P(n,e,i);if(o!==void 0)for(const d of t)f(n,d);const r=g(e,i),l=e.mark.type==="line"?["color","stroke","strokeDash","strokeWidth","opacity"]:["color","stroke","opacity"];for(const d of l){const a=e.encoding?.[d];if(a?.field===void 0)continue;if(!(["color","stroke"].includes(d)?d==="color"?e.mark.type==="area"?["nominal","ordinal","quantitative"]:["nominal","ordinal","quantitative","temporal"]:["nominal","ordinal","quantitative","temporal"]:d==="strokeDash"?["nominal","ordinal"]:["quantitative"]).includes(a.fieldType))throw new Error(`Path ${d} encoding has an unsupported field type.`);k(n,t,a.field,d,a,d==="color"?r:void 0)}return t}function A(n,e){const i=e.encoding?.group,o=i===void 0?[]:m(i.fields??i.field);for(const r of o)f(n,r);const t=n.map(r=>o.length===0?void 0:o.length===1?r[o[0]]:JSON.stringify(o.map(l=>r[l])));return{fields:o,values:t,domain:[...new Set(t)]}}export{F as derivePathSeriesFieldValues,m as normalizeGroupFields,E as pathGroupFields,h as pathSeriesKey,P as readPathSeriesFields,A as readSeriesIdentity,j as validatePathSeriesAppearance};
|