ggaction 0.0.18 → 0.0.19
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 +50 -0
- package/README.md +6 -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 +1 -1
- package/src/actions/canvas/fitting.js +6 -2
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +2 -0
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +24 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +5 -2
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/remove.js +11 -6
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/facets/actions.js +18 -7
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +58 -8
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +37 -27
- package/src/actions/guides/axes/lines.js +22 -20
- package/src/actions/guides/axes/remove.js +10 -6
- package/src/actions/guides/axes/tickGroups.js +8 -7
- package/src/actions/guides/axes/ticks.js +31 -22
- package/src/actions/guides/axes/titles.js +53 -60
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +3 -3
- package/src/actions/guides/grids/grid.js +1 -1
- package/src/actions/guides/grids/resolve.js +48 -51
- package/src/actions/guides/legends/blocks.js +43 -19
- package/src/actions/guides/legends/categorical/actions.js +30 -7
- package/src/actions/guides/legends/categorical/components.js +3 -10
- package/src/actions/guides/legends/categorical/index.js +2 -0
- package/src/actions/guides/legends/categorical/layout.js +9 -13
- package/src/actions/guides/legends/categorical/options.js +8 -2
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +9 -2
- package/src/actions/guides/legends/categorical/symbols.js +22 -8
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +27 -13
- package/src/actions/guides/legends/continuous/opacity.js +16 -11
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +6 -5
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +6 -3
- package/src/actions/guides/legends/size.js +16 -6
- package/src/actions/guides/legends/strokeWidth.js +2 -0
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +24 -5
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/marks/bar/edit.js +23 -12
- package/src/actions/marks/bar/materialize.js +6 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/remove.js +10 -10
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/edit.js +15 -3
- package/src/actions/scales/channels.js +2 -2
- package/src/actions/scales/create.js +3 -3
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/editPolicy.js +3 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/theme/reconcile.js +50 -23
- package/src/core/compositionState.js +2 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +2 -2
- package/src/core/validation.js +9 -0
- package/src/core/vocabulary.js +3 -0
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/computed.js +23 -1
- package/src/grammar/datasetSchema.js +10 -2
- package/src/grammar/displayLabels.js +8 -0
- package/src/grammar/facets/guides.js +54 -31
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +9 -0
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/regression/derive.js +9 -1
- package/src/grammar/regression/models.js +60 -0
- package/src/grammar/regression/parameters.js +9 -1
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +3 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/summary.js +1 -1
- package/src/grammar/timeUnit.js +6 -4
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +11 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +17 -4
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +52 -6
- package/src/materialization/facetGuides/preparation.js +7 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +13 -14
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/validation.js +174 -1
- package/src/persistence.js +1 -1
- package/src/renderers/canvas/index.js +20 -10
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/rect.js +1 -1
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +10 -3
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +5 -0
- package/types/program.d.ts +206 -61
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { validateOptionObject, validateNonEmptyString } from "../../core/validation.js";
|
|
3
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
4
|
+
import { LEGEND_CONFIG_KINDS, LEGEND_POSITIONS } from "../../core/vocabulary.js";
|
|
5
|
+
import { describeLegendBlock } from "../legendBlocks.js";
|
|
6
|
+
import { validateLegendGroupState } from "../legendGroups.js";
|
|
7
|
+
import { resolveSharedFacetLegends, sameFacetGuideValue as same } from "./guides.js";
|
|
8
|
+
|
|
9
|
+
const CONFIG_KEYS = Object.freeze([
|
|
10
|
+
"kind", "target", "channels", "scales", "scale", "field", "fieldType", "domain", "title", "titleVisible",
|
|
11
|
+
"inferredTitle", "inferredSymbol", "inferredStrokeWidth", "inferredOffset", "position", "align", "offset", "direction", "columns",
|
|
12
|
+
"titlePosition", "symbol", "labels", "titleStyle", "itemGap", "layout", "overflow", "border", "gradient",
|
|
13
|
+
"count", "sampling", "representation", "gradientDirection", "inheritAppearance", "inheritPosition", "blockOverrides",
|
|
14
|
+
"combine", "sizeAreas", "clipHeight"
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function isAuthoredFacetLegend(value) {
|
|
18
|
+
return isPlainObject(value) && value.type === "authored-legend";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateAuthoredFacetLegend(program, intent) {
|
|
22
|
+
validateOptionObject(intent, ["type", "blocks", "groups"], "Stored parent legend");
|
|
23
|
+
if (!isAuthoredFacetLegend(intent) || Object.keys(intent).length !== 3 ||
|
|
24
|
+
!isPlainObject(intent.blocks) || Object.keys(intent.blocks).length === 0 || !isPlainObject(intent.groups)) {
|
|
25
|
+
throw new Error("Stored parent legend requires its closed authored block intent.");
|
|
26
|
+
}
|
|
27
|
+
validateLegendGroupState(intent.groups);
|
|
28
|
+
const configs = {};
|
|
29
|
+
for (const [kind, block] of Object.entries(intent.blocks)) {
|
|
30
|
+
if (!LEGEND_CONFIG_KINDS.includes(kind)) throw new Error("Stored parent legend has an unknown block kind.");
|
|
31
|
+
validateOptionObject(block, ["config", "guide"], "Stored parent legend block");
|
|
32
|
+
validateOptionObject(block.config, kind === "colorSize" ? [...CONFIG_KEYS, "colorScale", "sizeScale"] : CONFIG_KEYS, "Stored parent legend config");
|
|
33
|
+
const config = block.config;
|
|
34
|
+
validateUserId(config.target, "Parent legend target");
|
|
35
|
+
if (!LEGEND_POSITIONS.includes(config.position)) throw new Error("Stored parent legend requires a native position.");
|
|
36
|
+
for (const flag of ["inferredTitle", "inferredSymbol", "inferredStrokeWidth", "inferredOffset", "titleVisible", "inheritAppearance", "inheritPosition"]) {
|
|
37
|
+
if (config[flag] !== undefined && typeof config[flag] !== "boolean") throw new Error(`Stored parent legend ${flag} must be boolean.`);
|
|
38
|
+
}
|
|
39
|
+
const descriptor = describeLegendBlock(program, kind, config);
|
|
40
|
+
if (block.guide !== undefined) {
|
|
41
|
+
validateOptionObject(block.guide, kind === "colorSize" ? ["colorScale", "sizeScale", "title"]
|
|
42
|
+
: ["scale", "scales", "channels", "title", "order"], "Stored parent legend meaning");
|
|
43
|
+
if (kind === "colorSize" && (block.guide.colorScale !== config.colorScale || block.guide.sizeScale !== config.sizeScale)) {
|
|
44
|
+
throw new Error("Stored parent color-size meaning must match both block scale bindings.");
|
|
45
|
+
}
|
|
46
|
+
if (block.guide.title !== undefined) validateNonEmptyString(block.guide.title, "Parent legend title");
|
|
47
|
+
if (block.guide.channels !== undefined && !same([...block.guide.channels].sort(), descriptor.channels)) {
|
|
48
|
+
throw new Error("Stored parent legend meaning must match its block channels.");
|
|
49
|
+
}
|
|
50
|
+
if (block.guide.scale !== undefined && block.guide.scale !== descriptor.scaleIds[0] ||
|
|
51
|
+
block.guide.scales !== undefined && !same(block.guide.scales, descriptor.scaleIds)) {
|
|
52
|
+
throw new Error("Stored parent legend meaning must match its block scale binding.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!same(config, program.guideConfigs.legend?.[kind])) throw new Error("Parent legend materialization must match its authored owner.");
|
|
56
|
+
configs[kind] = config;
|
|
57
|
+
}
|
|
58
|
+
if (!same(Object.keys(configs).sort(), Object.keys(program.guideConfigs.legend ?? {}).sort()) ||
|
|
59
|
+
!same(intent.groups, program.guideConfigs.legendGroups ?? {})) {
|
|
60
|
+
throw new Error("Parent legend materialization must contain exactly its authored blocks and groups.");
|
|
61
|
+
}
|
|
62
|
+
resolveSharedFacetLegends(program.compositionSpec.children.map(id => ({ id,
|
|
63
|
+
guideConfigs: { legend: configs, legendGroups: intent.groups }, resolvedScales: program.children[id].resolvedScales })));
|
|
64
|
+
return intent;
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isPlainObject as i}from"../core/immutable.js";import{validateGeneratedItemLimit as o}from"../core/validation.js";function n(e,s="Guide strokeDash"){if(!Array.isArray(e)||e.length%2!==0||!e.every(r=>Number.isFinite(r)&&r>=0))throw new TypeError(`${s} must be an even-length array of non-negative finite numbers.`);return o(e.length,s),e}function u(e,{reset:s=!0}={}){if(e==="auto"&&s)return;if(!Array.isArray(e))throw new TypeError("Guide strokeDashMap must be an array.");o(e.length,"Guide strokeDashMap count");const r=new Set,a=e.map(t=>{if(!i(t)||Reflect.ownKeys(t).length!==2||!Object.hasOwn(t,"value")||!Object.hasOwn(t,"strokeDash"))throw new TypeError("Guide strokeDashMap entries must contain exactly value and strokeDash.");if(!["string","boolean"].includes(typeof t.value)&&!Number.isFinite(t.value))throw new TypeError("Guide strokeDashMap values must be strings, booleans, or finite numbers.");if(r.has(t.value))throw new Error("Guide strokeDashMap contains a duplicate typed value.");return r.add(t.value),n(t.strokeDash),Object.freeze({value:t.value,strokeDash:Object.freeze([...t.strokeDash])})});return a.length===0?void 0:Object.freeze(a)}function d(e,s){const r={...e};if(Object.hasOwn(s,"strokeDash")&&(n(s.strokeDash),r.strokeDash=[...s.strokeDash]),Object.hasOwn(s,"strokeDashMap")){const a=u(s.strokeDashMap);a===void 0?delete r.strokeDashMap:r.strokeDashMap=a}return r}function D(e,s){const r=new Map(s.strokeDashMap?.map(a=>[a.value,a.strokeDash]));return e.map(a=>r.get(a)??s.strokeDash??[])}export{u as normalizeGuideDashMap,d as patchGuideDash,D as resolveGuideDashes,n as validateGuideDash};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LEGEND_CHANNELS as d,LEGEND_CONFIG_KINDS as h}from"../core/vocabulary.js";import{isEnumeratedSizeScaleType as u}from"./scales/size.js";function y(e,r){if(!Array.isArray(e))throw new TypeError(`${r} channels must be an array.`);if(e.length===0)throw new Error(`${r} channels must select at least one channel.`);const s=new Set;for(const t of e){if(!d.includes(t))throw new Error(`Unsupported legend channel "${t}".`);if(s.has(t))throw new Error(`${r} channels contains duplicate "${t}".`);s.add(t)}}const o=Object.freeze(["series","color","stroke"]),a=Object.freeze({size:"size",opacity:"opacity",strokeWidth:"strokeWidth",gradient:"color",interval:"color",strokeGradient:"stroke",strokeInterval:"stroke"}),c=(e,r)=>e<r?-1:e>r?1:0;function w(e,r){const s=e==="colorSize"?["color","size"]:o.includes(e)?r?.channels:a[e]===void 0?void 0:[a[e]];if(!Array.isArray(s)||s.length===0)throw new Error(`Legend block ${e} requires at least one channel.`);return y(s,`Legend block ${e}`),Object.freeze([...s].sort(c))}function p(e){if(!Array.isArray(e)||e.length===0)throw new TypeError("Legend block key requires a non-empty channel array.");return JSON.stringify([...e].sort(c))}function b(e,r,s){if(!h.includes(r))throw new Error(`Unknown legend block kind "${r}".`);const t=w(r,s),n=r==="colorSize"?[s.colorScale,s.sizeScale]:o.includes(r)?s?.scales:[s?.scale];if(!Array.isArray(n)||n.length===0||n.some(l=>typeof l!="string"||e.resolvedScales[l]===void 0))throw new Error(`Legend block ${r} requires resolved scales.`);const i=o.includes(r)?"categorical":r==="colorSize"?"sampled":r==="size"?u(e.resolvedScales[n[0]].type)?"discrete-size":"sampled":["opacity","strokeWidth"].includes(r)||r==="gradient"&&s.representation==="symbols"?"sampled":["gradient","strokeGradient"].includes(r)?"gradient":"interval";return Object.freeze({target:s.target,kind:r,family:i,key:p(t),channels:t,scaleIds:Object.freeze([...n]),config:s})}function g(e,r){return e.symbol.layers.length===1&&(e.kind==="series"&&r==="line"||["color","stroke"].includes(e.kind)&&r==="swatch")?`${e.kind}LegendSymbols`:`${e.kind}LegendSymbol${{line:"Lines",point:"Points",swatch:"Swatches"}[r]}`}export{g as categoricalLegendSymbolId,b as describeLegendBlock,w as legendBlockChannels,p as legendBlockKey,y as validateLegendChannels};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Legend borders use one closed rectangular path, so odd dash duplication is not implicit.
|
|
2
|
+
export function normalizeLegendBorderDash(value, label = "Legend border strokeDash") {
|
|
3
|
+
if (value === undefined) return undefined;
|
|
4
|
+
if (!Array.isArray(value) || value.length % 2 !== 0 ||
|
|
5
|
+
![...value].every(item => Number.isFinite(item) && item >= 0)) {
|
|
6
|
+
throw new TypeError(`${label} must be an even-length array of non-negative finite numbers.`);
|
|
7
|
+
}
|
|
8
|
+
return Object.freeze([...value]);
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function normalizeLegendClipHeight(value,{edit=false}={}){
|
|
2
|
+
if(value===undefined||edit&&value==='auto')return undefined;
|
|
3
|
+
if(!Number.isFinite(value)||value<=0)throw new RangeError('Legend clipHeight must be a positive finite number.');
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
export function validateLegendClip(kind,config){
|
|
7
|
+
if(config.clipHeight===undefined)return;
|
|
8
|
+
normalizeLegendClipHeight(config.clipHeight);
|
|
9
|
+
const supported=['size','opacity','colorSize'].includes(kind)||kind==='gradient'&&config.representation==='symbols'||
|
|
10
|
+
['series','color','stroke'].includes(kind)&&config.symbol?.layers?.some(layer=>layer.type==='point');
|
|
11
|
+
if(!supported)throw new Error('Legend clipHeight requires a point-bearing sample block.');
|
|
12
|
+
}
|
|
13
|
+
export function patchLegendClipHeight(config,value){
|
|
14
|
+
const {clipHeight:_old,...rest}=config,clipHeight=normalizeLegendClipHeight(value,{edit:true});
|
|
15
|
+
return clipHeight===undefined?rest:{...rest,clipHeight};
|
|
16
|
+
}
|
|
17
|
+
export function clippedSampleBounds(bounds,height){
|
|
18
|
+
return height===undefined?bounds:{...bounds,top:Math.max(bounds.top,-height/2),bottom:Math.min(bounds.bottom,height/2)};
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { sameOrderedValues, validateGeneratedItemLimit } from "../core/validation.js";
|
|
2
|
+
import { mapOrdinalValues } from "./scales/index.js";
|
|
3
|
+
import { readNominalField } from "./scales/fields.js";
|
|
4
|
+
import { resolveLegendOrderDomain } from "./categoryOrder.js";
|
|
5
|
+
|
|
6
|
+
export function isSizeShapeChannels(channels) {
|
|
7
|
+
return Array.isArray(channels) && channels.length === 2 &&
|
|
8
|
+
channels.includes('shape') && channels.includes('size');
|
|
9
|
+
}
|
|
10
|
+
export function validateLegendCombine(value) {
|
|
11
|
+
if(value===undefined)return;
|
|
12
|
+
if (!['separate','size-shape','color-size'].includes(value)) throw new Error('Legend combine must be separate, size-shape, or color-size.');
|
|
13
|
+
}
|
|
14
|
+
export function validateMappedSizeShapeSymbol(symbol) {
|
|
15
|
+
if(symbol===undefined||symbol==='auto')return;
|
|
16
|
+
const layers=symbol.layers??[];
|
|
17
|
+
if(layers.some(layer=>layer.type==='point'&&(Object.hasOwn(layer,'shape')||Object.hasOwn(layer,'size')))) {
|
|
18
|
+
throw new Error('Combined size-shape symbols cannot override mapped shape or size.');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveSizeShapeMapping({layer,scales,order,rows}) {
|
|
23
|
+
const shape=layer?.encoding?.shape,size=layer?.encoding?.size;
|
|
24
|
+
const shapeScale=scales[shape?.scale],sizeScale=scales[size?.scale];
|
|
25
|
+
if(layer?.mark?.type!=="point"||shape?.field!==size?.field||
|
|
26
|
+
!["nominal","ordinal"].includes(size?.fieldType)||shapeScale?.type!=="ordinal"||
|
|
27
|
+
sizeScale?.type!=="ordinal"||!sameOrderedValues(shapeScale.domain,sizeScale.domain)) {
|
|
28
|
+
throw new Error("Combined size-shape requires one categorical point field and identical ordered domains.");
|
|
29
|
+
}
|
|
30
|
+
validateGeneratedItemLimit(shapeScale.domain.length,"Categorical legend item count");
|
|
31
|
+
if(shapeScale.domain.length===0||sizeScale.range.some(area=>!Number.isFinite(area)||area<0)) {
|
|
32
|
+
throw new Error("Combined size-shape requires a nonempty domain and finite nonnegative areas.");
|
|
33
|
+
}
|
|
34
|
+
let linked;
|
|
35
|
+
if(order?.channel!==undefined){
|
|
36
|
+
const position=layer.encoding[order.channel];
|
|
37
|
+
if(!["nominal","ordinal"].includes(position?.fieldType)||position.field!==shape.field) {
|
|
38
|
+
throw new Error("Linked legend order requires the same categorical field on its target.");
|
|
39
|
+
}
|
|
40
|
+
linked=scales[position.scale]?.domain;
|
|
41
|
+
}
|
|
42
|
+
const domain=resolveLegendOrderDomain(shapeScale.domain,order,linked,
|
|
43
|
+
order?.values===undefined?undefined:readNominalField(rows,shape.field));
|
|
44
|
+
return {domain,sizeAreas:mapOrdinalValues(domain,sizeScale.domain,sizeScale.range)};
|
|
45
|
+
}
|
|
@@ -16,3 +16,12 @@ export function normalizeLegendGradient(patch, previous = DEFAULT_LEGEND_GRADIEN
|
|
|
16
16
|
validateLegendGradient(value);
|
|
17
17
|
return value;
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export function normalizeGradientDirection(value, label = "Gradient legend direction") {
|
|
22
|
+
if (value === undefined) return undefined;
|
|
23
|
+
if (value !== "horizontal" && value !== "vertical") {
|
|
24
|
+
throw new Error(`${label} must be horizontal or vertical.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateOptionObject as
|
|
1
|
+
import{validateOptionObject as o,validateNonNegativeFinite as d}from"../core/validation.js";import{validateUserId as l}from"../core/identifiers.js";import{PLOT_LEGEND_POSITIONS as i,LEGEND_POSITIONS as a,LEGEND_BLOCK_LAYOUTS as f}from"../core/vocabulary.js";function c(n){o(n,void 0,"Legend groups");for(const[t,r]of Object.entries(n)){if(l(t,"Legend group target"),o(r,["placement","blockLayout","inferredOffset"],"Legend group"),!f.includes(r.blockLayout))throw new Error("Legend group blockLayout must be row, column or wrap.");if(r.inferredOffset!==void 0&&typeof r.inferredOffset!="boolean")throw new TypeError("Legend group inferredOffset must be boolean.");const e=r.placement;if(o(e,["at","position","align","offset"],"Legend group placement"),Object.hasOwn(e,"at")){if(Object.keys(e).length!==1)throw new Error("Legend group at cannot coexist with an edge anchor.");if(o(e.at,["x","y"],"Legend group at"),!Number.isFinite(e.at.x)||!Number.isFinite(e.at.y))throw new Error("Legend group at requires finite x and y.")}else{if(Object.keys(e).length!==3||!a.includes(e.position))throw new Error("Legend group requires a complete edge placement.");if(!["left","center","right"].includes(e.align))throw new Error("Unsupported legend group alignment.");if((i.includes(e.position)||["left","right"].includes(e.position))&&e.align!=="center")throw new Error("Side and in-plot legend groups require center alignment.");d(e.offset,"Legend group offset")}}return n}function s(n){const t=Object.values(n?.blockOverrides??{}).map(r=>r.position).filter(r=>r!==void 0);if(t.length>1)throw new Error("Legend family has ambiguous local positions.");if(t.length&&!a.includes(t[0]))throw new Error("Invalid local legend position.");return t[0]}function m(n){const t=s(n);if(t!==void 0)return{position:t,align:["top","bottom"].includes(t)?n.align??"center":"center",offset:i.includes(t)?8:n.offset??(n.channels===void 0?30:8),inferredOffset:n.inferredOffset??!0}}export{m as localLegendPlacement,s as localLegendPosition,c as validateLegendGroupState};
|
|
@@ -14,7 +14,9 @@ export const POINT_SHAPES = cloneAndFreeze([
|
|
|
14
14
|
"cross",
|
|
15
15
|
"star",
|
|
16
16
|
"hexagon",
|
|
17
|
-
"wye"
|
|
17
|
+
"wye",
|
|
18
|
+
"wedge",
|
|
19
|
+
"arrow"
|
|
18
20
|
]);
|
|
19
21
|
|
|
20
22
|
const SHAPE_SET = new Set(POINT_SHAPES);
|
|
@@ -98,7 +100,19 @@ function shapePolygon(shape) {
|
|
|
98
100
|
cross: rotate(plus, Math.PI / 4),
|
|
99
101
|
star: alternatingPolygon(5, 0.45),
|
|
100
102
|
hexagon: regularPolygon(6),
|
|
101
|
-
wye: alternatingPolygon(3, 0.28)
|
|
103
|
+
wye: alternatingPolygon(3, 0.28),
|
|
104
|
+
wedge: [
|
|
105
|
+
{ x: 0, y: -2 / Math.sqrt(3) },
|
|
106
|
+
{ x: -1 / 4, y: 1 / Math.sqrt(3) },
|
|
107
|
+
{ x: 1 / 4, y: 1 / Math.sqrt(3) }
|
|
108
|
+
],
|
|
109
|
+
// The directional arrow is anchored at its bounding-box center, not its centroid.
|
|
110
|
+
arrow: [
|
|
111
|
+
{ x: -1 / 7, y: 1 }, { x: 1 / 7, y: 1 },
|
|
112
|
+
{ x: 1 / 7, y: -1 / 8 }, { x: 2 / 5, y: -1 / 8 },
|
|
113
|
+
{ x: 0, y: -1 }, { x: -2 / 5, y: -1 / 8 },
|
|
114
|
+
{ x: -1 / 7, y: -1 / 8 }
|
|
115
|
+
]
|
|
102
116
|
}[shape];
|
|
103
117
|
}
|
|
104
118
|
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
readNominalField,
|
|
4
4
|
readQuantitativeField
|
|
5
5
|
} from "../scales/index.js";
|
|
6
|
-
import { fitRegressionGroup, predictRegressionAt } from "./models.js";
|
|
6
|
+
import { fitRegressionGroup, predictRegressionAt, linearRegressionStatistics } from "./models.js";
|
|
7
7
|
import { requireFiniteResult } from "../numeric.js";
|
|
8
8
|
import {
|
|
9
9
|
validateGeneratedItemLimit,
|
|
@@ -81,6 +81,7 @@ export function deriveRegression(values, {
|
|
|
81
81
|
}
|
|
82
82
|
const groups = [...groupedRows.keys()];
|
|
83
83
|
const models = [];
|
|
84
|
+
const statistics = [];
|
|
84
85
|
const rows = [];
|
|
85
86
|
let work = 0;
|
|
86
87
|
|
|
@@ -119,6 +120,10 @@ export function deriveRegression(values, {
|
|
|
119
120
|
parameters
|
|
120
121
|
});
|
|
121
122
|
models.push({ ...(groupBy === undefined ? {} : { group }), ...model, xValues });
|
|
123
|
+
if (parameters.method === "linear") statistics.push({
|
|
124
|
+
...(groupBy === undefined ? {} : { group }),
|
|
125
|
+
...linearRegressionStatistics(groupRows, model, { x, y })
|
|
126
|
+
});
|
|
122
127
|
|
|
123
128
|
for (const xValue of xValues) {
|
|
124
129
|
const prediction = predictRegressionAt(model, xValue, parameters);
|
|
@@ -163,6 +168,9 @@ export function deriveRegression(values, {
|
|
|
163
168
|
parameters,
|
|
164
169
|
groups,
|
|
165
170
|
models,
|
|
171
|
+
...(parameters.method === "linear" ? {
|
|
172
|
+
fitStatistics: { version: 1, method: "linear", groups: statistics }
|
|
173
|
+
} : {}),
|
|
166
174
|
values: rows,
|
|
167
175
|
...(missing === undefined ? {} : {
|
|
168
176
|
report: {
|
|
@@ -477,7 +477,62 @@ function predictLoessAt(model, xValue) {
|
|
|
477
477
|
}, "LOESS regression prediction").prediction;
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
+
function fitExponentialGroup(rows, { x, y, group }) {
|
|
481
|
+
const label = `Regression group "${group === undefined ? "all" : String(group)}"`;
|
|
482
|
+
if (rows.length < 2) throw new Error(`${label} requires at least two rows.`);
|
|
483
|
+
if (rows.some(row => !(row[y] > 0))) {
|
|
484
|
+
throw new RangeError("Exponential regression y values must be strictly positive.");
|
|
485
|
+
}
|
|
486
|
+
const xValues = rows.map(row => row[x]);
|
|
487
|
+
if (new Set(xValues).size < 2) throw new Error(`${label} requires varying x values.`);
|
|
488
|
+
const responseScale = maximumMagnitude(rows.map(row => row[y]));
|
|
489
|
+
const xMagnitude = maximumMagnitude(xValues) || 1;
|
|
490
|
+
const weights = rows.map(row => row[y] / responseScale);
|
|
491
|
+
const weightSum = weights.reduce((sum, value) => sum + value, 0);
|
|
492
|
+
const logs = rows.map(row => Math.log(row[y]));
|
|
493
|
+
const meanX = restoreFiniteScale(rows.reduce((sum, row, index) =>
|
|
494
|
+
sum + weights[index] * (row[x] / xMagnitude), 0) / weightSum,
|
|
495
|
+
xMagnitude, `${label} weighted x mean`);
|
|
496
|
+
const meanLogY = logs.reduce((sum, value, index) => sum + weights[index] * value, 0) / weightSum;
|
|
497
|
+
const [centeredX, xScale] = normalizedCoordinates(xValues, meanX);
|
|
498
|
+
const variance = centeredX.reduce((sum, value, index) => sum + weights[index] * value ** 2, 0);
|
|
499
|
+
if (variance === 0) throw new Error(`${label} requires varying x values.`);
|
|
500
|
+
const covariance = centeredX.reduce((sum, value, index) =>
|
|
501
|
+
sum + weights[index] * value * (logs[index] - meanLogY), 0);
|
|
502
|
+
const rate = restoreFiniteRatio(covariance / variance, 1, xScale, `${label} exponential rate`);
|
|
503
|
+
const logAmplitude = requireFiniteResult(meanLogY - rate * meanX, `${label} exponential log amplitude`);
|
|
504
|
+
const amplitude = requireFiniteResult(Math.exp(logAmplitude), `${label} exponential amplitude`);
|
|
505
|
+
if (amplitude === 0) throw new RangeError("Exponential regression amplitude must be positive and representable.");
|
|
506
|
+
return { count: rows.length, amplitude, rate, logAmplitude };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function linearRegressionStatistics(rows, model, { x, y }) {
|
|
510
|
+
const responseScale = maximumMagnitude(rows.map(row => row[y])) || 1;
|
|
511
|
+
const [coordinates, xScale] = normalizedCoordinates(rows.map(row => row[x]), model.meanX);
|
|
512
|
+
const normalizedSlope = model.normalizedResponseSlope ?? (
|
|
513
|
+
Number.isFinite(model.slope * xScale) && (model.slope * xScale !== 0 || model.slope === 0)
|
|
514
|
+
? model.slope * xScale / responseScale
|
|
515
|
+
: restoreFiniteRatio(model.slope, xScale, responseScale, "Linear regression normalized slope")
|
|
516
|
+
);
|
|
517
|
+
let residualSquares = 0;
|
|
518
|
+
let totalSquares = 0;
|
|
519
|
+
rows.forEach((row, index) => {
|
|
520
|
+
const difference = row[y] - model.meanY;
|
|
521
|
+
const centered = Number.isFinite(difference)
|
|
522
|
+
? difference / responseScale
|
|
523
|
+
: row[y] / responseScale - model.meanY / responseScale;
|
|
524
|
+
const residual = centered - normalizedSlope * coordinates[index];
|
|
525
|
+
totalSquares += centered ** 2;
|
|
526
|
+
residualSquares += residual ** 2;
|
|
527
|
+
});
|
|
528
|
+
return {
|
|
529
|
+
count: rows.length, intercept: model.intercept, slope: model.slope,
|
|
530
|
+
rSquared: totalSquares === 0 ? null : 1 - residualSquares / totalSquares
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
480
534
|
export function fitRegressionGroup(rows, { x, y, group, parameters }) {
|
|
535
|
+
if (parameters.method === "exp") return fitExponentialGroup(rows, { x, y, group });
|
|
481
536
|
if (parameters.method === "linear") {
|
|
482
537
|
return fitLinearGroup(rows, {
|
|
483
538
|
x,
|
|
@@ -509,9 +564,14 @@ export function predictRegressionAt(model, xValue, parameters) {
|
|
|
509
564
|
: undefined;
|
|
510
565
|
const prediction = parameters.method === "linear"
|
|
511
566
|
? linearPrediction(model, xValue)
|
|
567
|
+
: parameters.method === "exp"
|
|
568
|
+
? requireFiniteResult(Math.exp(model.logAmplitude + model.rate * xValue), "Exponential regression prediction")
|
|
512
569
|
: parameters.method === "polynomial"
|
|
513
570
|
? polynomial.prediction
|
|
514
571
|
: predictLoessAt(model, xValue);
|
|
572
|
+
if (parameters.method === "exp" && prediction === 0) {
|
|
573
|
+
throw new RangeError("Exponential regression prediction must be positive and representable.");
|
|
574
|
+
}
|
|
515
575
|
if (parameters.method === "loess" || parameters.interval === false) return { prediction };
|
|
516
576
|
const leverage = parameters.method === "linear"
|
|
517
577
|
? model.normalizedSxx !== undefined
|
|
@@ -51,9 +51,17 @@ export function normalizeRegressionParameters({
|
|
|
51
51
|
confidence,
|
|
52
52
|
interval
|
|
53
53
|
} = {}) {
|
|
54
|
-
if (!["linear", "polynomial", "loess"].includes(method)) {
|
|
54
|
+
if (!["linear", "polynomial", "loess", "exp"].includes(method)) {
|
|
55
55
|
throw new Error(`Unsupported regression method "${method}".`);
|
|
56
56
|
}
|
|
57
|
+
if (method === "exp") {
|
|
58
|
+
if (degree !== undefined || span !== undefined || robustIterations !== undefined ||
|
|
59
|
+
confidenceMethod !== undefined || level !== undefined || confidence !== undefined ||
|
|
60
|
+
(interval !== undefined && interval !== false)) {
|
|
61
|
+
throw new Error("Exponential regression only supports interval false and no confidence or method-specific options.");
|
|
62
|
+
}
|
|
63
|
+
return cloneAndFreeze({ method, interval: false });
|
|
64
|
+
}
|
|
57
65
|
if (method === "loess") {
|
|
58
66
|
if (degree !== undefined) {
|
|
59
67
|
throw new Error("Regression degree requires the polynomial method.");
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { rejectUnknownProperties } from "../../core/validation.js";
|
|
3
|
+
|
|
4
|
+
export const REGRESSION_STATISTIC_FIELDS = Object.freeze(["count", "intercept", "slope", "rSquared"]);
|
|
5
|
+
|
|
6
|
+
export function validateRegressionStatisticsTransform(transform) {
|
|
7
|
+
if (!isPlainObject(transform)) throw new TypeError("Regression statistics transform must be a plain object.");
|
|
8
|
+
rejectUnknownProperties(transform, ["type"], "regression statistics transform");
|
|
9
|
+
if (transform.type !== "regressionStatistics") throw new Error("Unsupported regression statistics transform.");
|
|
10
|
+
return transform;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function requireRegressionStatisticsSource(source) {
|
|
14
|
+
const transform = source?.transform?.[0];
|
|
15
|
+
if (source?.transform?.length !== 1 || transform.type !== "regression") {
|
|
16
|
+
throw new Error("Regression statistics source must be a regression dataset.");
|
|
17
|
+
}
|
|
18
|
+
if (transform.method !== "linear") throw new Error("Regression statistics only supports linear regression.");
|
|
19
|
+
if (REGRESSION_STATISTIC_FIELDS.includes(transform.groupBy)) {
|
|
20
|
+
throw new Error(`Regression statistics group field "${transform.groupBy}" collides with a statistic output.`);
|
|
21
|
+
}
|
|
22
|
+
return transform;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateRegressionFitStatistics(cache, dataset, source) {
|
|
26
|
+
if (!isPlainObject(cache)) throw new TypeError("Regression fit statistics must be a plain object.");
|
|
27
|
+
rejectUnknownProperties(cache, ["version", "method", "groups"], "regression fit statistics");
|
|
28
|
+
const transform = dataset?.transform?.[0];
|
|
29
|
+
if (dataset?.transform?.length !== 1 || transform.type !== "regression" || transform.method !== "linear" ||
|
|
30
|
+
!Array.isArray(dataset.values) || cache.version !== 1 || cache.method !== "linear" || !Array.isArray(cache.groups)) {
|
|
31
|
+
throw new Error("Regression fit statistics requires a materialized linear owner and version 1 groups.");
|
|
32
|
+
}
|
|
33
|
+
const expected = new Map();
|
|
34
|
+
for (const row of source?.values ?? []) {
|
|
35
|
+
if (transform.missing === "drop" && (row[transform.x] == null || row[transform.y] == null)) continue;
|
|
36
|
+
if (!Number.isFinite(row[transform.x]) || !Number.isFinite(row[transform.y])) {
|
|
37
|
+
throw new Error("Regression fit statistics source must contain eligible finite pairs.");
|
|
38
|
+
}
|
|
39
|
+
const key = transform.groupBy === undefined ? undefined : row[transform.groupBy];
|
|
40
|
+
expected.set(key, (expected.get(key) ?? 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
const groups = [...expected.entries()];
|
|
43
|
+
if (groups.length !== cache.groups.length) throw new Error("Regression fit statistics group count does not match its source.");
|
|
44
|
+
cache.groups.forEach((row, index) => {
|
|
45
|
+
if (!isPlainObject(row)) throw new TypeError("Regression fit statistic row must be a plain object.");
|
|
46
|
+
const grouped = transform.groupBy !== undefined;
|
|
47
|
+
rejectUnknownProperties(row, [...REGRESSION_STATISTIC_FIELDS, ...(grouped ? ["group"] : [])], "regression fit statistic row");
|
|
48
|
+
if (grouped && !Object.is(row.group, groups[index][0]) ||
|
|
49
|
+
!Number.isSafeInteger(row.count) || row.count < 2 || row.count !== groups[index][1] ||
|
|
50
|
+
!Number.isFinite(row.intercept) || !Number.isFinite(row.slope) ||
|
|
51
|
+
row.rSquared !== null && !Number.isFinite(row.rSquared)) {
|
|
52
|
+
throw new Error("Regression fit statistics contains invalid coefficients, group identity or observation count.");
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return cache;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function regressionStatisticsRows(cache, dataset) {
|
|
59
|
+
const transform = requireRegressionStatisticsSource(dataset);
|
|
60
|
+
return cache.groups.map(({ group, count, intercept, slope, rSquared }) => ({
|
|
61
|
+
...(transform.groupBy === undefined ? {} : { [transform.groupBy]: group }),
|
|
62
|
+
count, intercept, slope, rSquared
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mapTransformedValues as
|
|
1
|
+
import{mapTransformedValues as F}from"./transformed.js";import{cloneAndFreeze as p,isPlainObject as j}from"../../core/immutable.js";import{validateGeneratedItemLimit as $}from"../../core/validation.js";import{normalizePalette as k,resolveContinuousPalette as S}from"../palettes.js";import{inverseLerp as C}from"../numeric.js";const B=p(["rgb","hsl","hsl-long","lab","hcl","hcl-long","cubehelix","cubehelix-long"]),D=Object.freeze({black:"#000000",blue:"#0000ff",cyan:"#00ffff",gray:"#808080",green:"#008000",grey:"#808080",magenta:"#ff00ff",navy:"#000080",orange:"#ffa500",purple:"#800080",red:"#ff0000",teal:"#008080",white:"#ffffff",yellow:"#ffff00"});function E(t,e=0,n=1){return Math.max(e,Math.min(n,t))}function d(t){return(t%360+360)%360}function q({h:t,s:e,l:n}){const r=(1-Math.abs(2*n-1))*e,o=d(t)/60,i=r*(1-Math.abs(o%2-1)),[a,u,l]=o<1?[r,i,0]:o<2?[i,r,0]:o<3?[0,r,i]:o<4?[0,i,r]:o<5?[i,0,r]:[r,0,i],s=n-r/2;return{r:a+s,g:u+s,b:l+s}}function w(t){if(typeof t!="string"||t.length===0)throw new TypeError("Continuous color range requires non-empty color strings.");const e=D[t.toLowerCase()]??t.toLowerCase();let n=e.match(/^#([0-9a-f]{3})$/i);if(n)return Object.fromEntries(["r","g","b"].map((r,o)=>[r,Number.parseInt(n[1][o].repeat(2),16)/255]));if(n=e.match(/^#([0-9a-f]{6})$/i),n)return{r:Number.parseInt(n[1].slice(0,2),16)/255,g:Number.parseInt(n[1].slice(2,4),16)/255,b:Number.parseInt(n[1].slice(4,6),16)/255};if(n=e.match(/^rgb\(\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*\)$/),n){const o=e.includes("%")?100:255,i=n.slice(1).map(a=>Number(a)/o);if(i.every(a=>Number.isFinite(a)&&a>=0&&a<=1))return{r:i[0],g:i[1],b:i[2]}}if(n=e.match(/^hsl\(\s*([-\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*\)$/),n){const r=Number(n[1]),o=Number(n[2])/100,i=Number(n[3])/100;if([r,o,i].every(Number.isFinite)&&o>=0&&o<=1&&i>=0&&i<=1)return q({h:r,s:o,l:i})}throw new Error(`Unsupported continuous color "${t}".`)}function N({r:t,g:e,b:n}){return`#${[t,e,n].map(r=>Math.round(E(r)*255).toString(16).padStart(2,"0")).join("")}`}function T({r:t,g:e,b:n}){return[t,e,n].map(r=>Math.round(r*255))}function A({r:t,g:e,b:n}){const r=Math.max(t,e,n),o=Math.min(t,e,n),i=r-o,a=(r+o)/2;if(i===0)return{h:0,s:0,l:a};const u=i/(1-Math.abs(2*a-1)),l=r===t?(e-n)/i%6:r===e?(n-t)/i+2:(t-e)/i+4;return{h:d(l*60),s:u,l:a}}function x(t){return t<=.04045?t/12.92:((t+.055)/1.055)**2.4}function m(t){return t<=.0031308?12.92*t:1.055*t**(1/2.4)-.055}function y(t,e="d65"){const n=x(t.r),r=x(t.g),o=x(t.b),i=e==="d50"?(.4360747*n+.3850649*r+.1430804*o)/.96422:(.4124564*n+.3575761*r+.1804375*o)/.95047,a=e==="d50"?.2225045*n+.7168786*r+.0606169*o:.2126729*n+.7151522*r+.072175*o,u=e==="d50"?(.0139322*n+.0971045*r+.7141733*o)/.82521:(.0193339*n+.119192*r+.9503041*o)/1.08883,l=b=>b>216/24389?Math.cbrt(b):841/108*b+4/29,s=l(i),c=l(a),f=l(u);return{l:116*c-16,a:500*(s-c),b:200*(c-f)}}function O({l:t,a:e,b:n},r="d65"){const o=(t+16)/116,i=o+e/500,a=o-n/200,u=f=>f**3>216/24389?f**3:108/841*(f-4/29),l=(r==="d50"?.96422:.95047)*u(i),s=u(o),c=(r==="d50"?.82521:1.08883)*u(a);return r==="d50"?{r:m(3.1338561*l-1.6168667*s-.4906146*c),g:m(-.9787684*l+1.9161415*s+.033454*c),b:m(.0719453*l-.2289914*s+1.4052427*c)}:{r:m(3.2404542*l-1.5371385*s-.4985314*c),g:m(-.969266*l+1.8760108*s+.041556*c),b:m(.0556434*l-.2040259*s+1.0572252*c)}}function I({l:t,a:e,b:n}){return{h:d(Math.atan2(n,e)*180/Math.PI),c:Math.hypot(e,n),l:t}}function H({h:t,c:e,l:n}){const r=t*Math.PI/180;return{l:n,a:Math.cos(r)*e,b:Math.sin(r)*e}}function R({r:t,g:e,b:n}){const l=-1.7884503806,s=1.97294*1.78277,c=1.78277*-.29227- -.90649*-.14861,f=(c*n+l*t-s*e)/(c+l-s),b=n-f,h=(1.97294*(e-f)- -.29227*b)/-.90649,g=1.97294*f*(1-f);return{h:d(Math.atan2(h,b)*180/Math.PI-120),s:g===0?0:Math.hypot(h,b)/g,l:f}}function U({h:t,s:e,l:n}){const r=(t+120)*Math.PI/180,o=e*n*(1-n),i=Math.cos(r),a=Math.sin(r);return{r:n+o*(-.14861*i+1.78277*a),g:n+o*(-.29227*i-.90649*a),b:n+o*(1.97294*i)}}function M(t,e,n,r){let o=e-t;return r||(o=(o+540)%360-180),d(t+o*n)}function W(t,e,n,r,o){const i=o.whitePoint??"d65";if(r.startsWith("hsl")){const s=A(t),c=A(e);return q({h:M(s.h,c.h,n,r.endsWith("-long")),s:s.s+(c.s-s.s)*n,l:s.l+(c.l-s.l)*n})}if(r==="lab"){const s=y(t,i),c=y(e,i);return O({l:s.l+(c.l-s.l)*n,a:s.a+(c.a-s.a)*n,b:s.b+(c.b-s.b)*n},i)}if(r.startsWith("hcl")){const s=I(y(t,i)),c=I(y(e,i));return O(H({h:M(s.h,c.h,n,r.endsWith("-long")),c:s.c+(c.c-s.c)*n,l:s.l+(c.l-s.l)*n}),i)}const a=R(t),u=R(e),l=n**(o.gamma??1);if(!Number.isFinite(l))throw new RangeError("Continuous cubehelix gamma produced a nonfinite interpolation.");return U({h:M(a.h,u.h,n,r.endsWith("-long")),s:a.s+(u.s-a.s)*n,l:a.l+(u.l-a.l)*l})}function P(t){if(!B.includes(t))throw new Error(`Unsupported continuous color interpolation "${t}".`);return t}function _(t,e={}){const n={};if(e.whitePoint!==void 0){if(!["lab","hcl","hcl-long"].includes(t))throw new Error("Scale whitePoint requires Lab or HCL interpolation.");if(!["d65","d50"].includes(e.whitePoint))throw new Error("Scale whitePoint must be d65 or d50.");n.whitePoint=e.whitePoint}if(e.gamma!==void 0){if(!["cubehelix","cubehelix-long"].includes(t))throw new Error("Scale gamma requires cubehelix interpolation.");if(!Number.isFinite(e.gamma)||e.gamma<=0)throw new RangeError("Scale gamma must be a positive finite number.");n.gamma=e.gamma}return n}function G(t){if(t==="auto")return t;if(Array.isArray(t)){if($(t.length,"Sequential color range length"),t.length<2)throw new TypeError("Sequential color range requires at least two colors.");return t.forEach(w),p(t)}if(!j(t)||Object.keys(t).length!==1||!Object.hasOwn(t,"palette"))throw new TypeError("Sequential color range requires colors or a palette descriptor.");const e=k(t.palette);if(e.count!==void 0&&e.count<2)throw new RangeError("Sequential palette count must be an integer of at least 2.");return p({palette:e})}function Z(t){const e=G(t);return e==="auto"?S("viridis"):Array.isArray(e)?p(e.map(n=>N(w(n)))):S(e.palette)}function L(t,e,n,r,o={}){const a=(r?E(e):e)*(t.length-1),u=Math.max(0,Math.min(t.length-2,Math.floor(a))),l=u+1,s=a-u;if(n==="rgb"){const c=T(t[u]),f=T(t[l]);return`#${c.map((b,h)=>Math.round(E(b+(f[h]-b)*s,0,255)).toString(16).padStart(2,"0")).join("")}`}return N(W(t[u],t[l],s,n,o))}function v(t,e,n="rgb"){const r=P(n);if(!Array.isArray(t)||t.length<2)throw new TypeError("Color interpolation requires at least two stops.");const o=t.map(w);return L(o,e,r,!0)}function z(t,e,n="auto"){if(!(t===void 0||t==="auto")){if(e!=="sequential")throw new Error(`Scale type "${e}" does not support midpoint.`);if(!Number.isFinite(t))throw new TypeError("Scale midpoint must be finite or auto.");if(n!=="auto"&&(!Array.isArray(n)||n.length!==2||!n.every(Number.isFinite)||!(t>Math.min(...n)&&t<Math.max(...n))))throw new RangeError("Scale midpoint must lie strictly inside its resolved domain.");return t}}function V(t,e,n,r={}){const{interpolation:o="rgb",clamp:i=!1}=r;if(_(o,r),["log","pow","sqrt","symlog"].includes(r.type)){z(r.midpoint,r.type,e);const h=F(t,e,[0,1],{type:r.type,clamp:i,...r.type==="log"?{base:r.base}:{},...r.type==="pow"?{exponent:r.exponent}:{},...r.type==="symlog"?{constant:r.constant}:{},...Object.hasOwn(r,"unknown")?{unknown:NaN}:{}});return V(h,[0,1],n,{...r,type:"sequential"})}const a=Object.hasOwn(r,"unknown");if(!Array.isArray(e)||e.length!==2||!e.every(Number.isFinite))throw new TypeError("Sequential color domain must contain two finite numbers.");if(!Array.isArray(t)||!a&&!t.every(Number.isFinite))throw new TypeError("Sequential color values must be finite numbers.");if(typeof i!="boolean")throw new TypeError("Sequential color clamp must be a boolean.");const u=z(r.midpoint,"sequential",e),[l,s]=e,c=l===s,f=P(o);if(!Array.isArray(n)||n.length<2)throw new TypeError("Color interpolation requires at least two stops.");const b=n.map(w);return p(t.map(h=>{if(!Number.isFinite(h)){if(a)return r.unknown;throw new TypeError("Sequential color values must be finite numbers.")}const g=u===void 0?c?.5:C(h,l,s):(s>l?h<=u:h>=u)?C(h,l,u)/2:.5+C(h,u,s)/2;return L(b,g,f,i,r)}))}export{B as CONTINUOUS_COLOR_INTERPOLATIONS,v as interpolateColorStops,V as mapSequentialColors,_ as normalizeColorInterpolationOptions,Z as resolveSequentialColorStops,P as validateContinuousColorInterpolation,G as validateSequentialColorRange,z as validateSequentialMidpoint};
|
|
@@ -7,7 +7,7 @@ export function validateContinuousColorConsumer(layer, encoding, scale, {
|
|
|
7
7
|
channel = "color"
|
|
8
8
|
} = {}) {
|
|
9
9
|
const kind = layer.mark?.type;
|
|
10
|
-
if (["log", "sqrt", "symlog"].includes(scale.type) && encoding.fieldType !== "quantitative") {
|
|
10
|
+
if (["log", "pow", "sqrt", "symlog"].includes(scale.type) && encoding.fieldType !== "quantitative") {
|
|
11
11
|
throw new Error(`Scale type "${scale.type}" requires quantitative color.`);
|
|
12
12
|
}
|
|
13
13
|
if (channel === "stroke") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateMeasuredRadiusScale as
|
|
1
|
+
import{validateMeasuredRadiusScale as D}from"./radial.js";import{normalizeColorInterpolationOptions as F,validateContinuousColorInterpolation as N,validateSequentialMidpoint as T}from"./color.js";import{isTransformedScaleType as k}from"./mapping.js";import{normalizeTransformParameters as q,validateTransformedDomain as A}from"./transformed.js";import{isContinuousColorScaleType as R,validateScalePropertyForType as b}from"./types.js";const p=Object.freeze(["nice","zero","clamp","reverse"]),B=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 _(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,g=f(l,d,e,"paddingInner",o)??0,w=f(l,d,e,"paddingOuter",o)??0,c=f(l,d,e,"align",o)??.5;if(!Number.isFinite(g)||g<0||g>=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(c)||c<0||c>1)throw new RangeError("Scale align must be between 0 and 1.");Object.assign(i,{paddingInner:g,paddingOuter:w,align:c})}function $(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 J({type:i,color:l=!1,previous:n={},patch:e={},defaults:o={},retainCoreOnTypeChange:m=!1,retainCompatibleOnTypeChange:d=!1,allowColorReset:g=!1,allowOrdinalBandParameters:w=!1,validateDomain:c,validateRange:I}){const s=n.type!==void 0&&n.type!==i,M=Object.hasOwn(e,"domain")?e.domain:(!s||m)&&Object.hasOwn(n,"domain")?n.domain:o.domain??"auto",x=Object.hasOwn(e,"range")?e.range:(!s||m)&&Object.hasOwn(n,"range")?n.range:o.range??"auto",r={type:i,domain:c(i,M),range:I(i,x)},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 p){let t=f(n,e,o,a,s);if(t===void 0&&s&&d&&Object.hasOwn(n,a))try{b(i,a),t=n[a]}catch{t=void 0}if(t!==void 0){if(typeof t!="boolean")throw new TypeError(`Scale ${a} must be a boolean.`);b(i,a),r[a]=t}}const j=Object.fromEntries(B.flatMap(a=>{const t=f(n,e,o,a,s);return t===void 0?[]:[[a,t]]}));if(k(i)){const a=q(i,j);i==="log"&&(r.base=a.base),i==="pow"&&(r.exponent=a.exponent),i==="symlog"&&(r.constant=a.constant),r.domain!=="auto"&&A(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&&R(i))O===void 0&&d&&(O=n.interpolate),r.interpolate=N(O??"rgb");else if(O!==void 0)throw new Error(`Scale type "${i}" does not support interpolate.`);const z=Object.hasOwn(e,"interpolate")&&e.interpolate!==n.interpolate,S={};for(const a of["whitePoint","gamma"]){const t=Object.hasOwn(e,a)?e[a]:z?void 0:f(n,e,o,a,s&&!(d&&R(i)));t!==void 0&&!(g&&t==="auto")&&(S[a]=t)}Object.assign(r,F(r.interpolate,S));const E=T(f(n,e,o,"midpoint",s),i,r.domain);if(E!==void 0&&(r.midpoint=E),i==="band"||i==="ordinal"&&w)_(r,n,e,o,s);else if(i==="point")$(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{J as normalizeScaleDefinition};
|
|
@@ -16,7 +16,7 @@ export const SCALE_TYPES_BY_ROLE = Object.freeze({
|
|
|
16
16
|
[SCALE_ROLES.bandPosition]: Object.freeze(["band"]),
|
|
17
17
|
[SCALE_ROLES.pointPosition]: Object.freeze(["point"]),
|
|
18
18
|
[SCALE_ROLES.discreteAppearance]: Object.freeze(["ordinal"]),
|
|
19
|
-
[SCALE_ROLES.continuousColor]: Object.freeze(["sequential", "log", "sqrt", "symlog"]),
|
|
19
|
+
[SCALE_ROLES.continuousColor]: Object.freeze(["sequential", "log", "pow", "sqrt", "symlog"]),
|
|
20
20
|
[SCALE_ROLES.discretizedColor]: Object.freeze([
|
|
21
21
|
"quantize", "quantile", "threshold"
|
|
22
22
|
])
|
|
@@ -53,6 +53,9 @@ export function validateConcreteGraphicValue(type, property, value) {
|
|
|
53
53
|
if (type === "text" && property === "fontSize" && value === 0) {
|
|
54
54
|
throw new RangeError("text.fontSize must be positive.");
|
|
55
55
|
}
|
|
56
|
+
if (property === "blend" && !(value === "normal" || value === "difference" && type !== "circle")) {
|
|
57
|
+
throw new Error(`${type}.blend must be ${type === "circle" ? "normal" : "normal or difference"}.`);
|
|
58
|
+
}
|
|
56
59
|
if (property === "opacity" && (value < 0 || value > 1)) {
|
|
57
60
|
throw new RangeError(`${type}.opacity must be between 0 and 1.`);
|
|
58
61
|
}
|
|
@@ -52,6 +52,7 @@ const GRAPHIC_PROPERTIES = Object.freeze({
|
|
|
52
52
|
"lineJoin",
|
|
53
53
|
"miterLimit",
|
|
54
54
|
"opacity",
|
|
55
|
+
"blend",
|
|
55
56
|
"length"
|
|
56
57
|
]),
|
|
57
58
|
rect: new Set([
|
|
@@ -66,6 +67,7 @@ const GRAPHIC_PROPERTIES = Object.freeze({
|
|
|
66
67
|
"lineJoin",
|
|
67
68
|
"miterLimit",
|
|
68
69
|
"opacity",
|
|
70
|
+
"blend",
|
|
69
71
|
"length"
|
|
70
72
|
]),
|
|
71
73
|
line: new Set([
|
|
@@ -108,6 +110,7 @@ const GRAPHIC_PROPERTIES = Object.freeze({
|
|
|
108
110
|
"lineJoin",
|
|
109
111
|
"miterLimit",
|
|
110
112
|
"opacity",
|
|
113
|
+
"blend",
|
|
111
114
|
"length"
|
|
112
115
|
])
|
|
113
116
|
});
|