ggaction 0.0.21 → 0.0.22
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 +73 -0
- package/README.md +3 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +6 -2
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +1 -0
- package/src/actions/boxPlots/components.js +1 -96
- package/src/actions/boxPlots/create.js +6 -1
- package/src/actions/boxPlots/edit.js +8 -0
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +1 -1
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +2 -1
- package/src/actions/canvas/plotOrder.js +14 -0
- package/src/actions/charts/area.js +1 -1
- package/src/actions/charts/bar.js +4 -4
- package/src/actions/charts/density.js +1 -1
- package/src/actions/charts/ecdf.js +1 -1
- package/src/actions/charts/endpoints.js +54 -5
- package/src/actions/charts/heatmap.js +1 -1
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +1 -1
- package/src/actions/charts/index.js +2 -1
- package/src/actions/charts/interval-regression.js +1 -1
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/radar.js +63 -3
- package/src/actions/charts/raincloud.js +37 -6
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/scatter.js +3 -3
- package/src/actions/charts/text.js +1 -1
- package/src/actions/composition/actions.js +10 -3
- package/src/actions/coordinates/actions.js +2 -0
- package/src/actions/data/basic.js +6 -0
- package/src/actions/data/derived.js +6 -2
- package/src/actions/data/edit.js +21 -35
- package/src/actions/data/filter.js +1 -376
- package/src/actions/data/flatten.js +19 -0
- package/src/actions/data/index.js +6 -0
- package/src/actions/data/lookup.js +50 -0
- package/src/actions/data/raincloudFilter.js +56 -0
- package/src/actions/data/shared.js +10 -0
- package/src/actions/distributions/revision.js +3 -1
- package/src/actions/encodings/angle.js +2 -1
- package/src/actions/encodings/appearance.js +38 -13
- package/src/actions/encodings/channels.js +14 -3
- package/src/actions/encodings/color/continuous.js +26 -10
- package/src/actions/encodings/color/index.js +50 -20
- package/src/actions/encodings/color/policy.js +57 -4
- package/src/actions/encodings/parallel.js +4 -0
- package/src/actions/encodings/position/policies/bar.js +10 -6
- package/src/actions/encodings/position/policies/line.js +1 -152
- package/src/actions/encodings/position/resolve.js +4 -1
- package/src/actions/encodings/ranged.js +46 -0
- package/src/actions/encodings/remove.js +1 -1
- package/src/actions/encodings/ruleAppearance.js +7 -6
- package/src/actions/encodings/shared.js +14 -2
- package/src/actions/encodings/stroke.js +20 -14
- package/src/actions/encodings/strokeDash.js +9 -11
- package/src/actions/encodings/text.js +30 -2
- package/src/actions/errorBars/edit.js +24 -4
- package/src/actions/facets/actions.js +1 -793
- package/src/actions/facets/derive.js +34 -5
- package/src/actions/facets/replay.js +1 -38
- package/src/actions/gradientPlots/components.js +21 -9
- package/src/actions/gradientPlots/create.js +4 -2
- package/src/actions/gradientPlots/edit.js +18 -1
- package/src/actions/gradientPlots/materialize.js +35 -36
- package/src/actions/gradientPlots/options.js +1 -1
- package/src/actions/gradientPlots/paint.js +13 -7
- package/src/actions/guides/applicability.js +2 -0
- package/src/actions/guides/axes/axes.js +39 -0
- package/src/actions/guides/axes/axis.js +9 -5
- package/src/actions/guides/axes/edit.js +11 -8
- package/src/actions/guides/axes/labels.js +24 -8
- package/src/actions/guides/axes/parallel/policy.js +3 -5
- package/src/actions/guides/axes/parallel/resolve.js +4 -3
- package/src/actions/guides/axes/policy.js +22 -0
- package/src/actions/guides/axes/tickGroups.js +4 -2
- package/src/actions/guides/axes/ticks.js +22 -4
- package/src/actions/guides/axes/titles.js +19 -6
- package/src/actions/guides/facadeAxes.js +3 -0
- package/src/actions/guides/grids/grid.js +3 -1
- package/src/actions/guides/grids/resolve.js +7 -11
- package/src/actions/guides/guides.js +21 -1
- package/src/actions/guides/legends/categorical/actions.js +1 -515
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +4 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -3
- package/src/actions/guides/legends/categorical/symbols.js +3 -3
- package/src/actions/guides/legends/colorSize.js +2 -0
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -360
- package/src/actions/guides/legends/continuous/interval.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +15 -1
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/sampling.js +1 -140
- package/src/actions/guides/legends/strokeWidth.js +3 -2
- package/src/actions/guides/legends/transition.js +19 -2
- package/src/actions/guides/polar/axes/facade.js +5 -4
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/resolve.js +2 -1
- package/src/actions/guides/tickValues.js +7 -7
- package/src/actions/marks/arc/actions.js +5 -7
- package/src/actions/marks/area/actions.js +9 -3
- package/src/actions/marks/bar/create.js +4 -2
- package/src/actions/marks/bar/edit.js +9 -6
- package/src/actions/marks/bar/materialize.js +5 -3
- package/src/actions/marks/dependentReferences.js +87 -0
- package/src/actions/marks/index.js +2 -0
- package/src/actions/marks/lifecycle.js +2 -1
- package/src/actions/marks/line/actions.js +24 -6
- package/src/actions/marks/line/materialize.js +7 -3
- package/src/actions/marks/point/create.js +5 -4
- package/src/actions/marks/point/edit.js +6 -3
- package/src/actions/marks/point/jitter.js +6 -6
- package/src/actions/marks/point/materialize.js +36 -39
- package/src/actions/marks/rect/actions.js +16 -4
- package/src/actions/marks/references.js +183 -35
- package/src/actions/marks/rule/actions.js +20 -9
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +10 -3
- package/src/actions/marks/tick/actions.js +11 -2
- package/src/actions/primitives/editGraphics.js +28 -3
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semanticAction.js +13 -7
- package/src/actions/primitives/semanticValidation/layer.js +20 -0
- package/src/actions/primitives/semanticValidation/scale.js +8 -1
- package/src/actions/scales/channels.js +1 -1
- package/src/actions/scales/consumers/common.js +19 -1
- package/src/actions/scales/consumers/families.js +2 -2
- package/src/actions/scales/consumers/seriesLayout.js +4 -3
- package/src/actions/scales/edit.js +11 -0
- package/src/actions/scales/editPolicy.js +5 -3
- package/src/actions/scales/preview.js +2 -1
- package/src/actions/scales/quantitativeColor.js +2 -4
- package/src/actions/selection/actions.js +10 -1
- package/src/actions/theme/reconcile.js +37 -9
- package/src/core/compositionState.js +21 -4
- package/src/core/datasetReferences.js +17 -0
- package/src/core/immutable.js +1 -0
- package/src/core/resourceReferences.js +5 -3
- package/src/core/validation.js +4 -0
- package/src/core/vocabulary.js +24 -2
- package/src/grammar/aggregate.js +1 -0
- package/src/grammar/arcs.js +1 -1
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bars/policy.js +2 -2
- package/src/grammar/datasetDependencies.js +97 -0
- package/src/grammar/datasetSchema.js +31 -1
- package/src/grammar/facetHeaders.js +1 -0
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/legendIntent.js +6 -2
- package/src/grammar/facets/scales.js +27 -7
- package/src/grammar/flatten.js +85 -0
- package/src/grammar/histogram.js +13 -1
- package/src/grammar/horizon.js +1 -494
- package/src/grammar/jitter.js +2 -2
- package/src/grammar/legendSpacing.js +45 -0
- package/src/grammar/lineMissing.js +90 -0
- package/src/grammar/lineSeries.js +1 -376
- package/src/grammar/lookup.js +97 -0
- package/src/grammar/markFilter.js +4 -2
- package/src/grammar/markLabels.js +8 -3
- package/src/grammar/numeric.js +1 -1
- package/src/grammar/pathCommands.js +1 -1
- package/src/grammar/pathSeries.js +1 -109
- package/src/grammar/pointPolygon.js +81 -0
- package/src/grammar/pointShapes.js +15 -15
- package/src/grammar/rects.js +1 -1
- package/src/grammar/regression/derive.js +1 -195
- package/src/grammar/scales/appearance.js +1 -1
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -63
- package/src/grammar/scales/continuous.js +58 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +3 -3
- package/src/grammar/schemas/graphicBounds.js +40 -12
- package/src/grammar/schemas/graphicClip.js +23 -0
- package/src/grammar/schemas/graphicTree.js +16 -0
- package/src/grammar/statisticalReference.js +22 -0
- package/src/grammar/text.js +65 -1
- package/src/grammar/ticks.js +92 -1
- package/src/grammar/transformTopology.js +3 -1
- package/src/grammar/transforms.js +24 -0
- package/src/inspection.js +1 -495
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -1
- package/src/layout/facetContentFlow.js +101 -0
- package/src/layout/facetHeaderTitles.js +1 -0
- package/src/layout/facetTopLane.js +29 -0
- package/src/layout/facets.js +1 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +26 -10
- package/src/layout/legendSpacing.js +66 -0
- package/src/layout/plotDrawOrder.js +17 -0
- package/src/materialization/bars/histogram.js +8 -2
- package/src/materialization/bars/resolve.js +1 -1
- package/src/materialization/composition.js +1 -0
- package/src/materialization/compositionSnapshot.js +22 -2
- package/src/materialization/dataProvenance.js +1 -1
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/placement.js +23 -11
- package/src/materialization/facetGuides/preparation.js +43 -24
- package/src/materialization/facetHeaders.js +1 -1
- package/src/materialization/facetTopLane.js +25 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -0
- package/src/materialization/graphicTranslation.js +2 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/markClip.js +26 -0
- package/src/materialization/marks/capabilities.js +3 -2
- package/src/materialization/marks/grouping.js +7 -5
- package/src/materialization/marks/index.js +4 -2
- package/src/materialization/marks/policies.js +4 -0
- package/src/materialization/plotDrawOrder.js +44 -0
- package/src/materialization/rect.js +11 -3
- package/src/materialization/scaleGuideDependencies.js +13 -5
- package/src/materialization/selection/items/common.js +1 -1
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/policies/index.js +2 -1
- package/src/materialization/text.js +10 -5
- package/src/mcp/cli.js +0 -0
- package/src/persistence/validation.js +108 -18
- package/src/persistence.js +4 -2
- package/src/renderers/canvas/index.js +16 -0
- package/src/renderers/canvas/isolated.js +11 -1
- package/src/renderers/canvas/native.js +7 -0
- package/src/renderers/canvas/scene.js +66 -0
- package/src/renderers/canvas/text.js +3 -2
- package/src/renderers/svg.js +1 -617
- package/src/renderers/svgSerialization.js +643 -0
- package/src/renderers/transitionScene.js +67 -0
- package/src/scene.js +89 -0
- package/src/selectors/axes.js +1 -1
- package/src/selectors/datasets.js +1 -0
- package/src/selectors/grids.js +9 -0
- package/src/selectors/markClip.js +28 -0
- package/src/selectors/raincloud.js +7 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +7 -0
- package/types/index.d.ts +8 -0
- package/types/program.d.ts +270 -60
- package/types/scene.d.ts +35 -0
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
cleanNumericValue,
|
|
6
6
|
interpolateNumber,
|
|
7
7
|
inverseLerp,
|
|
8
|
+
nearestNumericIncrement,
|
|
8
9
|
niceNumericStep,
|
|
9
10
|
normalizeNumericRange
|
|
10
11
|
} from "../numeric.js";
|
|
@@ -76,6 +77,59 @@ export function niceLinearDomain(domain) {
|
|
|
76
77
|
return cloneAndFreeze([lower, upper]);
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
export function normalizeScaleNice(type, value) {
|
|
81
|
+
if (typeof value === "boolean") return value;
|
|
82
|
+
if (!isPlainObject(value)) {
|
|
83
|
+
throw new TypeError("Scale nice must be a boolean or a linear count object.");
|
|
84
|
+
}
|
|
85
|
+
if (type !== "linear") {
|
|
86
|
+
throw new Error("Count-based nice requires a linear position scale.");
|
|
87
|
+
}
|
|
88
|
+
if (Reflect.ownKeys(value).length !== 1 || !Object.hasOwn(value, "count")) {
|
|
89
|
+
throw new TypeError("Count-based nice requires exactly one count property.");
|
|
90
|
+
}
|
|
91
|
+
const count = value.count;
|
|
92
|
+
if (!Number.isSafeInteger(count) || count <= 0) {
|
|
93
|
+
throw new RangeError("Nice count must be a positive safe integer.");
|
|
94
|
+
}
|
|
95
|
+
return cloneAndFreeze({ count });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function niceLinearCountDomain(domain, count) {
|
|
99
|
+
const input = validatePair(domain, "Nice linear domain");
|
|
100
|
+
normalizeScaleNice("linear", { count });
|
|
101
|
+
const reversed = input[0] > input[1];
|
|
102
|
+
let lower = Math.min(...input), upper = Math.max(...input);
|
|
103
|
+
let stable = lower === upper;
|
|
104
|
+
for (let iteration = 0; iteration < 10 && lower !== upper; iteration++) {
|
|
105
|
+
let start = lower, end = upper, scale = 1;
|
|
106
|
+
let increment = nearestNumericIncrement(end - start, count);
|
|
107
|
+
if (increment === null) {
|
|
108
|
+
const normalized = normalizeNumericRange(lower, upper);
|
|
109
|
+
({ start, end, scale } = normalized);
|
|
110
|
+
increment = nearestNumericIncrement(normalized.span, count);
|
|
111
|
+
if (increment === null) break;
|
|
112
|
+
}
|
|
113
|
+
let nextLower = (increment < 0
|
|
114
|
+
? Math.floor(start * -increment) / -increment
|
|
115
|
+
: Math.floor(start / increment) * increment) * scale;
|
|
116
|
+
let nextUpper = (increment < 0
|
|
117
|
+
? Math.ceil(end * -increment) / -increment
|
|
118
|
+
: Math.ceil(end / increment) * increment) * scale;
|
|
119
|
+
// At numeric limits an outward step may not be representable. Retain the
|
|
120
|
+
// observed endpoint rather than overflow, move inward, or invent a value.
|
|
121
|
+
if (!Number.isFinite(nextLower) || nextLower > lower) nextLower = lower;
|
|
122
|
+
if (!Number.isFinite(nextUpper) || nextUpper < upper) nextUpper = upper;
|
|
123
|
+
if (nextLower === lower && nextUpper === upper) { stable = true; break; }
|
|
124
|
+
lower = nextLower; upper = nextUpper;
|
|
125
|
+
}
|
|
126
|
+
// A density such as one interval across zero may have no fixed outward
|
|
127
|
+
// grid. Do not return an arbitrarily enlarged result after the loop cap.
|
|
128
|
+
if (!stable) return input;
|
|
129
|
+
const result = [lower === 0 ? 0 : lower, upper === 0 ? 0 : upper];
|
|
130
|
+
return cloneAndFreeze(reversed ? result.reverse() : result);
|
|
131
|
+
}
|
|
132
|
+
|
|
79
133
|
export function resolveContinuousDomain({ domain, values, type, nice, zero }) {
|
|
80
134
|
const explicit = domain !== "auto";
|
|
81
135
|
let resolved = resolveScaleDomain(domain, values);
|
|
@@ -86,7 +140,10 @@ export function resolveContinuousDomain({ domain, values, type, nice, zero }) {
|
|
|
86
140
|
Math.max(0, resolved[1])
|
|
87
141
|
]);
|
|
88
142
|
}
|
|
89
|
-
if (nice ===
|
|
143
|
+
if (nice !== undefined && typeof nice === "object") {
|
|
144
|
+
const policy = normalizeScaleNice(type, nice);
|
|
145
|
+
resolved = niceLinearCountDomain(resolved, policy.count);
|
|
146
|
+
} else if (nice === true) {
|
|
90
147
|
resolved = type === "time"
|
|
91
148
|
? niceTimeDomain(resolved)
|
|
92
149
|
: niceLinearDomain(resolved);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateMeasuredRadiusScale as D}from"./radial.js";import{normalizeColorInterpolationOptions as F,validateContinuousColorInterpolation as
|
|
1
|
+
import{validateMeasuredRadiusScale as D}from"./radial.js";import{normalizeScaleNice as R}from"./continuous.js";import{normalizeColorInterpolationOptions as F,validateContinuousColorInterpolation as T,validateSequentialMidpoint as k}from"./color.js";import{isTransformedScaleType as q}from"./mapping.js";import{normalizeTransformParameters as A,validateTransformedDomain as _}from"./transformed.js";import{isContinuousColorScaleType as M,validateScalePropertyForType as b}from"./types.js";const $=Object.freeze(["nice","zero","clamp","reverse"]),p=Object.freeze(["base","exponent","constant"]);function f(i,l,n,e,o){return Object.hasOwn(l,e)?l[e]:!o&&Object.hasOwn(i,e)?i[e]:n[e]}function B(i,l,n,e,o){const m=Object.hasOwn(n,"padding");if(m&&(Object.hasOwn(n,"paddingInner")||Object.hasOwn(n,"paddingOuter")))throw new Error("Scale padding cannot be combined with paddingInner or paddingOuter.");if(m&&(!Number.isFinite(n.padding)||n.padding<0||n.padding>=1))throw new RangeError("Scale padding must be from 0 (inclusive) to 1 (exclusive).");const d=m?{...n,paddingInner:n.padding,paddingOuter:n.padding}:n,c=f(l,d,e,"paddingInner",o)??0,w=f(l,d,e,"paddingOuter",o)??0,g=f(l,d,e,"align",o)??.5;if(!Number.isFinite(c)||c<0||c>=1)throw new RangeError("Scale paddingInner must be from 0 (inclusive) to 1 (exclusive).");if(!Number.isFinite(w)||w<0)throw new RangeError("Scale paddingOuter must be non-negative and finite.");if(!Number.isFinite(g)||g<0||g>1)throw new RangeError("Scale align must be between 0 and 1.");Object.assign(i,{paddingInner:c,paddingOuter:w,align:g})}function L(i,l,n,e,o){const m=f(l,n,e,"padding",o)??.5,d=f(l,n,e,"align",o)??.5;if(!Number.isFinite(m)||m<0)throw new RangeError("Scale padding must be non-negative and finite.");if(!Number.isFinite(d)||d<0||d>1)throw new RangeError("Scale align must be between 0 and 1.");Object.assign(i,{padding:m,align:d})}function K({type:i,color:l=!1,previous:n={},patch:e={},defaults:o={},retainCoreOnTypeChange:m=!1,retainCompatibleOnTypeChange:d=!1,allowColorReset:c=!1,allowOrdinalBandParameters:w=!1,validateDomain:g,validateRange:I}){const s=n.type!==void 0&&n.type!==i,x=Object.hasOwn(e,"domain")?e.domain:(!s||m)&&Object.hasOwn(n,"domain")?n.domain:o.domain??"auto",z=Object.hasOwn(e,"range")?e.range:(!s||m)&&Object.hasOwn(n,"range")?n.range:o.range??"auto",r={type:i,domain:g(i,x),range:I(i,z)},u=f(n,e,o,"emptyDomain",s);if(u!==void 0){if(!["preserve","require-explicit"].includes(u))throw new Error('Scale emptyDomain must be "preserve" or "require-explicit".');r.emptyDomain=u}for(const a of $){let t=f(n,e,o,a,s);if(t===void 0&&s&&d&&Object.hasOwn(n,a))try{b(i,a),a==="nice"&&R(i,n[a]),t=n[a]}catch{t=void 0}if(t!==void 0){if(a==="nice")t=R(i,t);else if(typeof t!="boolean")throw new TypeError(`Scale ${a} must be a boolean.`);b(i,a),r[a]=t}}const j=Object.fromEntries(p.flatMap(a=>{const t=f(n,e,o,a,s);return t===void 0?[]:[[a,t]]}));if(q(i)){const a=A(i,j);i==="log"&&(r.base=a.base),i==="pow"&&(r.exponent=a.exponent),i==="symlog"&&(r.constant=a.constant),r.domain!=="auto"&&_(i,r.domain,j)}else for(const a of Object.keys(j))b(i,a);let O=f(n,e,o,"interpolate",s);if(i==="sequential"||l&&M(i))O===void 0&&d&&(O=n.interpolate),r.interpolate=T(O??"rgb");else if(O!==void 0)throw new Error(`Scale type "${i}" does not support interpolate.`);const N=Object.hasOwn(e,"interpolate")&&e.interpolate!==n.interpolate,S={};for(const a of["whitePoint","gamma"]){const t=Object.hasOwn(e,a)?e[a]:N?void 0:f(n,e,o,a,s&&!(d&&M(i)));t!==void 0&&!(c&&t==="auto")&&(S[a]=t)}Object.assign(r,F(r.interpolate,S));const E=k(f(n,e,o,"midpoint",s),i,r.domain);if(E!==void 0&&(r.midpoint=E),i==="band"||i==="ordinal"&&w)B(r,n,e,o,s);else if(i==="point")L(r,n,e,o,s);else for(const a of["paddingInner","paddingOuter","padding","align"])Object.hasOwn(e,a)&&b(i,a);const P=Object.hasOwn(e,"radialMapping")?e.radialMapping:n.radialMapping;return P!==void 0&&(r.radialMapping=P,D({...r,...Object.hasOwn(e,"unknown")||Object.hasOwn(n,"unknown")?{unknown:e.unknown??n.unknown}:{}},{allowAuto:!0})),r}export{K as normalizeScaleDefinition};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
2
|
import { isNominalValue } from "./fields.js";
|
|
3
|
-
import { validateColorRange, validateStrokeDashRange } from "./appearance.js";
|
|
3
|
+
import { validateColorRange, validateShapeRange, validateStrokeDashRange } from "./appearance.js";
|
|
4
4
|
import { validatePair, validateScaleRange } from "./validation.js";
|
|
5
5
|
import { resolveScaleRange } from "./continuous.js";
|
|
6
6
|
import { interpolateNumber } from "../numeric.js";
|
|
@@ -70,6 +70,9 @@ export function validateOrdinalDomain(domain) {
|
|
|
70
70
|
export function validateOrdinalRange(range) {
|
|
71
71
|
if (range === "auto") return range;
|
|
72
72
|
if (Array.isArray(range)) {
|
|
73
|
+
if (range.some(value => isPlainObject(value) && Object.hasOwn(value, "polygon"))) {
|
|
74
|
+
return validateShapeRange(range);
|
|
75
|
+
}
|
|
73
76
|
if (range.length === 2 && range.every(Number.isFinite)) {
|
|
74
77
|
return cloneAndFreeze(range);
|
|
75
78
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
|
-
import { POINT_SHAPES } from "../pointShapes.js";
|
|
2
|
+
import { POINT_SHAPES, validatePointShape } from "../pointShapes.js";
|
|
3
3
|
import { normalizeStrokeDashPattern } from "./appearance.js";
|
|
4
4
|
|
|
5
5
|
export function validateScaleUnknown(channel, value) {
|
|
@@ -30,10 +30,10 @@ export function validateScaleUnknown(channel, value) {
|
|
|
30
30
|
return value;
|
|
31
31
|
}
|
|
32
32
|
if (channel === "shape") {
|
|
33
|
-
if (!POINT_SHAPES.includes(value)) {
|
|
33
|
+
if (typeof value !== "object" && !POINT_SHAPES.includes(value)) {
|
|
34
34
|
throw new Error(`Unsupported scale unknown shape "${value}".`);
|
|
35
35
|
}
|
|
36
|
-
return value;
|
|
36
|
+
return validatePointShape(value);
|
|
37
37
|
}
|
|
38
38
|
if (channel === "strokeDash") {
|
|
39
39
|
return cloneAndFreeze(normalizeStrokeDashPattern(value));
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
resolveTextBounds
|
|
4
4
|
} from "../../core/textMetrics.js";
|
|
5
5
|
import { interpolateNumber } from "../numeric.js";
|
|
6
|
+
import { graphicClipBounds, intersectClipBounds } from "./graphicClip.js";
|
|
6
7
|
import { resolveStrokeDetails } from "../strokeStyle.js";
|
|
7
8
|
import {
|
|
8
9
|
findGraphic,
|
|
@@ -452,7 +453,7 @@ function canvasOffset(object, property) {
|
|
|
452
453
|
return Number.isFinite(value) ? value : 0;
|
|
453
454
|
}
|
|
454
455
|
|
|
455
|
-
function
|
|
456
|
+
export function resolveGraphicParentOffset(graphicSpec, target) {
|
|
456
457
|
const offset = [0, 0];
|
|
457
458
|
const visited = new Set();
|
|
458
459
|
let current = target;
|
|
@@ -476,11 +477,15 @@ function intersectVisibleBounds(bounds,clip) {
|
|
|
476
477
|
return result.right<=result.left||result.bottom<=result.top?undefined:result;
|
|
477
478
|
}
|
|
478
479
|
|
|
479
|
-
function clipAncestorBounds(graphicSpec,id,bounds) {
|
|
480
|
+
function clipAncestorBounds(graphicSpec,id,bounds,includeOwnerClips) {
|
|
480
481
|
let parent=findGraphicParent(graphicSpec,id);
|
|
481
482
|
while(parent!==undefined){
|
|
483
|
+
if (includeOwnerClips && parent.object.clip !== undefined) {
|
|
484
|
+
bounds = intersectVisibleBounds(bounds, graphicClipBounds(parent.object.clip,
|
|
485
|
+
...resolveGraphicParentOffset(graphicSpec, parent.id)));
|
|
486
|
+
}
|
|
482
487
|
if(parent.object.type==="canvas"&&findGraphicParent(graphicSpec,parent.id)!==undefined){
|
|
483
|
-
const [x,y]=
|
|
488
|
+
const [x,y]=resolveGraphicParentOffset(graphicSpec,parent.id),p=parent.object.properties;
|
|
484
489
|
bounds=intersectVisibleBounds(bounds,{left:x+(p.x??0),right:x+(p.x??0)+p.width,top:y+(p.y??0),bottom:y+(p.y??0)+p.height});
|
|
485
490
|
}
|
|
486
491
|
parent=findGraphicParent(graphicSpec,parent.id);
|
|
@@ -488,7 +493,27 @@ function clipAncestorBounds(graphicSpec,id,bounds) {
|
|
|
488
493
|
return bounds;
|
|
489
494
|
}
|
|
490
495
|
|
|
491
|
-
|
|
496
|
+
// Effective rectangular visibility, including the named owner itself. Inspection
|
|
497
|
+
// can opt into the root viewport without changing geometry/selection properties.
|
|
498
|
+
export function resolveGraphicClipBounds(graphicSpec, target, {rootViewport = false} = {}) {
|
|
499
|
+
const found = requireGraphic(graphicSpec, target);
|
|
500
|
+
let current = found.kind === "item" ? found.ownerId : found.id;
|
|
501
|
+
let clip;
|
|
502
|
+
while (current !== undefined) {
|
|
503
|
+
const object = graphicSpec.objects[current];
|
|
504
|
+
const offset = resolveGraphicParentOffset(graphicSpec, current);
|
|
505
|
+
const parent = findGraphicParent(graphicSpec, current);
|
|
506
|
+
const local = object.clip !== undefined ? graphicClipBounds(object.clip, ...offset)
|
|
507
|
+
: object.type === "canvas" && (parent !== undefined || rootViewport)
|
|
508
|
+
? graphicClipBounds({...object.properties, x: parent === undefined ? 0 : object.properties.x,
|
|
509
|
+
y: parent === undefined ? 0 : object.properties.y}, ...offset) : undefined;
|
|
510
|
+
if (local !== undefined) clip = clip === undefined ? local : intersectClipBounds(clip, local);
|
|
511
|
+
current = parent?.id;
|
|
512
|
+
}
|
|
513
|
+
return clip;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function objectBounds(graphicSpec, id, object, ancestors, x, y, profile, includeOwnerClips) {
|
|
492
517
|
if (ancestors.has(id)) {
|
|
493
518
|
throw new Error(`Graphic attachment cycle includes "${id}".`);
|
|
494
519
|
}
|
|
@@ -508,7 +533,7 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y, profile) {
|
|
|
508
533
|
throw new Error(`Unknown attached graphic "${childId}".`);
|
|
509
534
|
}
|
|
510
535
|
const bounds = objectBounds(
|
|
511
|
-
graphicSpec, childId, child.object, ancestors, childX, childY, profile
|
|
536
|
+
graphicSpec, childId, child.object, ancestors, childX, childY, profile, includeOwnerClips
|
|
512
537
|
);
|
|
513
538
|
if (bounds !== undefined) {
|
|
514
539
|
result = includeExtent(
|
|
@@ -520,28 +545,31 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y, profile) {
|
|
|
520
545
|
result=intersectVisibleBounds(result,{left:childX,right:childX+object.properties.width,
|
|
521
546
|
top:childY,bottom:childY+object.properties.height});
|
|
522
547
|
}
|
|
548
|
+
if (includeOwnerClips && object.clip !== undefined) result = intersectVisibleBounds(result, graphicClipBounds(object.clip, x, y));
|
|
523
549
|
ancestors.delete(id);
|
|
524
550
|
return result;
|
|
525
551
|
}
|
|
526
552
|
|
|
527
|
-
export function resolveConcreteGraphicBounds(graphicSpec, target, profile) {
|
|
553
|
+
export function resolveConcreteGraphicBounds(graphicSpec, target, profile, {includeOwnerClips = true} = {}) {
|
|
528
554
|
const found = requireGraphic(graphicSpec, target);
|
|
529
555
|
const anchor=found.kind==="item"?found.ownerId:found.id;
|
|
530
|
-
const [x, y] =
|
|
556
|
+
const [x, y] = resolveGraphicParentOffset(graphicSpec, anchor);
|
|
531
557
|
return requireFiniteBounds(clipAncestorBounds(graphicSpec,anchor,found.kind === "item"
|
|
532
|
-
? translateBounds(
|
|
558
|
+
? (!includeOwnerClips || found.owner.clip === undefined ? translateBounds(
|
|
533
559
|
resolvePrimitiveGraphicBounds(found.object.type ?? found.owner.type, found.object.properties, profile),
|
|
534
560
|
x,
|
|
535
561
|
y
|
|
536
|
-
)
|
|
537
|
-
|
|
562
|
+
) : intersectVisibleBounds(translateBounds(
|
|
563
|
+
resolvePrimitiveGraphicBounds(found.object.type ?? found.owner.type, found.object.properties, profile), x, y
|
|
564
|
+
), graphicClipBounds(found.owner.clip, x, y)))
|
|
565
|
+
: objectBounds(graphicSpec, found.id, found.object, new Set(), x, y, profile, includeOwnerClips), includeOwnerClips));
|
|
538
566
|
}
|
|
539
567
|
|
|
540
|
-
export function unionConcreteGraphicBounds(graphicSpec, targets, profile) {
|
|
568
|
+
export function unionConcreteGraphicBounds(graphicSpec, targets, profile, options) {
|
|
541
569
|
if (!Array.isArray(targets)) {
|
|
542
570
|
throw new TypeError("Graphic bounds targets must be an array.");
|
|
543
571
|
}
|
|
544
572
|
return unionBounds(targets.map(target =>
|
|
545
|
-
resolveConcreteGraphicBounds(graphicSpec, target, profile)
|
|
573
|
+
resolveConcreteGraphicBounds(graphicSpec, target, profile, options)
|
|
546
574
|
));
|
|
547
575
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
// A named owner's rectangle uses its existing parent frame; it never rebases items.
|
|
4
|
+
export function validateGraphicClip(value, type) {
|
|
5
|
+
if (type === "canvas") throw new Error("Canvas uses its existing viewport, not an owner clip.");
|
|
6
|
+
if (!isPlainObject(value) || Object.keys(value).length !== 4 ||
|
|
7
|
+
!["x", "y", "width", "height"].every(key => Object.hasOwn(value, key) && Number.isFinite(value[key])) ||
|
|
8
|
+
value.width < 0 || value.height < 0 ||
|
|
9
|
+
!Number.isFinite(value.x + value.width) || !Number.isFinite(value.y + value.height)) {
|
|
10
|
+
throw new TypeError("Graphic clip requires finite x, y and non-negative width, height.");
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function graphicClipBounds(rect, x = 0, y = 0) {
|
|
16
|
+
return {left: x + rect.x, right: x + rect.x + rect.width,
|
|
17
|
+
top: y + rect.y, bottom: y + rect.y + rect.height};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function intersectClipBounds(a, b) {
|
|
21
|
+
return {left: Math.max(a.left, b.left), right: Math.min(a.right, b.right),
|
|
22
|
+
top: Math.max(a.top, b.top), bottom: Math.min(a.bottom, b.bottom)};
|
|
23
|
+
}
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { isGraphicContainerType } from "./graphic.js";
|
|
3
|
+
|
|
4
|
+
export function resolveGraphicChildOrder(graphicSpec, target, value) {
|
|
5
|
+
const found = requireGraphic(graphicSpec, target);
|
|
6
|
+
if (found.kind !== "object" || !isGraphicContainerType(found.object.type)) {
|
|
7
|
+
throw new Error("Graphic children order requires a named Canvas or collection.");
|
|
8
|
+
}
|
|
9
|
+
const children = found.object.children ?? [];
|
|
10
|
+
const existing = new Set(children);
|
|
11
|
+
if (!Array.isArray(value) || value.length !== children.length ||
|
|
12
|
+
new Set(value).size !== children.length ||
|
|
13
|
+
value.some(id => typeof id !== "string" || !existing.has(id))) {
|
|
14
|
+
throw new Error("Graphic children order must be an exact permutation of existing direct children.");
|
|
15
|
+
}
|
|
16
|
+
return [...value];
|
|
17
|
+
}
|
|
2
18
|
|
|
3
19
|
function requireGraphicSpec(graphicSpec) {
|
|
4
20
|
if (
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
2
2
|
import { aggregateRows } from "./aggregate.js";
|
|
3
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
4
|
+
import { scalarKey } from "./transformKeys.js";
|
|
4
5
|
|
|
5
6
|
const STATISTIC_KEYS = Object.freeze(["op", "p"]);
|
|
6
7
|
const SIMPLE_STATISTICS = new Set(["mean", "median", "min", "max"]);
|
|
7
8
|
const POPULATIONS = new Set(["boundData", "visibleItems"]);
|
|
8
9
|
const TRANSFORM_KEYS = Object.freeze(["type", "target"]);
|
|
9
10
|
|
|
11
|
+
export function normalizeReferenceGroupBy(value) {
|
|
12
|
+
if (value === undefined) return undefined;
|
|
13
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
14
|
+
throw new TypeError("Statistical reference groupBy must be a non-empty field name.");
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function partitionReferenceRows(rows, groupBy) {
|
|
20
|
+
if (groupBy === undefined) return [{ rows }];
|
|
21
|
+
const groups = new Map();
|
|
22
|
+
for (const [index, row] of rows.entries()) {
|
|
23
|
+
if (!isPlainObject(row) || !Object.hasOwn(row, groupBy)) {
|
|
24
|
+
throw new Error(`Statistical reference groupBy field "${groupBy}" is missing at item ${index}.`);
|
|
25
|
+
}
|
|
26
|
+
const group = row[groupBy], key = scalarKey(group, "Statistical reference groupBy");
|
|
27
|
+
if (!groups.has(key)) groups.set(key, { key, group, rows: [] });
|
|
28
|
+
groups.get(key).rows.push(row);
|
|
29
|
+
}
|
|
30
|
+
return [...groups.values()];
|
|
31
|
+
}
|
|
10
32
|
|
|
11
33
|
export function normalizeReferenceStatistic(value, label = "Reference statistic") {
|
|
12
34
|
if (!isPlainObject(value)) {
|
package/src/grammar/text.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { cloneAndFreeze } from "../core/immutable.js";
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { schemaField } from "./datasetSchema.js";
|
|
3
|
+
import { readQuantitativeField } from "./scales/fields.js";
|
|
4
|
+
import { applyItemMissingPolicy } from "./itemMissing.js";
|
|
5
|
+
import { validateOptionObject } from "../core/validation.js";
|
|
2
6
|
import { validateConcreteGraphicValue } from "./schemas/concreteGraphic.js";
|
|
3
7
|
import {
|
|
4
8
|
formatValue,
|
|
@@ -11,6 +15,66 @@ export function isSourceOwnedText(layer) {
|
|
|
11
15
|
return layer?.mark?.type === "text" && layer.source !== undefined;
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
export function isIndependentText(layer) {
|
|
19
|
+
return layer?.mark?.type === "text" && layer.source === undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateTextOffsetEncoding(encoding, channel) {
|
|
23
|
+
if (!isPlainObject(encoding)) throw new TypeError(`Text ${channel} encoding must be an object.`);
|
|
24
|
+
const field = Object.hasOwn(encoding, "field");
|
|
25
|
+
const datum = Object.hasOwn(encoding, "datum");
|
|
26
|
+
if (field === datum) throw new Error(`Text ${channel} requires exactly one field or datum.`);
|
|
27
|
+
const allowed = field ? ["field", "fieldType"] : ["datum"];
|
|
28
|
+
if (Object.keys(encoding).some(key => !allowed.includes(key))) {
|
|
29
|
+
throw new Error(`Unsupported Text ${channel} encoding option.`);
|
|
30
|
+
}
|
|
31
|
+
if (field) {
|
|
32
|
+
if (typeof encoding.field !== "string" || encoding.field.length === 0 ||
|
|
33
|
+
encoding.fieldType !== "quantitative") {
|
|
34
|
+
throw new TypeError(`Text ${channel} requires a quantitative field.`);
|
|
35
|
+
}
|
|
36
|
+
} else if (!Number.isFinite(encoding.datum)) {
|
|
37
|
+
throw new TypeError(`Text ${channel} datum must be finite logical pixels.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeTextOffsetAssignment(args, channel) {
|
|
42
|
+
const operation = channel === "dx" ? "encodeDx" : "encodeDy";
|
|
43
|
+
validateOptionObject(args, ["target", "field", "value"], operation);
|
|
44
|
+
const field = Object.hasOwn(args, "field");
|
|
45
|
+
if (field === Object.hasOwn(args, "value")) {
|
|
46
|
+
throw new Error(`${operation} requires exactly one of field or value.`);
|
|
47
|
+
}
|
|
48
|
+
const encoding = field ? { field: args.field, fieldType: "quantitative" } : { datum: args.value };
|
|
49
|
+
validateTextOffsetEncoding(encoding, channel);
|
|
50
|
+
return cloneAndFreeze(encoding);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Resolve missing rows once for positions, content, color and both pixel offsets.
|
|
54
|
+
export function resolveTextOffsetDataset(layer, sourceDataset) {
|
|
55
|
+
const channels = ["dx", "dy"].filter(channel => layer.encoding?.[channel] !== undefined);
|
|
56
|
+
if (channels.length && !isIndependentText(layer)) {
|
|
57
|
+
throw new Error("Numeric text offsets require independent Text marks.");
|
|
58
|
+
}
|
|
59
|
+
const dataset = applyItemMissingPolicy(layer, sourceDataset);
|
|
60
|
+
for (const channel of channels) {
|
|
61
|
+
const encoding = layer.encoding[channel];
|
|
62
|
+
validateTextOffsetEncoding(encoding, channel);
|
|
63
|
+
if (!Object.hasOwn(encoding, "field")) continue;
|
|
64
|
+
if (sourceDataset.values.length === 0 &&
|
|
65
|
+
schemaField(sourceDataset.schema, encoding.field)?.storageType !== "number") {
|
|
66
|
+
throw new Error(`Empty Text ${channel} data requires a known numeric field "${encoding.field}".`);
|
|
67
|
+
}
|
|
68
|
+
readQuantitativeField(dataset.values, encoding.field);
|
|
69
|
+
}
|
|
70
|
+
return dataset;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function textOffsetValue(encoding, row, fallback) {
|
|
74
|
+
if (encoding === undefined) return fallback;
|
|
75
|
+
return Object.hasOwn(encoding, "field") ? row[encoding.field] : encoding.datum;
|
|
76
|
+
}
|
|
77
|
+
|
|
14
78
|
export const TEXT_MARK_STYLE_OPTIONS = Object.freeze([
|
|
15
79
|
"fill", "opacity", "fontSize", "fontFamily", "fontWeight",
|
|
16
80
|
"align", "baseline", "rotation", "dx", "dy", "inheritColor", "lineHeight", "blockAlign"
|
package/src/grammar/ticks.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
alignNumericStep,
|
|
8
8
|
cleanNumericValue,
|
|
9
|
+
nearestNumericIncrement,
|
|
9
10
|
niceNumericStep,
|
|
10
11
|
normalizeNumericRange
|
|
11
12
|
} from "./numeric.js";
|
|
@@ -36,12 +37,64 @@ function ticksForStep(start, end, step, low, high, scale) {
|
|
|
36
37
|
return values;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export function
|
|
40
|
+
export function validateTickRounding(rounding, optional = true) {
|
|
41
|
+
if ((!optional || rounding !== undefined) && !["ceiling", "nearest"].includes(rounding)) {
|
|
42
|
+
throw new TypeError('Tick rounding must be "ceiling" or "nearest".');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateTickSelectionRounding(config, scaleType) {
|
|
47
|
+
validateTickRounding(config.rounding, !Object.hasOwn(config, "rounding"));
|
|
48
|
+
if (config.rounding !== undefined && (config.mode !== "count" || scaleType !== "linear")) {
|
|
49
|
+
throw new Error("Tick rounding requires an automatic linear Cartesian axis.");
|
|
50
|
+
}
|
|
51
|
+
if (config.rounding !== undefined) {
|
|
52
|
+
if (!Number.isInteger(config.count) || config.count <= 0) {
|
|
53
|
+
throw new RangeError("Tick rounding count must be a positive integer.");
|
|
54
|
+
}
|
|
55
|
+
validateGeneratedItemLimit(config.count, "Tick rounding count");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function axisTickSelection(config) {
|
|
60
|
+
return config.mode === "values" ? { mode: "values", values: config.values } : {
|
|
61
|
+
mode: "count", count: config.count,
|
|
62
|
+
...(config.rounding === "nearest" ? { rounding: "nearest" } : {})
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function nearestTicks(low, high, count) {
|
|
67
|
+
let start = low, end = high, scale = 1;
|
|
68
|
+
let increment = nearestNumericIncrement(end - start, count);
|
|
69
|
+
if (increment === null) {
|
|
70
|
+
const normalized = normalizeNumericRange(low, high);
|
|
71
|
+
({ start, end, scale } = normalized);
|
|
72
|
+
increment = nearestNumericIncrement(normalized.span, count);
|
|
73
|
+
}
|
|
74
|
+
if (increment === null) return cloneAndFreeze([low, high]);
|
|
75
|
+
const first = Math.ceil(increment < 0 ? start * -increment : start / increment);
|
|
76
|
+
const last = Math.floor(increment < 0 ? end * -increment : end / increment);
|
|
77
|
+
const length = last - first + 1;
|
|
78
|
+
if (!Number.isFinite(length) || length <= 0) return cloneAndFreeze([low, high]);
|
|
79
|
+
const size = Math.min(length, MAX_GENERATED_ITEMS), values = [];
|
|
80
|
+
for (let index = 0; index < size; index++) {
|
|
81
|
+
const offset = length === size ? index : Math.round(index * (length - 1) / (size - 1));
|
|
82
|
+
const at = first + offset;
|
|
83
|
+
const value = (increment < 0 ? at / -increment : at * increment) * scale;
|
|
84
|
+
if (Number.isFinite(value) && value >= low && value <= high &&
|
|
85
|
+
(values.length === 0 || value > values.at(-1))) values.push(value === 0 ? 0 : value);
|
|
86
|
+
}
|
|
87
|
+
return cloneAndFreeze(values.length > 0 ? values : [low, high]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function niceTicks(domain, count, rounding = "ceiling") {
|
|
91
|
+
validateTickRounding(rounding);
|
|
40
92
|
if (!Number.isInteger(count) || count <= 0) throw new RangeError("Tick count must be a positive integer.");
|
|
41
93
|
const effectiveCount = Math.min(count, MAX_GENERATED_ITEMS);
|
|
42
94
|
const low = Math.min(...domain);
|
|
43
95
|
const high = Math.max(...domain);
|
|
44
96
|
if (low === high) return cloneAndFreeze([low]);
|
|
97
|
+
if (rounding === "nearest") return nearestTicks(low, high, effectiveCount);
|
|
45
98
|
const directSpan = high - low;
|
|
46
99
|
if (Number.isFinite(directSpan) && directSpan > 0) {
|
|
47
100
|
const directStep = niceNumericStep(directSpan, effectiveCount);
|
|
@@ -254,3 +307,41 @@ export function formatTimeTicks(values, domain) {
|
|
|
254
307
|
}
|
|
255
308
|
return cloneAndFreeze(values.map(value => String(value)));
|
|
256
309
|
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
// Fixed numeric display precision belongs to one settled axis, not its dataset.
|
|
313
|
+
// Sorting a copied distinct list preserves the caller's tick and painter order.
|
|
314
|
+
export function formatNumericTickValues(values) {
|
|
315
|
+
if (!Array.isArray(values) || !values.every(Number.isFinite)) {
|
|
316
|
+
throw new TypeError("Numeric tick formatting requires finite tick values.");
|
|
317
|
+
}
|
|
318
|
+
const ordered = [...new Set(values)].sort((a, b) => a - b);
|
|
319
|
+
if (ordered.length === 0) return [];
|
|
320
|
+
let step = Infinity;
|
|
321
|
+
for (let index = 1; index < ordered.length; index += 1) {
|
|
322
|
+
step = Math.min(step, ordered[index] - ordered[index - 1]);
|
|
323
|
+
}
|
|
324
|
+
let precision;
|
|
325
|
+
if (ordered.length === 1) {
|
|
326
|
+
const normalized = Number(ordered[0].toPrecision(12));
|
|
327
|
+
const [coefficient, exponent = "0"] = String(normalized).split("e");
|
|
328
|
+
precision = Math.max(0, (coefficient.split(".")[1]?.length ?? 0) - Number(exponent));
|
|
329
|
+
} else {
|
|
330
|
+
const normalizedStep = Number(step.toPrecision(12));
|
|
331
|
+
precision = Math.max(0, -Math.floor(Math.log10(normalizedStep)));
|
|
332
|
+
}
|
|
333
|
+
if (precision > 12) {
|
|
334
|
+
throw new RangeError("Numeric tick format supports at most 12 decimals.");
|
|
335
|
+
}
|
|
336
|
+
const fixed = (value, digits) => {
|
|
337
|
+
const label = value.toFixed(digits);
|
|
338
|
+
return Number(label) === 0 ? (0).toFixed(digits) : label.replace(/^-/, "−");
|
|
339
|
+
};
|
|
340
|
+
for (; precision <= 12; precision += 1) {
|
|
341
|
+
const labels = ordered.map(value => fixed(value, precision));
|
|
342
|
+
if (new Set(labels).size === labels.length) {
|
|
343
|
+
return values.map(value => fixed(value, precision));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
throw new RangeError("Numeric tick format cannot distinguish values within 12 decimals.");
|
|
347
|
+
}
|
|
@@ -8,12 +8,14 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
|
|
|
8
8
|
density: Object.freeze({ facetTopology: "statistical" }),
|
|
9
9
|
ecdf: Object.freeze({ facetTopology: "statistical" }),
|
|
10
10
|
filter: Object.freeze({ facetTopology: "rowPreserving" }),
|
|
11
|
+
flatten: Object.freeze({ facetTopology: "statistical" }),
|
|
12
|
+
lookup: Object.freeze({ facetTopology: "rowPreserving" }),
|
|
11
13
|
fold: Object.freeze({ facetTopology: "statistical" }),
|
|
12
14
|
gradientProfile: Object.freeze({ facetTopology: "statistical" }),
|
|
13
15
|
horizon: Object.freeze({ facetTopology: "statistical" }),
|
|
14
16
|
interval: Object.freeze({ facetTopology: "statistical" }),
|
|
15
17
|
impute: Object.freeze({ facetTopology: "statistical" }),
|
|
16
|
-
markFilter: Object.freeze({ provenanceTransparent: true }),
|
|
18
|
+
markFilter: Object.freeze({ provenanceTransparent: true, facetTopology: "statistical" }),
|
|
17
19
|
normalize: Object.freeze({ facetTopology: "statistical" }),
|
|
18
20
|
regression: Object.freeze({ facetTopology: "statistical" }),
|
|
19
21
|
regressionStatistics: Object.freeze({ facetTopology: "statistical" }),
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
validateFilterTransform
|
|
30
30
|
} from "./filter.js";
|
|
31
31
|
import { normalizeFoldTransform, validateFoldTransform } from "./fold.js";
|
|
32
|
+
import { normalizeFlattenTransform, validateFlattenTransform } from "./flatten.js";
|
|
33
|
+
import {normalizeLookupTransform,validateLookupTransform} from "./lookup.js";
|
|
32
34
|
import {
|
|
33
35
|
requestedGradientProfileTransform,
|
|
34
36
|
validateGradientProfileTransform
|
|
@@ -381,6 +383,28 @@ const TRANSFORM_POLICIES = Object.freeze({
|
|
|
381
383
|
normalizeEdit: normalizeFilterTransformEdit,
|
|
382
384
|
outputRoles: () => ({})
|
|
383
385
|
}),
|
|
386
|
+
flatten: Object.freeze({
|
|
387
|
+
...findTransformTopology("flatten"),
|
|
388
|
+
validate: validateFlattenTransform,
|
|
389
|
+
materializeOp: "materializeFlattenData",
|
|
390
|
+
replayTransform: requestedTransform,
|
|
391
|
+
editable: true,
|
|
392
|
+
normalizeEdit: normalizeSimple(normalizeFlattenTransform),
|
|
393
|
+
outputRoles: transform => Object.fromEntries(
|
|
394
|
+
transform.fields.map((field, index) => [`value:${field}`, transform.as[index]])
|
|
395
|
+
)
|
|
396
|
+
}),
|
|
397
|
+
lookup: Object.freeze({
|
|
398
|
+
...findTransformTopology("lookup"),
|
|
399
|
+
validate:validateLookupTransform,
|
|
400
|
+
materializeOp:"materializeLookupData",
|
|
401
|
+
replayTransform:requestedTransform,
|
|
402
|
+
editable:true,
|
|
403
|
+
normalizeEdit:normalizeSimple(normalizeLookupTransform),
|
|
404
|
+
outputRoles:transform=>Object.fromEntries(transform.lookup.fields.map((field,index)=>[
|
|
405
|
+
`value:${field}`,transform.lookup.as[index]
|
|
406
|
+
]))
|
|
407
|
+
}),
|
|
384
408
|
fold: Object.freeze({
|
|
385
409
|
...findTransformTopology("fold"),
|
|
386
410
|
validate: validateFoldTransform,
|