ggaction 0.0.21 → 0.0.23
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 +90 -0
- package/README.md +83 -90
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +11 -4
- 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 +3 -3
- 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 +22 -36
- 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/data/timeUnit.js +1 -1
- 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/components.js +1 -4
- 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/regression/create.js +6 -1
- package/src/actions/regression/edit.js +10 -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 +26 -7
- package/src/core/datasetReferences.js +17 -0
- package/src/core/facetAxisPolicy.js +30 -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/curveCommands.js +35 -2
- package/src/grammar/datasetDependencies.js +97 -0
- package/src/grammar/datasetSchema.js +32 -2
- package/src/grammar/facetHeaders.js +1 -0
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +7 -5
- 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/regression/parameters.js +7 -0
- 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/timeUnit.js +9 -3
- 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/guideCollisions.js +9 -3
- 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/facetContentBounds.js +96 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/placement.js +39 -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 +30 -1
- 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 +114 -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 +286 -70
- package/types/scene.d.ts +35 -0
|
@@ -1,63 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { validateAggregate, validateAggregateFieldType } from "../aggregate.js";
|
|
3
|
-
import { isDiscretizedColorScaleType } from "./types.js";
|
|
4
|
-
|
|
5
|
-
export function validateContinuousColorConsumer(layer, encoding, scale, {
|
|
6
|
-
inferAggregate = false,
|
|
7
|
-
channel = "color"
|
|
8
|
-
} = {}) {
|
|
9
|
-
const kind = layer.mark?.type;
|
|
10
|
-
if (["log", "pow", "sqrt", "symlog"].includes(scale.type) && encoding.fieldType !== "quantitative") {
|
|
11
|
-
throw new Error(`Scale type "${scale.type}" requires quantitative color.`);
|
|
12
|
-
}
|
|
13
|
-
if (channel === "stroke") {
|
|
14
|
-
if (!["point", "line", "area", "bar", "rect", "arc", "rule", "tick"].includes(kind)) {
|
|
15
|
-
throw new Error("Continuous stroke requires a supported graphical mark consumer.");
|
|
16
|
-
}
|
|
17
|
-
if (!['quantitative', 'temporal'].includes(encoding.fieldType) ||
|
|
18
|
-
(isDiscretizedColorScaleType(scale.type) && encoding.fieldType !== "quantitative")) {
|
|
19
|
-
throw new Error(`Scale type "${scale.type}" has an incompatible stroke field type.`);
|
|
20
|
-
}
|
|
21
|
-
if (encoding.aggregate !== undefined) {
|
|
22
|
-
throw new Error("Continuous stroke does not support aggregate.");
|
|
23
|
-
}
|
|
24
|
-
if (Object.hasOwn(scale, "unknown") && kind !== "point") {
|
|
25
|
-
throw new Error("Continuous stroke scale unknown currently requires a row-owned point mark.");
|
|
26
|
-
}
|
|
27
|
-
return undefined;
|
|
28
|
-
}
|
|
29
|
-
if (!["point", "bar", "rect", "text", "area"].includes(kind)) {
|
|
30
|
-
throw new Error("Continuous color requires a Point, aggregate Bar, Rect, Text, or quantitative Area consumer.");
|
|
31
|
-
}
|
|
32
|
-
if (!["quantitative", "temporal"].includes(encoding.fieldType) ||
|
|
33
|
-
(isDiscretizedColorScaleType(scale.type) && encoding.fieldType !== "quantitative")) {
|
|
34
|
-
throw new Error(`Scale type "${scale.type}" has an incompatible color field type.`);
|
|
35
|
-
}
|
|
36
|
-
if (Object.hasOwn(scale, "unknown") && kind !== "point") {
|
|
37
|
-
throw new Error("Continuous color scale unknown currently requires a row-owned point mark.");
|
|
38
|
-
}
|
|
39
|
-
if (kind === "area" && encoding.fieldType !== "quantitative") {
|
|
40
|
-
throw new Error("Continuous area color requires a quantitative field.");
|
|
41
|
-
}
|
|
42
|
-
if (kind !== "bar") {
|
|
43
|
-
if (encoding.aggregate !== undefined) throw new Error(`${kind} continuous color does not support aggregate.`);
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
if (["nominal", "ordinal"].includes(layer.encoding?.color?.fieldType)) {
|
|
47
|
-
throw new Error("Continuous bar color cannot replace an existing nominal color layout.");
|
|
48
|
-
}
|
|
49
|
-
if (encoding.fieldType !== "quantitative") {
|
|
50
|
-
throw new Error("Aggregate bar color currently requires a quantitative field.");
|
|
51
|
-
}
|
|
52
|
-
if (resolveBarGrain(layer) !== BAR_GRAINS.aggregate) {
|
|
53
|
-
throw new Error("Continuous bar color requires a complete categorical aggregate bar.");
|
|
54
|
-
}
|
|
55
|
-
const measure = layer.encoding?.[resolveBarChannels(layer).measure];
|
|
56
|
-
const aggregate = encoding.aggregate ?? (inferAggregate && measure?.field === encoding.field ? measure.aggregate : undefined);
|
|
57
|
-
if (aggregate === undefined) {
|
|
58
|
-
throw new Error("Continuous bar color requires aggregate when its field differs from the measure field.");
|
|
59
|
-
}
|
|
60
|
-
validateAggregate(aggregate);
|
|
61
|
-
validateAggregateFieldType(aggregate, encoding.fieldType);
|
|
62
|
-
return aggregate;
|
|
63
|
-
}
|
|
1
|
+
import{isNominalValue as d,readNominalField as s}from"./fields.js";import{BAR_GRAINS as f,resolveBarChannels as p,resolveBarGrain as c}from"../bars/policy.js";import{validateAggregate as w,validateAggregateFieldType as g}from"../aggregate.js";import{isDiscretizedColorScaleType as l}from"./types.js";function m(o,e){if(o.mark?.type!=="tick")return;const{color:r,stroke:t}=o.encoding??{};if(r!==void 0&&t!==void 0||e==="color"&&t!==void 0||e==="stroke"&&r!==void 0)throw new Error("Tick color and stroke cannot coexist; remove the existing encoding before switching channels.");if(r!==void 0&&!["quantitative","temporal"].includes(r.fieldType))throw new Error("Tick color requires a quantitative or temporal field.")}function v(o,e,r,{inferAggregate:t=!1,channel:n="color"}={}){const i=o.mark?.type;if(m(o,n),["log","pow","sqrt","symlog"].includes(r.type)&&e.fieldType!=="quantitative")throw new Error(`Scale type "${r.type}" requires quantitative color.`);if(n==="stroke"){if(!["point","line","area","bar","rect","arc","rule","tick"].includes(i))throw new Error("Continuous stroke requires a supported graphical mark consumer.");if(!["quantitative","temporal"].includes(e.fieldType)||l(r.type)&&e.fieldType!=="quantitative")throw new Error(`Scale type "${r.type}" has an incompatible stroke field type.`);if(e.aggregate!==void 0)throw new Error("Continuous stroke does not support aggregate.");if(Object.hasOwn(r,"unknown")&&i!=="point")throw new Error("Continuous stroke scale unknown currently requires a row-owned point mark.");return}if(!["point","line","bar","rect","text","area","arc","tick"].includes(i))throw new Error("Continuous color requires a Point, Line, aggregate Bar, Rect, Text, Arc, Tick, or quantitative Area consumer.");if(!["quantitative","temporal"].includes(e.fieldType)||l(r.type)&&e.fieldType!=="quantitative")throw new Error(`Scale type "${r.type}" has an incompatible color field type.`);if(Object.hasOwn(r,"unknown")&&i!=="point")throw new Error("Continuous color scale unknown currently requires a row-owned point mark.");if(i==="area"&&e.fieldType!=="quantitative")throw new Error("Continuous area color requires a quantitative field.");if(i!=="bar"){if(e.aggregate!==void 0)throw new Error(`${i} continuous color does not support aggregate.`);return}if(["nominal","ordinal"].includes(o.encoding?.color?.fieldType))throw new Error("Continuous bar color cannot replace an existing nominal color layout.");if(e.fieldType!=="quantitative")throw new Error("Aggregate bar color currently requires a quantitative field.");if(c(o)!==f.aggregate)throw new Error("Continuous bar color requires a complete categorical aggregate bar.");const u=o.encoding?.[p(o).measure],a=e.aggregate??(t&&u?.field===e.field?u.aggregate:void 0);if(a===void 0)throw new Error("Continuous bar color requires aggregate when its field differs from the measure field.");return w(a),g(a,e.fieldType),a}function C(o,e,r,t="color"){if(!Object.hasOwn(r,"unknown")||o.mark?.type==="point")return!1;const n=o.encoding??{};if(t==="strokeDash"){if(!["line","rule"].includes(o.mark?.type)||!["nominal","ordinal"].includes(e.fieldType)||r.type!=="ordinal"||n.parallel!==void 0||n.theta!==void 0||n.radius!==void 0||[n.x,n.y,n.x2,n.y2].some(i=>i?.aggregate!==void 0||i?.bin!==void 0))throw new Error("StrokeDash unknown requires a Cartesian line or rule with categorical ordinal mapping.");return!1}if(o.mark?.type!=="line"||t!=="color"||!["nominal","ordinal"].includes(e.fieldType)||r.type!=="ordinal"||!Array.isArray(r.domain)||r.domain.length===0||n.parallel!==void 0||n.theta!==void 0||n.radius!==void 0||[n.x,n.y].some(i=>i?.aggregate!==void 0||i?.bin!==void 0))throw new Error("Categorical unknown requires a row-owned point or a Cartesian line with explicit ordinal domain.");return!0}function T(o,e){return s([],e),o.map((r,t)=>{const n=r[e];if(n==null||d(n))return n;throw new TypeError(`Field "${e}" must contain a nominal value at row ${t}.`)})}export{T as readCategoricalLineColor,C as validateCategoricalColorConsumer,v as validateContinuousColorConsumer,m as validateTickColorChannels};
|
|
@@ -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
|
+
}
|
package/src/grammar/timeUnit.js
CHANGED
|
@@ -41,7 +41,7 @@ export const TIME_UNITS = cloneAndFreeze([
|
|
|
41
41
|
|
|
42
42
|
const TRANSFORM_KEYS = Object.freeze([
|
|
43
43
|
"type", "field", "unit", "as", "temporalUnit", "timeZone",
|
|
44
|
-
"weekStartsOn", "weekRule"
|
|
44
|
+
"weekStartsOn", "weekRule", "missing"
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
47
|
const CALENDAR_FORMATTERS = new Map();
|
|
@@ -93,6 +93,9 @@ export function validateTimeUnitTransform(transform) {
|
|
|
93
93
|
if (transform.type !== "timeUnit") {
|
|
94
94
|
throw new Error(`Unsupported time-unit transform "${transform.type}".`);
|
|
95
95
|
}
|
|
96
|
+
if (Object.hasOwn(transform, "missing") && !["error", "null"].includes(transform.missing)) {
|
|
97
|
+
throw new Error('Time-unit missing must be "error" or "null".');
|
|
98
|
+
}
|
|
96
99
|
validateTemporalUnit(transform.temporalUnit);
|
|
97
100
|
const field = requireField(transform.field, "Time-unit field");
|
|
98
101
|
const output = requireField(transform.as, "Time-unit output field");
|
|
@@ -127,7 +130,8 @@ export function normalizeTimeUnitTransform(args = {}) {
|
|
|
127
130
|
const as = args.as === undefined ? `${field}_${unit}` : args.as;
|
|
128
131
|
const transform = { type: "timeUnit", field, unit, as,
|
|
129
132
|
...(temporalUnit === undefined ? {} : { temporalUnit }),
|
|
130
|
-
...(timeZone === undefined ? {} : { timeZone })
|
|
133
|
+
...(timeZone === undefined ? {} : { timeZone }),
|
|
134
|
+
...(Object.hasOwn(args, "missing") ? { missing: args.missing } : {}) };
|
|
131
135
|
if (unit === "week") {
|
|
132
136
|
transform.weekStartsOn = args.weekStartsOn ?? 1;
|
|
133
137
|
transform.weekRule = args.weekRule ?? "calendar";
|
|
@@ -319,7 +323,8 @@ export function deriveTimeUnitRows(rows, transform) {
|
|
|
319
323
|
throw new TypeError("Time-unit source must contain plain row objects.");
|
|
320
324
|
}
|
|
321
325
|
const values = rows.map((row, index) => {
|
|
322
|
-
|
|
326
|
+
const missing = !Object.hasOwn(row, transform.field) || row[transform.field] === null;
|
|
327
|
+
if (!Object.hasOwn(row, transform.field) && transform.missing !== "null") {
|
|
323
328
|
throw new Error(
|
|
324
329
|
`Time-unit source does not contain field "${transform.field}" at row ${index}.`
|
|
325
330
|
);
|
|
@@ -327,6 +332,7 @@ export function deriveTimeUnitRows(rows, transform) {
|
|
|
327
332
|
if (Object.hasOwn(row, transform.as)) {
|
|
328
333
|
throw new Error(`Time-unit output field "${transform.as}" already exists.`);
|
|
329
334
|
}
|
|
335
|
+
if (missing && transform.missing === "null") return { ...row, [transform.as]: null };
|
|
330
336
|
const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index, transform.temporalUnit);
|
|
331
337
|
return {
|
|
332
338
|
...row,
|