ggaction 0.0.3 → 0.0.4

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/README.md +95 -83
  3. package/package.json +11 -6
  4. package/src/actions/data/regression.js +1 -1
  5. package/src/actions/encodings/appearance.js +1 -1
  6. package/src/actions/encodings/color/continuous.js +137 -0
  7. package/src/actions/encodings/color/index.js +151 -0
  8. package/src/actions/encodings/color/layout.js +91 -0
  9. package/src/actions/encodings/color/policy.js +88 -0
  10. package/src/actions/encodings/index.js +2 -2
  11. package/src/actions/encodings/offset.js +1 -1
  12. package/src/actions/encodings/{position.js → position/index.js} +6 -6
  13. package/src/actions/encodings/position/resolve.js +1 -1
  14. package/src/actions/encodings/ranged.js +2 -2
  15. package/src/actions/encodings/strokeDash.js +1 -1
  16. package/src/actions/encodings/text.js +1 -1
  17. package/src/actions/errorBands/options.js +1 -1
  18. package/src/actions/errorBars/options.js +1 -1
  19. package/src/actions/facets/actions.js +1 -1
  20. package/src/actions/facets/derive.js +2 -2
  21. package/src/actions/facets/guides.js +1 -1
  22. package/src/actions/guides/axes/labels.js +1 -1
  23. package/src/actions/guides/axes/ticks.js +1 -1
  24. package/src/actions/guides/axes/titles.js +1 -1
  25. package/src/actions/guides/grids/resolve.js +1 -1
  26. package/src/actions/guides/legends/categorical/layout.js +4 -2
  27. package/src/actions/guides/legends/continuous/interval.js +1 -1
  28. package/src/actions/guides/legends/continuous/opacity.js +1 -1
  29. package/src/actions/guides/legends/edit.js +2 -20
  30. package/src/actions/guides/legends/remove.js +2 -20
  31. package/src/actions/guides/legends/size.js +1 -1
  32. package/src/actions/guides/legends/target.js +25 -0
  33. package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
  34. package/src/actions/guides/polar/index.js +1 -1
  35. package/src/actions/guides/polar/resolve.js +1 -1
  36. package/src/actions/guides/tickValues.js +1 -1
  37. package/src/actions/intervals/resolve.js +1 -1
  38. package/src/actions/marks/arc/actions.js +2 -2
  39. package/src/actions/marks/area/actions.js +2 -2
  40. package/src/actions/marks/bar/edit.js +1 -1
  41. package/src/actions/marks/line/actions.js +3 -3
  42. package/src/actions/marks/rect/actions.js +1 -1
  43. package/src/actions/marks/rule/actions.js +1 -1
  44. package/src/actions/marks/shared.js +1 -1
  45. package/src/actions/marks/text/actions.js +1 -1
  46. package/src/actions/primitives/semanticValue.js +1 -1
  47. package/src/actions/regression/components.js +1 -1
  48. package/src/actions/regression/create.js +1 -1
  49. package/src/actions/regression/edit.js +1 -1
  50. package/src/actions/scales/consumers/common.js +91 -0
  51. package/src/actions/scales/consumers/families.js +47 -0
  52. package/src/actions/scales/consumers/index.js +41 -0
  53. package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
  54. package/src/actions/scales/create.js +1 -1
  55. package/src/actions/scales/definitions.js +2 -2
  56. package/src/actions/scales/edit.js +3 -3
  57. package/src/actions/scales/materialize.js +2 -2
  58. package/src/core/ChartProgram.js +22 -236
  59. package/src/core/compositionState.js +169 -0
  60. package/src/core/materializationState.js +65 -0
  61. package/src/core/programState.js +28 -0
  62. package/src/grammar/arcs.js +1 -1
  63. package/src/grammar/areaSeries.js +1 -1
  64. package/src/grammar/bars/aggregate.js +1 -1
  65. package/src/grammar/density.js +1 -1
  66. package/src/grammar/{facets.js → facets/index.js} +5 -5
  67. package/src/grammar/interval.js +2 -96
  68. package/src/grammar/lineSeries.js +1 -1
  69. package/src/grammar/palettes.js +5 -7
  70. package/src/grammar/polarLineCommands.js +1 -1
  71. package/src/grammar/regression/derive.js +97 -0
  72. package/src/grammar/regression/index.js +22 -0
  73. package/src/grammar/regression/models.js +282 -0
  74. package/src/grammar/regression/parameters.js +106 -0
  75. package/src/grammar/rules.js +1 -1
  76. package/src/grammar/scales/color.js +4 -2
  77. package/src/grammar/statistics/studentT.js +98 -0
  78. package/src/grammar/transforms.js +1 -1
  79. package/src/materialization/bars/aggregate.js +1 -1
  80. package/src/materialization/bars/grouped.js +1 -1
  81. package/src/materialization/bars/histogram.js +1 -1
  82. package/src/materialization/bars/ranged.js +1 -1
  83. package/src/materialization/density.js +1 -1
  84. package/src/materialization/dependencies.js +6 -87
  85. package/src/materialization/encodings.js +2 -2
  86. package/src/materialization/facetGuides/index.js +2 -0
  87. package/src/materialization/facetGuides/legacyCategorical.js +125 -0
  88. package/src/materialization/facetGuides/placement.js +126 -0
  89. package/src/materialization/facetGuides/preparation.js +188 -0
  90. package/src/materialization/facets.js +1 -1
  91. package/src/materialization/marks/capabilities.js +141 -0
  92. package/src/materialization/marks/index.js +105 -0
  93. package/src/materialization/marks/policies.js +87 -0
  94. package/src/materialization/rect.js +1 -1
  95. package/src/materialization/rowEncoding.js +1 -1
  96. package/src/materialization/scaleGuideDependencies.js +94 -0
  97. package/src/materialization/scales/map.js +1 -1
  98. package/src/materialization/scales/policies/series.js +1 -1
  99. package/src/materialization/scales/resolve.js +1 -1
  100. package/src/materialization/selection/items/bar.js +1 -1
  101. package/src/materialization/selection/styles.js +1 -1
  102. package/src/renderers/canvas/text.js +14 -1
  103. package/types/extension.d.ts +4 -1
  104. package/src/actions/encodings/color.js +0 -424
  105. package/src/grammar/regression.js +0 -578
  106. package/src/grammar/scales.js +0 -1
  107. package/src/materialization/facetGuides.js +0 -426
  108. package/src/materialization/marks.js +0 -316
