ggaction 0.0.8 → 0.0.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ggaction",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Build charts through immutable, traceable graphical actions.",
5
5
  "license": "MIT",
6
6
  "author": "Hyeon Jeon",
@@ -24,11 +24,19 @@
24
24
  "files": [
25
25
  "src/",
26
26
  "!src/**/AGENTS.md",
27
+ "knowledge/action-cards.json",
28
+ "knowledge/intent-taxonomy.json",
29
+ "knowledge/mcp-resources.json",
30
+ "knowledge/task-packet.schema.json",
31
+ "knowledge/task-resolver.js",
27
32
  "types/",
28
33
  "README.md",
29
34
  "CHANGELOG.md",
30
35
  "LICENSE"
31
36
  ],
37
+ "bin": {
38
+ "ggaction-mcp": "./src/mcp/cli.js"
39
+ },
32
40
  "types": "./types/index.d.ts",
33
41
  "exports": {
34
42
  ".": {
@@ -110,6 +118,7 @@
110
118
  "tag": "latest"
111
119
  },
112
120
  "dependencies": {
121
+ "@modelcontextprotocol/sdk": "1.30.0",
113
122
  "@napi-rs/canvas": "^1.0.2"
114
123
  },
115
124
  "devDependencies": {
@@ -1,15 +1,15 @@
1
1
  import { registerBasicCanvasActions } from "./canvas/index.js";
2
2
  import { registerBasicChartActions } from "./charts/basic.js";
3
- import { registerCoordinateActions } from "./coordinates/index.js";
3
+ import { registerCoordinateActions } from "./coordinates/actions.js";
4
4
  import { registerBasicDataActions } from "./data/basic.js";
5
5
  import { registerBasicEncodingActions } from "./encodings/basic.js";
6
6
  import { registerBasicGuideActions } from "./guides/basic.js";
7
7
  import { registerBasicMarkActions } from "./marks/basic.js";
8
- import { registerPrimitiveActions } from "./primitives/index.js";
8
+ import { registerBasicPrimitiveActions } from "./primitives/index.js";
9
9
  import { registerBasicScaleActions } from "./scales/index.js";
10
10
 
11
11
  export function registerBasicActions(ProgramClass) {
12
- registerPrimitiveActions(ProgramClass);
12
+ registerBasicPrimitiveActions(ProgramClass);
13
13
  registerBasicCanvasActions(ProgramClass);
14
14
  registerBasicDataActions(ProgramClass);
15
15
  registerBasicMarkActions(ProgramClass);
@@ -80,3 +80,7 @@ export const createCoordinate = action(
80
80
  return program;
81
81
  }
82
82
  );
83
+
84
+ export function registerCoordinateActions(ProgramClass) {
85
+ ProgramClass.prototype.createCoordinate = createCoordinate;
86
+ }
@@ -38,6 +38,10 @@ export function resolveLinePositionPolicy({
38
38
  }
39
39
  const regression = dataset.transform?.some(item => item.type === "regression");
40
40
  const interval = dataset.transform?.some(item => item.type === "interval");
41
+ const windowOutput = dataset.transform?.some(item =>
42
+ item.type === "window" &&
43
+ item.operations?.some(operation => operation.as === args.field)
44
+ );
41
45
 
42
46
  if (channel === "x") {
43
47
  const directPair =
@@ -73,7 +77,7 @@ export function resolveLinePositionPolicy({
73
77
  (fieldType === "temporal" ||
74
78
  (fieldType === "quantitative" &&
75
79
  (layer.encoding?.x === undefined || quantitativePair)));
76
- if (interval || prospectiveDirect) {
80
+ if (interval || (windowOutput && args.aggregate === undefined) || prospectiveDirect) {
77
81
  if (!["quantitative", "temporal"].includes(fieldType)) {
78
82
  throw new Error(
79
83
  "Direct line y encoding requires a quantitative or temporal field."
@@ -1,3 +1,6 @@
1
+ import { action } from "../../../../core/action.js";
2
+ import { resolveStoredSelection } from
3
+ "../../../../materialization/selection/state.js";
1
4
  import {
2
5
  createCategoricalLegend,
3
6
  createLegend,
@@ -12,6 +15,7 @@ import {
12
15
  rematerializeLegendLabels,
13
16
  rematerializeLegendTitle
14
17
  } from "./components.js";
18
+ import { activeConfig, noOptions, symbolGraphic } from "./layout.js";
15
19
  import {
16
20
  createLegendSymbolLines,
17
21
  createLegendSymbolPoints,
@@ -21,7 +25,7 @@ import {
21
25
  rematerializeLegendSymbolPoints,
22
26
  rematerializeLegendSymbolSwatches,
23
27
  rematerializeLegendSymbols,
24
- rematerializeLegendHighlights
28
+ rematerializeBasicLegendHighlights
25
29
  } from "./symbols.js";
26
30
  import { editLegend } from "../edit.js";
27
31
  import {
@@ -29,6 +33,82 @@ import {
29
33
  rematerializeSideLegendLane
30
34
  } from "../lane.js";
31
35
 
36
+ function exactLegendSelection(program, config, highlight) {
37
+ if (highlight.target !== config.target) return undefined;
38
+ const resolved = resolveStoredSelection(program, highlight.selection);
39
+ const selected = new Set(resolved.keys);
40
+ const groups = config.domain.map(value =>
41
+ resolved.items.filter(item => item.fields?.[config.field] === value)
42
+ );
43
+ if (
44
+ groups.some(group => group.length === 0) ||
45
+ resolved.items.some(item => !config.domain.includes(item.fields?.[config.field]))
46
+ ) return undefined;
47
+ const states = groups.map(group => {
48
+ const count = group.filter(item => selected.has(item.key)).length;
49
+ return count === 0 ? false : count === group.length ? true : undefined;
50
+ });
51
+ return states.includes(undefined) ? undefined : states;
52
+ }
53
+
54
+ function legendLayerStyle(layer, style) {
55
+ const properties = layer.type === "line"
56
+ ? ["stroke", "strokeWidth", "strokeDash", "opacity"]
57
+ : ["fill", "stroke", "strokeWidth", "opacity"];
58
+ return Object.fromEntries(
59
+ properties.flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
60
+ );
61
+ }
62
+
63
+ function createLegendHighlightAction() {
64
+ return action(
65
+ {
66
+ op: "rematerializeLegendHighlights",
67
+ description: "Reflect exact categorical mark highlights in legend symbols."
68
+ },
69
+ function (args = {}) {
70
+ noOptions(args, "rematerializeLegendHighlights");
71
+ const hasCategorical =
72
+ this.guideConfigs.legend?.series !== undefined ||
73
+ this.guideConfigs.legend?.color !== undefined;
74
+ if (!hasCategorical) return this;
75
+ const { config } = activeConfig(this);
76
+ const highlights = Object.values(
77
+ this.materializationConfigs.highlights ?? {}
78
+ ).map(highlight => ({
79
+ highlight,
80
+ states: exactLegendSelection(this, config, highlight)
81
+ })).filter(entry => entry.states !== undefined);
82
+ if (highlights.length === 0) return this;
83
+
84
+ let next = this.rematerializeLegendSymbols();
85
+ for (const { highlight, states } of highlights) {
86
+ for (const layer of config.symbol.layers) {
87
+ const id = symbolGraphic(config, layer.type);
88
+ const graphic = next.graphicSpec.objects[id];
89
+ const selectedStyle = legendLayerStyle(layer, highlight.style);
90
+ const dimOpacity = highlight.dimOthers === false
91
+ ? undefined
92
+ : highlight.dimOthers.opacity;
93
+ next = next.editGraphics({
94
+ target: id,
95
+ property: "items",
96
+ value: graphic.items.map((child, index) => ({
97
+ type: child.type ?? graphic.type,
98
+ properties: states[index]
99
+ ? { ...child.properties, ...selectedStyle }
100
+ : dimOpacity === undefined
101
+ ? child.properties
102
+ : { ...child.properties, opacity: dimOpacity }
103
+ }))
104
+ });
105
+ }
106
+ }
107
+ return next;
108
+ }
109
+ );
110
+ }
111
+
32
112
  export function registerBasicCategoricalLegendActions(ProgramClass) {
33
113
  ProgramClass.prototype.createLegend = createLegend;
34
114
  ProgramClass.prototype.createCategoricalLegend = createCategoricalLegend;
@@ -37,7 +117,8 @@ export function registerBasicCategoricalLegendActions(ProgramClass) {
37
117
  ProgramClass.prototype.rematerializeLegendBackground = rematerializeLegendBackground;
38
118
  ProgramClass.prototype.createLegendSymbols = createLegendSymbols;
39
119
  ProgramClass.prototype.rematerializeLegendSymbols = rematerializeLegendSymbols;
40
- ProgramClass.prototype.rematerializeLegendHighlights = rematerializeLegendHighlights;
120
+ ProgramClass.prototype.rematerializeLegendHighlights =
121
+ rematerializeBasicLegendHighlights;
41
122
  ProgramClass.prototype.createLegendSymbolLines = createLegendSymbolLines;
42
123
  ProgramClass.prototype.rematerializeLegendSymbolLines = rematerializeLegendSymbolLines;
43
124
  ProgramClass.prototype.createLegendSymbolPoints = createLegendSymbolPoints;
@@ -57,5 +138,7 @@ export function registerBasicCategoricalLegendActions(ProgramClass) {
57
138
 
58
139
  export function registerCategoricalLegendActions(ProgramClass) {
59
140
  registerBasicCategoricalLegendActions(ProgramClass);
141
+ ProgramClass.prototype.rematerializeLegendHighlights =
142
+ createLegendHighlightAction();
60
143
  ProgramClass.prototype.editLegend = editLegend;
61
144
  }
@@ -12,44 +12,6 @@ import {
12
12
  import { resolveLegendGraphicPlacement } from
13
13
  "../../../../materialization/graphicHierarchy.js";
14
14
  import { createPointShapeGraphic } from "../../../../grammar/pointShapes.js";
15
- import { resolveStoredSelection } from "../../../../materialization/selection/state.js";
16
-
17
- function exactLegendSelection(program, config, highlight) {
18
- if (highlight.target !== config.target) return undefined;
19
- const resolved = resolveStoredSelection(program, highlight.selection);
20
- const selected = new Set(resolved.keys);
21
- const groups = config.domain.map(value =>
22
- resolved.items.filter(item => item.fields?.[config.field] === value)
23
- );
24
- if (
25
- groups.some(group => group.length === 0) ||
26
- resolved.items.some(item => !config.domain.includes(item.fields?.[config.field]))
27
- ) return undefined;
28
- const states = groups.map(group => {
29
- const count = group.filter(item => selected.has(item.key)).length;
30
- return count === 0 ? false : count === group.length ? true : undefined;
31
- });
32
- return states.includes(undefined) ? undefined : states;
33
- }
34
-
35
- function legendLayerStyle(layer, style) {
36
- if (layer.type === "line") {
37
- return Object.fromEntries(
38
- ["stroke", "strokeWidth", "strokeDash", "opacity"]
39
- .flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
40
- );
41
- }
42
- if (layer.type === "swatch") {
43
- return Object.fromEntries(
44
- ["fill", "stroke", "strokeWidth", "opacity"]
45
- .flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
46
- );
47
- }
48
- return Object.fromEntries(
49
- ["fill", "stroke", "strokeWidth", "opacity"]
50
- .flatMap(key => Object.hasOwn(style, key) ? [[key, style[key]]] : [])
51
- );
52
- }
53
15
 
54
16
  function makeEditSymbol(type) {
55
17
  const suffix = { line: "Lines", point: "Points", swatch: "Swatches" }[type];
@@ -232,49 +194,13 @@ export const rematerializeLegendSymbols = action(
232
194
  }
233
195
  );
234
196
 
235
- export const rematerializeLegendHighlights = action(
197
+ export const rematerializeBasicLegendHighlights = action(
236
198
  {
237
199
  op: "rematerializeLegendHighlights",
238
200
  description: "Reflect exact categorical mark highlights in legend symbols."
239
201
  },
240
202
  function (args = {}) {
241
203
  noOptions(args, "rematerializeLegendHighlights");
242
- const hasCategorical =
243
- this.guideConfigs.legend?.series !== undefined ||
244
- this.guideConfigs.legend?.color !== undefined;
245
- if (!hasCategorical) return this;
246
- const { config } = activeConfig(this);
247
- const highlights = Object.values(
248
- this.materializationConfigs.highlights ?? {}
249
- ).map(highlight => ({
250
- highlight,
251
- states: exactLegendSelection(this, config, highlight)
252
- })).filter(entry => entry.states !== undefined);
253
- if (highlights.length === 0) return this;
254
-
255
- let next = this.rematerializeLegendSymbols();
256
- for (const { highlight, states } of highlights) {
257
- for (const layer of config.symbol.layers) {
258
- const id = symbolGraphic(config, layer.type);
259
- const graphic = next.graphicSpec.objects[id];
260
- const selectedStyle = legendLayerStyle(layer, highlight.style);
261
- const dimOpacity = highlight.dimOthers === false
262
- ? undefined
263
- : highlight.dimOthers.opacity;
264
- next = next.editGraphics({
265
- target: id,
266
- property: "items",
267
- value: graphic.items.map((child, index) => ({
268
- type: child.type ?? graphic.type,
269
- properties: states[index]
270
- ? { ...child.properties, ...selectedStyle }
271
- : dimOpacity === undefined
272
- ? child.properties
273
- : { ...child.properties, opacity: dimOpacity }
274
- }))
275
- });
276
- }
277
- }
278
- return next;
204
+ return this;
279
205
  }
280
206
  );
@@ -1,7 +1,7 @@
1
1
  import { registerCanvasActions } from "./canvas/index.js";
2
2
  import { registerChartActions } from "./charts/index.js";
3
3
  import { registerCategoryOrderActions } from "./categoryOrder/index.js";
4
- import { registerCoordinateActions } from "./coordinates/index.js";
4
+ import { registerCoordinateActions } from "./coordinates/actions.js";
5
5
  import { registerCompositionActions } from "./composition/index.js";
6
6
  import { registerDataActions } from "./data/index.js";
7
7
  import { registerEncodingActions } from "./encodings/index.js";
@@ -1,6 +1,33 @@
1
+ import { isPlainObject } from "../../core/immutable.js";
2
+ import { validateBin2DTransform } from "../../grammar/bin2d.js";
1
3
  import { registerCreateGraphicsAction } from "./createGraphics.js";
2
4
  import { registerEditGraphicsAction } from "./editGraphics.js";
3
5
  import { registerSemanticPrimitiveAction } from "./semantic.js";
6
+ import { createSemanticPrimitiveAction } from "./semanticAction.js";
7
+ import { createSemanticValueValidator } from "./semanticValidation/index.js";
8
+
9
+ function validateBasicDatasetTransforms(value) {
10
+ if (!Array.isArray(value) || value.length !== 1 || !isPlainObject(value[0])) {
11
+ throw new TypeError(
12
+ "Dataset transform must contain exactly one plain object."
13
+ );
14
+ }
15
+ if (value[0].type !== "bin2d") {
16
+ throw new Error(`Unsupported dataset transform "${value[0].type}".`);
17
+ }
18
+ validateBin2DTransform(value[0]);
19
+ }
20
+
21
+ export function registerBasicPrimitiveActions(ProgramClass) {
22
+ const validateBasicSemanticValue = createSemanticValueValidator({
23
+ validateDatasetTransforms: validateBasicDatasetTransforms,
24
+ sourceMarkTypes: ["point", "bar", "rect"]
25
+ });
26
+ ProgramClass.prototype.editSemantic =
27
+ createSemanticPrimitiveAction(validateBasicSemanticValue);
28
+ registerCreateGraphicsAction(ProgramClass);
29
+ registerEditGraphicsAction(ProgramClass);
30
+ }
4
31
 
5
32
  export function registerPrimitiveActions(ProgramClass) {
6
33
  registerSemanticPrimitiveAction(ProgramClass);
@@ -1,191 +1,28 @@
1
- import { action } from "../../core/action.js";
1
+ import { createSemanticPrimitiveAction } from "./semanticAction.js";
2
+ import { validateDatasetTransforms } from "../../grammar/transforms.js";
2
3
  import {
3
- cloneAndFreeze,
4
- freezeOwned,
5
- isPlainObject,
6
- removeOwnedPath
7
- } from "../../core/immutable.js";
8
- import { parseSemanticPath } from "../../grammar/schemas/semanticPath.js";
9
- import { validateSemanticValue } from "./semanticValue.js";
10
-
11
- const CONTEXT_KEYS = Object.freeze({
12
- dataset: "currentData",
13
- layer: "currentMark",
14
- scale: "currentScale",
15
- coordinate: "currentCoordinate",
16
- guide: "currentGuide"
17
- });
18
-
19
- function setNestedProperty(source, path, value) {
20
- const [key, ...rest] = path;
21
-
22
- if (rest.length === 0) {
23
- return freezeOwned({ ...source, [key]: cloneAndFreeze(value) });
24
- }
25
-
26
- const child = isPlainObject(source[key]) ? source[key] : {};
27
- return freezeOwned({
28
- ...source,
29
- [key]: setNestedProperty(child, rest, value)
30
- });
31
- }
32
-
33
- function updateEntity(spec, parsed, value) {
34
- const collection = spec[parsed.collection];
35
- const index = collection.findIndex(item => item.id === parsed.id);
36
-
37
- if (
38
- parsed.kind === "dataset" &&
39
- index !== -1 &&
40
- Object.hasOwn(collection[index], "values")
41
- ) {
42
- throw new Error(`Dataset "${parsed.id}" is immutable after creation.`);
43
- }
44
-
45
- const current = index === -1 ? { id: parsed.id } : collection[index];
46
- const updated = setNestedProperty(current, parsed.path, value);
47
- const nextCollection = [...collection];
48
-
49
- if (index === -1) {
50
- nextCollection.push(updated);
51
- } else {
52
- nextCollection[index] = updated;
53
- }
54
-
55
- return freezeOwned({
56
- ...spec,
57
- [parsed.collection]: freezeOwned(nextCollection)
58
- });
59
- }
60
-
61
- function updateGuides(spec, parsed, value) {
62
- return freezeOwned({
63
- ...spec,
64
- guides: setNestedProperty(spec.guides, parsed.path, value)
65
- });
66
- }
67
-
68
- function updateTitle(spec, parsed, value) {
69
- return freezeOwned({
70
- ...spec,
71
- title: setNestedProperty(spec.title, parsed.path, value)
72
- });
73
- }
74
-
75
- function removeEntity(spec, parsed) {
76
- const collection = spec[parsed.collection];
77
- const index = collection.findIndex(item => item.id === parsed.id);
78
- if (index === -1) return spec;
79
- if (parsed.kind === "layer" && parsed.path.length === 0) {
80
- const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
81
- return freezeOwned({
82
- ...spec,
83
- [parsed.collection]: freezeOwned(nextCollection)
84
- });
85
- }
86
- if (parsed.kind === "dataset" && parsed.path.length === 0) {
87
- const dataset = collection[index];
88
- if (dataset.source === undefined) {
89
- throw new Error(`Source dataset "${parsed.id}" is immutable after creation.`);
90
- }
91
- const referenced = spec.layers.some(layer => layer.data === parsed.id) ||
92
- spec.datasets.some(candidate => candidate.source === parsed.id);
93
- if (referenced) {
94
- throw new Error(`Derived dataset "${parsed.id}" is still referenced.`);
95
- }
96
- const nextCollection = collection.filter((_, itemIndex) => itemIndex !== index);
97
- return freezeOwned({
98
- ...spec,
99
- [parsed.collection]: freezeOwned(nextCollection)
100
- });
4
+ validateParallelDimensions,
5
+ validateParallelKeyField,
6
+ validateParallelMissingPolicy
7
+ } from "../../grammar/parallelCoordinates.js";
8
+ import { createSemanticValueValidator } from "./semanticValidation/index.js";
9
+
10
+ function validateParallel(property, value) {
11
+ if (property === "encoding.parallel.dimensions") {
12
+ validateParallelDimensions(value, { normalized: true });
13
+ } else if (property === "encoding.parallel.key") {
14
+ validateParallelKeyField(value);
15
+ } else if (property === "encoding.parallel.missing") {
16
+ validateParallelMissingPolicy(value);
101
17
  }
102
- if (parsed.kind === "dataset") {
103
- throw new Error(`Dataset "${parsed.id}" is immutable after creation.`);
104
- }
105
- const removed = removeOwnedPath(collection[index], parsed.path);
106
- if (!removed.removed) return spec;
107
- const nextCollection = [...collection];
108
- nextCollection[index] = removed.value;
109
- return freezeOwned({
110
- ...spec,
111
- [parsed.collection]: freezeOwned(nextCollection)
112
- });
113
18
  }
114
19
 
115
- function removeRootProperty(spec, root, path) {
116
- const removed = removeOwnedPath(spec[root], path);
117
- return removed.removed
118
- ? freezeOwned({ ...spec, [root]: removed.value })
119
- : spec;
120
- }
121
-
122
- const editSemantic = action(
123
- {
124
- op: "editSemantic",
125
- description: "Create, replace, or remove one semantic property.",
126
- scope: "any"
127
- },
128
- function ({ property, value, remove = false } = {}) {
129
- if (typeof remove !== "boolean") {
130
- throw new TypeError("editSemantic remove must be a boolean.");
131
- }
132
- if (remove && value !== undefined) {
133
- throw new Error("editSemantic cannot combine value and remove.");
134
- }
135
- if (!remove && value === undefined) {
136
- throw new TypeError("editSemantic requires a value.");
137
- }
138
-
139
- const parsed = parseSemanticPath(property, { allowContainer: remove });
140
- if (this.compositionSpec !== undefined && !(
141
- this.compositionSpec.type === "facet" && parsed.kind === "title"
142
- )) {
143
- throw new Error(
144
- "editSemantic on a composition parent currently supports only facet title state."
145
- );
146
- }
147
- if (remove) {
148
- const semanticSpec = parsed.kind === "guide"
149
- ? removeRootProperty(this.semanticSpec, "guides", parsed.path)
150
- : parsed.kind === "title"
151
- ? removeRootProperty(this.semanticSpec, "title", parsed.path)
152
- : removeEntity(this.semanticSpec, parsed);
153
- if (semanticSpec === this.semanticSpec) return this;
154
- const clearsCurrentData =
155
- parsed.kind === "dataset" &&
156
- parsed.path.length === 0 &&
157
- this.context.currentData === parsed.id;
158
- const clearsCurrentMark =
159
- parsed.kind === "layer" &&
160
- parsed.path.length === 0 &&
161
- this.context.currentMark === parsed.id;
162
- return this._clone({
163
- semanticSpec,
164
- ...(clearsCurrentData || clearsCurrentMark
165
- ? { context: freezeOwned({
166
- ...this.context,
167
- ...(clearsCurrentData ? { currentData: undefined } : {}),
168
- ...(clearsCurrentMark ? { currentMark: undefined } : {})
169
- }) }
170
- : {})
171
- });
172
- }
173
- validateSemanticValue(this, parsed, value);
174
-
175
- const semanticSpec = parsed.kind === "guide"
176
- ? updateGuides(this.semanticSpec, parsed, value)
177
- : parsed.kind === "title"
178
- ? updateTitle(this.semanticSpec, parsed, value)
179
- : updateEntity(this.semanticSpec, parsed, value);
180
- const contextKey = CONTEXT_KEYS[parsed.kind];
181
- const context = contextKey === undefined
182
- ? this.context
183
- : freezeOwned({ ...this.context, [contextKey]: parsed.id });
184
-
185
- return this._clone({ semanticSpec, context });
186
- }
187
- );
188
-
189
20
  export function registerSemanticPrimitiveAction(ProgramClass) {
190
- ProgramClass.prototype.editSemantic = editSemantic;
21
+ const validateSemanticValue = createSemanticValueValidator({
22
+ validateDatasetTransforms,
23
+ validateParallel,
24
+ sourceMarkTypes: ["point", "bar", "rule", "rect"]
25
+ });
26
+ ProgramClass.prototype.editSemantic =
27
+ createSemanticPrimitiveAction(validateSemanticValue);
191
28
  }