@@ -7,7 +7,7 @@ import {
7
7
  validateUnitInterval
8
8
  } from "../../core/validation.js";
9
9
  import { validatePointShape } from "../../grammar/pointShapes.js";
10
- import { normalizeStrokeDashPattern } from "../../grammar/scales.js";
10
+ import { normalizeStrokeDashPattern } from "../../grammar/scales/index.js";
11
11
  import { DEFAULT_COLORS } from "../../theme/defaults.js";
12
12
 
13
13
  const DEFAULT_POINT_SIZE = 2;
@@ -2,6 +2,18 @@ import { requireFiniteProperty, requireStringProperty } from "./validation.js";
2
2
  import { validateConcreteGraphicProperties } from
3
3
  "../../grammar/schemas/concreteGraphic.js";
4
4
 
5
+ const MIN_CANVAS_NUMERIC_FONT_WEIGHT = 100;
6
+ const MAX_CANVAS_NUMERIC_FONT_WEIGHT = 900;
7
+
8
+ export function normalizeCanvasFontWeight(fontWeight) {
9
+ if (typeof fontWeight !== "number") return fontWeight;
10
+ const rounded = Math.round(fontWeight / 100) * 100;
11
+ return Math.min(
12
+ MAX_CANVAS_NUMERIC_FONT_WEIGHT,
13
+ Math.max(MIN_CANVAS_NUMERIC_FONT_WEIGHT, rounded)
14
+ );
15
+ }
16
+
5
17
  function drawText(context, child, collectionId) {
6
18
  const properties = child.properties ?? {};
7
19
  const graphicId = child.id ?? collectionId;
@@ -73,7 +85,8 @@ function drawText(context, child, collectionId) {
73
85
  try {
74
86
  context.fillStyle = fill;
75
87
  context.globalAlpha = opacity;
76
- context.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
88
+ const canvasFontWeight = normalizeCanvasFontWeight(fontWeight);
89
+ context.font = `${canvasFontWeight} ${fontSize}px ${fontFamily}`;
77
90
  context.textAlign = textAlign;
78
91
  context.textBaseline = textBaseline;
79
92
  context.translate(x, y);
@@ -12,4 +12,7 @@ export interface ActionMetadata {
12
12
  export function action<TOptions extends ActionOptions = ActionOptions>(
13
13
  metadata: ActionMetadata,
14
14
  implementation: (this: ChartProgram, options: TOptions) => ChartProgram
15
- ): (this: ChartProgram, options?: TOptions) => ChartProgram;
15
+ ): <TProgram extends ChartProgram>(
16
+ this: TProgram,
17
+ options?: TOptions
18
+ ) => TProgram;
@@ -1,424 +0,0 @@
1
- import { action } from "../../core/action.js";
2
- import {
3
- readNominalField,
4
- readQuantitativeField,
5
- readScaleField,
6
- readTemporalField,
7
- validateCategoricalFieldType
8
- } from "../../grammar/scales.js";
9
- import {
10
- BAR_GRAINS,
11
- inferBarColorLayout,
12
- resolveBarChannels,
13
- resolveBarOffsetChannel,
14
- resolveBarGrain
15
- } from "../../grammar/bars/policy.js";
16
- import { validateColorLayout } from "../../grammar/seriesLayout.js";
17
- import {
18
- validateAggregate,
19
- validateAggregateFieldType,
20
- validateAggregateFieldValues
21
- } from "../../grammar/aggregate.js";
22
- import {
23
- resolveColorScaleDefinition,
24
- resolveOffsetScaleDefinition,
25
- resolveQuantitativeColorScaleDefinition
26
- } from "../scales/definitions.js";
27
- import {
28
- applyEncodingScale,
29
- resolveReassignmentScaleOptions,
30
- resolveTarget,
31
- validateLineSeriesCompatibility,
32
- validateOptions
33
- } from "./shared.js";
34
- import { applyMaterializationPlan } from "../../materialization/dependencies.js";
35
- import { planEncodingRematerialization } from "../../materialization/encodings.js";
36
-
37
- const COLOR_ENCODING_OPTIONS = Object.freeze([
38
- "field", "target", "fieldType", "scale", "palette", "layout", "aggregate"
39
- ]);
40
-
41
- function resolveColorScaleOptions(args) {
42
- const options = args.scale ?? {};
43
- if (args.palette === undefined) return options;
44
- if (options.palette !== undefined || options.range !== undefined) {
45
- throw new Error(
46
- "encodeColor palette cannot be combined with scale.palette or scale.range."
47
- );
48
- }
49
- return { ...options, palette: args.palette };
50
- }
51
-
52
- function assertNoConstantColor(program, layer) {
53
- const config = program.markConfigs[layer.id];
54
- const hasConstant = {
55
- point: config?.fill !== undefined,
56
- line: config?.stroke !== undefined,
57
- bar: config?.barAppearance?.fill !== undefined,
58
- arc: config?.fill !== undefined,
59
- rect: config?.fillExplicit === true
60
- }[layer.mark.type] ?? false;
61
- if (hasConstant) {
62
- throw new Error(
63
- `encodeColor cannot replace constant appearance on ${layer.mark.type} mark "${layer.id}".`
64
- );
65
- }
66
- }
67
-
68
- function isRangedArea(layer) {
69
- return layer.mark.type === "area" && (
70
- layer.encoding?.x2 !== undefined ||
71
- layer.encoding?.y2 !== undefined
72
- );
73
- }
74
-
75
- function resolveColorLayout(layer, requested, barGrain) {
76
- const existing = layer.encoding?.color === undefined
77
- ? undefined
78
- : layer.encoding.color.layout ?? (
79
- layer.mark.type === "bar"
80
- ? inferBarColorLayout(layer)
81
- : layer.mark.type === "area"
82
- ? "overlay"
83
- : undefined
84
- );
85
- if (requested !== undefined) validateColorLayout(requested);
86
- if (existing !== undefined && requested !== undefined && requested !== existing) {
87
- throw new Error(
88
- `Color layout transition from "${existing}" to "${requested}" is not supported.`
89
- );
90
- }
91
- const layout = requested ?? existing ?? (
92
- layer.mark.type === "bar"
93
- ? barGrain === BAR_GRAINS.histogram ? "stack" : barGrain === BAR_GRAINS.ranged ? "overlay" : "group"
94
- : layer.mark.type === "area"
95
- ? "overlay"
96
- : undefined
97
- );
98
-
99
- if (["point", "line", "rect"].includes(layer.mark.type) && layout !== undefined) {
100
- throw new Error(`Color layout is not supported for ${layer.mark.type} marks.`);
101
- }
102
- if (layer.mark.type === "area" && layout === "group") {
103
- throw new Error('Area color layout does not support "group".');
104
- }
105
- if (
106
- layer.mark.type === "arc" &&
107
- layout !== undefined &&
108
- layout !== "overlay"
109
- ) {
110
- throw new Error('Arc color layout currently supports only "overlay".');
111
- }
112
- if (
113
- isRangedArea(layer) &&
114
- layout !== "overlay"
115
- ) {
116
- throw new Error('Ranged area color encoding supports only "overlay" layout.');
117
- }
118
- return layout;
119
- }
120
-
121
- function applyColorLayoutCompanion(
122
- program,
123
- { target, layer, layout, scale, field }
124
- ) {
125
- if (layout === undefined) return program;
126
- if (layer.mark.type === "arc") return program;
127
- if (layer.mark.type === "bar" && resolveBarGrain(layer) === BAR_GRAINS.ranged) return program;
128
- if (isRangedArea(layer)) {
129
- return program;
130
- }
131
- const channels = layer.mark.type === "bar" ? resolveBarChannels(layer) : undefined;
132
- const measureChannel = channels?.measure ?? "y";
133
- const measure = layer.encoding?.[measureChannel];
134
- if (measure?.field === undefined || measure.scale === undefined) {
135
- throw new Error(`Color layout on mark "${target}" requires a measure encoding.`);
136
- }
137
- const stack = layout === "fill"
138
- ? "normalize"
139
- : layout === "overlay" || layout === "group"
140
- ? null
141
- : "zero";
142
- let next = program;
143
- if (layout === "group") {
144
- const offsetChannel = resolveBarOffsetChannel(layer);
145
- const method = offsetChannel === "xOffset" ? "encodeXOffset" : "encodeYOffset";
146
- next = next[method]({
147
- field,
148
- target,
149
- scale: {
150
- ...(layer.encoding?.[offsetChannel]?.scale === undefined
151
- ? {}
152
- : { id: layer.encoding[offsetChannel].scale }),
153
- domain: scale.domain
154
- }
155
- });
156
- }
157
- return next[measureChannel === "x" ? "encodeX" : "encodeY"]({
158
- target,
159
- field: measure.field,
160
- fieldType: measure.fieldType,
161
- ...(measure.aggregate === undefined ? {} : { aggregate: measure.aggregate }),
162
- stack
163
- });
164
- }
165
-
166
- function preSynchronizeGroupedOffset(
167
- program,
168
- { target, layer, layout, field, fieldType, scale, requestedScale }
169
- ) {
170
- if (
171
- layer.mark.type !== "bar" ||
172
- layout !== "group" ||
173
- layer.encoding?.color?.scale !== scale.id ||
174
- !Object.keys(requestedScale).some(key => key !== "id")
175
- ) {
176
- return program;
177
- }
178
- const offsetChannel = resolveBarOffsetChannel(layer);
179
- const offset = layer.encoding?.[offsetChannel];
180
- if (offset?.scale === undefined) return program;
181
- const offsetScale = resolveOffsetScaleDefinition(program, {
182
- id: offset.scale,
183
- domain: scale.domain
184
- }, offsetChannel);
185
- return program
186
- .editSemantic({
187
- property: `layer[${target}].encoding.${offsetChannel}.field`,
188
- value: field
189
- })
190
- .editSemantic({
191
- property: `layer[${target}].encoding.${offsetChannel}.fieldType`,
192
- value: fieldType
193
- })
194
- .editSemantic({
195
- property: `scale[${offsetScale.id}].domain`,
196
- value: offsetScale.domain
197
- });
198
- }
199
-
200
- function encodeContinuousColor(program, args) {
201
- if (!["quantitative", "temporal"].includes(args.fieldType)) {
202
- throw new Error(`Unsupported color field type "${args.fieldType}".`);
203
- }
204
- if (args.layout !== undefined) {
205
- throw new Error("Continuous color does not support layout.");
206
- }
207
- const { id: target, dataset, layer } = resolveTarget(
208
- program,
209
- args.target,
210
- ["point", "bar", "rect"],
211
- "continuous color mark"
212
- );
213
- assertNoConstantColor(program, layer);
214
- const requestedScale = resolveReassignmentScaleOptions(
215
- layer.encoding?.color,
216
- resolveColorScaleOptions(args)
217
- );
218
- const scale = resolveQuantitativeColorScaleDefinition(
219
- program,
220
- args.fieldType,
221
- requestedScale
222
- );
223
- if (Object.hasOwn(scale, "unknown") && layer.mark.type !== "point") {
224
- throw new Error(
225
- "Continuous color scale unknown currently requires a row-owned point mark."
226
- );
227
- }
228
- if (
229
- layer.mark.type === "bar" &&
230
- ["nominal", "ordinal"].includes(layer.encoding?.color?.fieldType)
231
- ) {
232
- throw new Error(
233
- "Continuous bar color cannot replace an existing nominal color layout."
234
- );
235
- }
236
- let aggregate;
237
- if (["point", "rect"].includes(layer.mark.type)) {
238
- if (args.aggregate !== undefined) {
239
- throw new Error(`${layer.mark.type} continuous color does not support aggregate.`);
240
- }
241
- } else {
242
- if (args.fieldType !== "quantitative") {
243
- throw new Error("Aggregate bar color currently requires a quantitative field.");
244
- }
245
- if (resolveBarGrain(layer) !== BAR_GRAINS.aggregate) {
246
- throw new Error(
247
- "Continuous bar color requires a complete categorical aggregate bar."
248
- );
249
- }
250
- const channels = resolveBarChannels(layer);
251
- const measure = layer.encoding?.[channels.measure];
252
- aggregate = args.aggregate ?? (
253
- measure?.field === args.field ? measure.aggregate : undefined
254
- );
255
- if (aggregate === undefined) {
256
- throw new Error(
257
- "Continuous bar color requires aggregate when its field differs from the measure field."
258
- );
259
- }
260
- aggregate = validateAggregate(aggregate);
261
- validateAggregateFieldType(aggregate, args.fieldType);
262
- validateAggregateFieldValues(dataset.values, args.field, args.fieldType);
263
- }
264
- if (layer.mark.type === "rect") {
265
- readScaleField(dataset.values, args.field, args.fieldType, {
266
- allowUnknown: true
267
- });
268
- } else if (Object.hasOwn(scale, "unknown")) {
269
- readScaleField(dataset.values, args.field, args.fieldType, {
270
- allowUnknown: true
271
- });
272
- } else if (args.fieldType === "temporal") {
273
- readTemporalField(dataset.values, args.field);
274
- } else {
275
- readQuantitativeField(dataset.values, args.field);
276
- }
277
- const next = program
278
- .editSemantic({
279
- property: `layer[${target}].encoding.color.field`,
280
- value: args.field
281
- })
282
- .editSemantic({
283
- property: `layer[${target}].encoding.color.fieldType`,
284
- value: args.fieldType
285
- })
286
- .editSemantic({
287
- property: `layer[${target}].encoding.color.scale`,
288
- value: scale.id
289
- });
290
- const encoded = aggregate === undefined
291
- ? next
292
- : next.editSemantic({
293
- property: `layer[${target}].encoding.color.aggregate`,
294
- value: aggregate
295
- });
296
- const scaled = encoded.setQuantitativeColorScale(scale);
297
- return applyMaterializationPlan(
298
- scaled,
299
- planEncodingRematerialization(scaled, {
300
- target,
301
- channel: "color",
302
- scale: scale.id
303
- })
304
- );
305
- }
306
-
307
- const encodeColor = action(
308
- {
309
- op: "encodeColor",
310
- description: "Encode a field as graphical color."
311
- },
312
- function (args = {}) {
313
- validateOptions(args, COLOR_ENCODING_OPTIONS, "encodeColor");
314
- const requestedFieldType = args.fieldType ?? "nominal";
315
- if (!["nominal", "ordinal"].includes(requestedFieldType)) {
316
- return encodeContinuousColor(this, {
317
- ...args,
318
- fieldType: requestedFieldType
319
- });
320
- }
321
- if (args.aggregate !== undefined) {
322
- throw new Error("Categorical color does not support aggregate.");
323
- }
324
- const fieldType = validateCategoricalFieldType(requestedFieldType);
325
- const { id: target, dataset, layer } = resolveTarget(
326
- this,
327
- args.target,
328
- ["point", "line", "bar", "area", "arc", "rect"],
329
- "color mark"
330
- );
331
- assertNoConstantColor(this, layer);
332
- if (
333
- layer.mark.type === "area" &&
334
- (layer.encoding?.group?.field === undefined ||
335
- layer.encoding.group.field !== args.field)
336
- ) {
337
- throw new Error(
338
- "Area color encoding must match an existing group encoding."
339
- );
340
- }
341
- validateLineSeriesCompatibility(layer, "color", args.field);
342
-
343
- const barGrain = resolveBarGrain(layer);
344
- if (layer.mark.type === "bar" && barGrain === undefined) {
345
- throw new Error(
346
- "Bar color encoding requires a complete histogram encoding or a complete ordinal aggregate encoding."
347
- );
348
- }
349
- const layout = resolveColorLayout(layer, args.layout, barGrain);
350
- const requestedScale = resolveReassignmentScaleOptions(
351
- layer.encoding?.color,
352
- resolveColorScaleOptions(args)
353
- );
354
- const scale = resolveColorScaleDefinition(this, requestedScale);
355
- if (Object.hasOwn(scale, "unknown") && layer.mark.type !== "point") {
356
- throw new Error(
357
- "Categorical color scale unknown currently requires a row-owned point mark."
358
- );
359
- }
360
- if (layer.mark.type === "rect") {
361
- readScaleField(dataset.values, args.field, fieldType, {
362
- allowUnknown: true
363
- });
364
- } else if (Object.hasOwn(scale, "unknown")) {
365
- readScaleField(dataset.values, args.field, fieldType, {
366
- allowUnknown: true
367
- });
368
- } else {
369
- readNominalField(dataset.values, args.field);
370
- }
371
-
372
- let next = this
373
- .editSemantic({
374
- property: `layer[${target}].encoding.color.field`,
375
- value: args.field
376
- })
377
- .editSemantic({
378
- property: `layer[${target}].encoding.color.fieldType`,
379
- value: fieldType
380
- })
381
- .editSemantic({
382
- property: `layer[${target}].encoding.color.scale`,
383
- value: scale.id
384
- });
385
- if (layout !== undefined) {
386
- next = next.editSemantic({
387
- property: `layer[${target}].encoding.color.layout`,
388
- value: layout
389
- });
390
- }
391
- next = preSynchronizeGroupedOffset(next, {
392
- target,
393
- layer,
394
- layout,
395
- field: args.field,
396
- fieldType,
397
- scale,
398
- requestedScale
399
- });
400
- next = applyEncodingScale(next, scale, requestedScale, {
401
- reassignment: layer.encoding?.color?.scale === scale.id
402
- });
403
- next = applyColorLayoutCompanion(next, {
404
- target,
405
- layer,
406
- layout,
407
- scale,
408
- field: args.field
409
- });
410
-
411
- return applyMaterializationPlan(
412
- next,
413
- planEncodingRematerialization(next, {
414
- target,
415
- channel: "color",
416
- scale: scale.id
417
- })
418
- );
419
- }
420
- );
421
-
422
- export function registerColorEncodingAction(ProgramClass) {
423
- ProgramClass.prototype.encodeColor = encodeColor;
424
- }