ggaction 0.0.4 → 0.0.5

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 (157) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +35 -13
  3. package/package.json +18 -7
  4. package/src/actions/boxPlots/create.js +13 -7
  5. package/src/actions/boxPlots/edit.js +31 -35
  6. package/src/actions/boxPlots/materialize.js +3 -0
  7. package/src/actions/boxPlots/options.js +8 -1
  8. package/src/actions/boxPlots/resolve.js +10 -2
  9. package/src/actions/charts/bar.js +50 -0
  10. package/src/actions/charts/heatmap.js +205 -0
  11. package/src/actions/charts/histogram.js +58 -0
  12. package/src/actions/charts/index.js +15 -0
  13. package/src/actions/charts/line.js +69 -0
  14. package/src/actions/charts/parallel.js +76 -0
  15. package/src/actions/charts/scatter.js +56 -0
  16. package/src/actions/charts/shared.js +121 -0
  17. package/src/actions/coordinates/parallel.js +50 -0
  18. package/src/actions/data/bin2d.js +137 -0
  19. package/src/actions/data/density.js +54 -13
  20. package/src/actions/data/derived.js +22 -0
  21. package/src/actions/data/filter.js +5 -0
  22. package/src/actions/data/gradientProfile.js +65 -0
  23. package/src/actions/data/horizon.js +71 -0
  24. package/src/actions/data/index.js +29 -2
  25. package/src/actions/data/window.js +50 -0
  26. package/src/actions/encodings/color/index.js +17 -1
  27. package/src/actions/encodings/color/layout.js +8 -0
  28. package/src/actions/encodings/density/resolve.js +109 -0
  29. package/src/actions/encodings/density.js +193 -88
  30. package/src/actions/encodings/horizon/resolve.js +283 -0
  31. package/src/actions/encodings/horizon.js +364 -0
  32. package/src/actions/encodings/index.js +6 -0
  33. package/src/actions/encodings/parallel.js +115 -0
  34. package/src/actions/encodings/pathOrder.js +131 -0
  35. package/src/actions/encodings/position/apply.js +73 -4
  36. package/src/actions/encodings/position/index.js +15 -2
  37. package/src/actions/encodings/position/policies/arc.js +27 -4
  38. package/src/actions/encodings/position/policies/area.js +33 -2
  39. package/src/actions/encodings/position/policies/index.js +7 -2
  40. package/src/actions/encodings/position/policies/line.js +18 -2
  41. package/src/actions/encodings/position/resolve.js +24 -2
  42. package/src/actions/encodings/ranged.js +3 -1
  43. package/src/actions/encodings/ruleAppearance.js +88 -10
  44. package/src/actions/facets/actions.js +1 -2
  45. package/src/actions/facets/derive.js +33 -3
  46. package/src/actions/facets/replay.js +7 -36
  47. package/src/actions/gradientPlots/components.js +177 -0
  48. package/src/actions/gradientPlots/create.js +96 -0
  49. package/src/actions/gradientPlots/edit.js +153 -0
  50. package/src/actions/gradientPlots/index.js +24 -0
  51. package/src/actions/gradientPlots/materialize.js +287 -0
  52. package/src/actions/gradientPlots/options.js +120 -0
  53. package/src/actions/gradientPlots/paint.js +107 -0
  54. package/src/actions/gradientPlots/rebind.js +51 -0
  55. package/src/actions/gradientPlots/resolve.js +81 -0
  56. package/src/actions/guides/applicability.js +44 -17
  57. package/src/actions/guides/axes/axes.js +37 -3
  58. package/src/actions/guides/axes/index.js +2 -0
  59. package/src/actions/guides/axes/labels.js +11 -6
  60. package/src/actions/guides/axes/parallel/resolve.js +86 -0
  61. package/src/actions/guides/axes/parallel.js +228 -0
  62. package/src/actions/guides/axes/titles.js +11 -8
  63. package/src/actions/guides/guides.js +60 -5
  64. package/src/actions/guides/legends/categorical/actions.js +48 -11
  65. package/src/actions/guides/legends/edit.js +5 -0
  66. package/src/actions/guides/legends/index.js +2 -0
  67. package/src/actions/guides/legends/remove.js +4 -11
  68. package/src/actions/guides/legends/strokeWidth.js +170 -0
  69. package/src/actions/index.js +6 -0
  70. package/src/actions/marks/area/actions.js +60 -136
  71. package/src/actions/marks/area/materialize.js +163 -0
  72. package/src/actions/marks/index.js +5 -1
  73. package/src/actions/marks/line/actions.js +99 -80
  74. package/src/actions/marks/line/materialize.js +146 -0
  75. package/src/actions/marks/point/create.js +76 -0
  76. package/src/actions/marks/point/edit.js +85 -0
  77. package/src/actions/marks/point/index.js +11 -1
  78. package/src/actions/marks/point/jitter.js +72 -0
  79. package/src/actions/marks/point/{actions.js → materialize.js} +175 -170
  80. package/src/actions/marks/rect/actions.js +3 -0
  81. package/src/actions/marks/remove.js +13 -2
  82. package/src/actions/marks/rule/actions.js +25 -3
  83. package/src/actions/marks/text/actions.js +19 -4
  84. package/src/actions/marks/text/index.js +7 -1
  85. package/src/actions/marks/text/layout.js +321 -0
  86. package/src/actions/primitives/semanticValidation/dataset.js +22 -0
  87. package/src/actions/primitives/semanticValidation/guide.js +43 -0
  88. package/src/actions/primitives/semanticValidation/index.js +23 -0
  89. package/src/actions/primitives/semanticValidation/layer.js +74 -0
  90. package/src/actions/primitives/semanticValidation/scale.js +74 -0
  91. package/src/actions/primitives/semanticValidation/shared.js +5 -0
  92. package/src/actions/primitives/semanticValue.js +1 -204
  93. package/src/actions/regression/edit.js +19 -21
  94. package/src/actions/scales/consumers/common.js +18 -0
  95. package/src/actions/scales/consumers/families.js +14 -3
  96. package/src/actions/scales/definitions.js +30 -0
  97. package/src/actions/scales/edit.js +6 -204
  98. package/src/actions/scales/editPolicy.js +214 -0
  99. package/src/actions/scales/materialize.js +1 -1
  100. package/src/actions/scales/patch.js +26 -0
  101. package/src/actions/selection/actions.js +28 -3
  102. package/src/actions/violinPlots/create.js +198 -0
  103. package/src/actions/violinPlots/index.js +5 -0
  104. package/src/core/textMetrics.js +47 -0
  105. package/src/core/vocabulary.js +7 -2
  106. package/src/grammar/arcs.js +49 -12
  107. package/src/grammar/areaSeries.js +41 -5
  108. package/src/grammar/bin2d.js +333 -0
  109. package/src/grammar/categoricalDensity.js +192 -0
  110. package/src/grammar/coordinates.js +1 -1
  111. package/src/grammar/curveCommands.js +3 -1
  112. package/src/grammar/density.js +252 -26
  113. package/src/grammar/facets/dependencies.js +3 -11
  114. package/src/grammar/facets/index.js +3 -1
  115. package/src/grammar/gradientProfile.js +213 -0
  116. package/src/grammar/horizon.js +455 -0
  117. package/src/grammar/jitter.js +197 -0
  118. package/src/grammar/lineSeries.js +69 -0
  119. package/src/grammar/paint.js +88 -0
  120. package/src/grammar/parallelCoordinates.js +213 -0
  121. package/src/grammar/pathOrder.js +35 -0
  122. package/src/grammar/pointShapes.js +24 -0
  123. package/src/grammar/rules.js +1 -0
  124. package/src/grammar/scales/appearance.js +21 -0
  125. package/src/grammar/schemas/concreteGraphic.js +11 -1
  126. package/src/grammar/schemas/graphicBounds.js +19 -37
  127. package/src/grammar/schemas/semanticPath.js +17 -2
  128. package/src/grammar/transforms.js +100 -11
  129. package/src/grammar/window.js +339 -0
  130. package/src/layout/labels.js +270 -0
  131. package/src/layout/text.js +4 -1
  132. package/src/layout/title.js +10 -20
  133. package/src/materialization/dataProvenance.js +56 -2
  134. package/src/materialization/dependencies.js +9 -14
  135. package/src/materialization/guides/resources.js +74 -24
  136. package/src/materialization/horizon.js +29 -0
  137. package/src/materialization/layout.js +15 -0
  138. package/src/materialization/marks/capabilities.js +21 -1
  139. package/src/materialization/marks/index.js +3 -0
  140. package/src/materialization/marks/pathOrder.js +71 -0
  141. package/src/materialization/marks/policies.js +6 -2
  142. package/src/materialization/scaleGuideDependencies.js +8 -0
  143. package/src/materialization/scales/resolve.js +2 -0
  144. package/src/materialization/selection/items/path.js +27 -1
  145. package/src/materialization/selection/items/point.js +1 -5
  146. package/src/materialization/selection/items/rect.js +21 -0
  147. package/src/materialization/selection/styles.js +20 -9
  148. package/src/materialization/text.js +12 -4
  149. package/src/renderers/canvas/fill.js +33 -0
  150. package/src/renderers/canvas/index.js +7 -0
  151. package/src/renderers/canvas/path.js +7 -4
  152. package/src/renderers/canvas/rect.js +9 -3
  153. package/src/selectors/datasets.js +4 -0
  154. package/src/theme/defaults.js +4 -0
  155. package/types/extension.d.ts +6 -0
  156. package/types/index.d.ts +76 -0
  157. package/types/program.d.ts +632 -10
@@ -7,9 +7,21 @@ import {
7
7
  POLAR_POSITION_CHANNELS
8
8
  } from "../../core/vocabulary.js";
9
9
  import { findSemanticScale } from "../../selectors/scales.js";
10
+ import { findDataset } from "../../selectors/datasets.js";
11
+ import { findUpstreamTransform } from
12
+ "../../materialization/dataProvenance.js";
13
+
14
+ function isHorizonLayer(program, layer) {
15
+ return findUpstreamTransform(
16
+ program,
17
+ findDataset(program, layer.data),
18
+ "horizon"
19
+ ) !== undefined;
20
+ }
10
21
 
11
22
  function hasEncoding(program, channel, { scaled = false, grid = false } = {}) {
12
23
  return program.semanticSpec.layers.some(layer => {
24
+ if (isHorizonLayer(program, layer) && channel !== "x") return false;
13
25
  const encoding = layer.encoding?.[channel];
14
26
  if (!scaled && !grid) return encoding !== undefined;
15
27
  if (encoding?.scale === undefined) return false;
@@ -33,22 +45,24 @@ function positionalApplicability(program, channels) {
33
45
 
34
46
  export function hasInferableLegend(program) {
35
47
  return program.semanticSpec.layers.some(layer =>
36
- (layer.mark?.type === "point" &&
37
- layer.encoding?.opacity?.scale !== undefined) ||
38
- (layer.mark?.type === "point" &&
39
- layer.encoding?.size?.scale !== undefined) ||
40
- (layer.mark?.type === "point" &&
41
- layer.encoding?.color?.scale !== undefined &&
42
- (layer.encoding?.shape?.scale !== undefined ||
43
- ["sequential", "quantize", "quantile", "threshold"].includes(
44
- findSemanticScale(program, layer.encoding.color.scale)?.type
45
- ))) ||
46
- (layer.mark?.type === "line" &&
47
- ["color", "strokeDash"].some(
48
- channel => layer.encoding?.[channel]?.scale !== undefined
49
- )) ||
50
- (["bar", "area", "arc", "rect"].includes(layer.mark?.type) &&
51
- layer.encoding?.color?.scale !== undefined)
48
+ !isHorizonLayer(program, layer) && (
49
+ (layer.mark?.type === "point" &&
50
+ layer.encoding?.opacity?.scale !== undefined) ||
51
+ (layer.mark?.type === "point" &&
52
+ layer.encoding?.size?.scale !== undefined) ||
53
+ (layer.mark?.type === "point" &&
54
+ layer.encoding?.color?.scale !== undefined &&
55
+ (layer.encoding?.shape?.scale !== undefined ||
56
+ ["sequential", "quantize", "quantile", "threshold"].includes(
57
+ findSemanticScale(program, layer.encoding.color.scale)?.type
58
+ ))) ||
59
+ (layer.mark?.type === "line" &&
60
+ ["color", "strokeDash"].some(
61
+ channel => layer.encoding?.[channel]?.scale !== undefined
62
+ )) ||
63
+ (["bar", "area", "arc", "rect"].includes(layer.mark?.type) &&
64
+ layer.encoding?.color?.scale !== undefined)
65
+ )
52
66
  );
53
67
  }
54
68
 
@@ -58,12 +72,22 @@ export function resolveGuideApplicability(program) {
58
72
  CARTESIAN_POSITION_CHANNELS
59
73
  );
60
74
  const polar = positionalApplicability(program, POLAR_POSITION_CHANNELS);
75
+ const parallel = program.semanticSpec.layers.some(layer =>
76
+ layer.encoding?.parallel?.dimensions?.length >= 2
77
+ );
61
78
  return Object.freeze({
62
79
  axes: Object.freeze({
63
80
  cartesian: CARTESIAN_POSITION_CHANNELS.some(
64
81
  channel => cartesian[channel].axis
65
82
  ),
66
- polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].axis)
83
+ polar: POLAR_POSITION_CHANNELS.some(channel => polar[channel].axis),
84
+ parallel,
85
+ directions: Object.freeze({
86
+ x: cartesian.x.axis,
87
+ y: cartesian.y.axis,
88
+ theta: polar.theta.axis,
89
+ radius: polar.radius.axis
90
+ })
67
91
  }),
68
92
  grid: Object.freeze({
69
93
  cartesian: CARTESIAN_POSITION_CHANNELS.some(
@@ -90,6 +114,9 @@ export function resolveAutomaticGridOptions(program) {
90
114
  ...(directions.radial ? { radial: {} } : {})
91
115
  });
92
116
  }
117
+ if (directions.vertical && !directions.horizontal) {
118
+ return Object.freeze({ horizontal: false, vertical: {} });
119
+ }
93
120
 
94
121
  const barOrientations = program.semanticSpec.layers
95
122
  .map(resolveBarOrientation)
@@ -9,7 +9,9 @@ const TOP_OPTIONS = Object.freeze([
9
9
  "coordinate", "x", "y", "theta", "radius"
10
10
  ]);
11
11
  const COORDINATE_OPTIONS = Object.freeze(["id", "type"]);
12
- const COORDINATE_API_TYPES = new Set(["auto", "cartesian", "polar"]);
12
+ const COORDINATE_API_TYPES = new Set([
13
+ "auto", "cartesian", "polar", "parallel"
14
+ ]);
13
15
 
14
16
  function validateAxisOption(value, channel) {
15
17
  if (value !== undefined && value !== false && !isPlainObject(value)) {
@@ -47,6 +49,9 @@ function inspectChannels(layers) {
47
49
  layer =>
48
50
  layer.encoding?.theta !== undefined || layer.encoding?.radius !== undefined
49
51
  );
52
+ const parallelLayers = layers.filter(
53
+ layer => layer.encoding?.parallel !== undefined
54
+ );
50
55
  const hasMixedLayer = layers.some(layer => {
51
56
  const hasCartesian =
52
57
  layer.encoding?.x !== undefined || layer.encoding?.y !== undefined;
@@ -61,7 +66,7 @@ function inspectChannels(layers) {
61
66
  );
62
67
  }
63
68
 
64
- return { cartesianLayers, polarLayers };
69
+ return { cartesianLayers, polarLayers, parallelLayers };
65
70
  }
66
71
 
67
72
  function resolveCoordinate(program, descriptor, cartesianLayers) {
@@ -185,7 +190,7 @@ const createAxes = action(
185
190
  },
186
191
  function (args = {}) {
187
192
  const coordinateDescriptor = validateArgs(args);
188
- const { cartesianLayers, polarLayers } = inspectChannels(
193
+ const { cartesianLayers, polarLayers, parallelLayers } = inspectChannels(
189
194
  this.semanticSpec.layers
190
195
  );
191
196
  const requestedCoordinateType = coordinateDescriptor.id === undefined
@@ -194,6 +199,35 @@ const createAxes = action(
194
199
  const explicitlyPolar = coordinateDescriptor.type === "polar" ||
195
200
  requestedCoordinateType === "polar" ||
196
201
  args.theta !== undefined || args.radius !== undefined;
202
+ const explicitlyParallel = coordinateDescriptor.type === "parallel" ||
203
+ requestedCoordinateType === "parallel";
204
+ if (parallelLayers.length > 0 && (
205
+ (cartesianLayers.length === 0 && polarLayers.length === 0) ||
206
+ explicitlyParallel
207
+ )) {
208
+ if (
209
+ args.x !== undefined || args.y !== undefined ||
210
+ args.theta !== undefined || args.radius !== undefined
211
+ ) {
212
+ throw new Error(
213
+ "createAxes channel options are not supported for Parallel axes."
214
+ );
215
+ }
216
+ const candidates = coordinateDescriptor.id === undefined
217
+ ? parallelLayers
218
+ : parallelLayers.filter(layer =>
219
+ layer.coordinate === coordinateDescriptor.id
220
+ );
221
+ if (candidates.length !== 1) {
222
+ throw new Error(
223
+ "createAxes requires coordinate.id when one Parallel layer cannot be inferred."
224
+ );
225
+ }
226
+ return this.createParallelAxes({
227
+ target: candidates[0].id,
228
+ coordinate: candidates[0].coordinate
229
+ });
230
+ }
197
231
  if (polarLayers.length > 0 &&
198
232
  (cartesianLayers.length === 0 || explicitlyPolar)) {
199
233
  if (coordinateDescriptor.type === "cartesian") {
@@ -12,6 +12,7 @@ import {
12
12
  removeXAxis,
13
13
  removeYAxis
14
14
  } from "./remove.js";
15
+ import { registerParallelAxisActions } from "./parallel.js";
15
16
 
16
17
  export function registerGuideAxisActions(ProgramClass) {
17
18
  registerAxisLineActions(ProgramClass);
@@ -22,6 +23,7 @@ export function registerGuideAxisActions(ProgramClass) {
22
23
  registerAxisActions(ProgramClass);
23
24
  registerAxisCollectionActions(ProgramClass);
24
25
  registerCompleteAxisEditActions(ProgramClass);
26
+ registerParallelAxisActions(ProgramClass);
25
27
  ProgramClass.prototype.removeXAxis = removeXAxis;
26
28
  ProgramClass.prototype.removeYAxis = removeYAxis;
27
29
  ProgramClass.prototype.removeThetaAxis = removeThetaAxis;
@@ -27,6 +27,7 @@ import {
27
27
  } from "./policy.js";
28
28
  import { findCanvasGraphic, resolvePlotGraphicPlacement } from
29
29
  "../../../materialization/graphicHierarchy.js";
30
+ import { resolveTextBounds } from "../../../core/textMetrics.js";
30
31
 
31
32
  const OPTIONS = Object.freeze([
32
33
  "scale", "position", "count", "values", "offset", "format", "color",
@@ -122,14 +123,18 @@ function resolve(program, channel, config) {
122
123
  })
123
124
  };
124
125
  const canvas = findCanvasGraphic(program)?.properties;
125
- const maximumTextWidth = Math.max(
126
- 0,
127
- ...text.map(value => [...value].length * config.fontSize * 0.6)
128
- );
126
+ const labelBounds = text.map((value, index) => resolveTextBounds({
127
+ x: Array.isArray(resolved.x) ? resolved.x[index] : resolved.x,
128
+ y: Array.isArray(resolved.y) ? resolved.y[index] : resolved.y,
129
+ text: value,
130
+ fontSize: config.fontSize,
131
+ textAlign: resolved.textAlign,
132
+ textBaseline: resolved.textBaseline
133
+ }));
129
134
  const fits = config.position === "top"
130
- ? resolved.y - config.fontSize >= 0
135
+ ? labelBounds.every(item => item.top >= 0)
131
136
  : config.position === "right"
132
- ? resolved.x + maximumTextWidth <= canvas?.width
137
+ ? labelBounds.every(item => item.right <= canvas?.width)
133
138
  : true;
134
139
  if (!canvas || !fits) {
135
140
  throw new Error(`The ${channel}-axis labels do not fit the Canvas margin.`);
@@ -0,0 +1,86 @@
1
+ import { validateUserId } from "../../../../core/identifiers.js";
2
+ import {
3
+ isTransformedScaleType,
4
+ mapContinuousScaleValues,
5
+ mapOrdinalPositionValues,
6
+ transformedTicks
7
+ } from "../../../../grammar/scales/index.js";
8
+ import { niceTicks } from "../../../../grammar/ticks.js";
9
+ import { resolveGraphicBounds } from "../../../../layout/canvas.js";
10
+ import { findCoordinate } from "../../../../selectors/coordinates.js";
11
+ import { findLayer } from "../../../../selectors/layers.js";
12
+
13
+ export function requireParallelAxisLayer(program, target) {
14
+ const layer = findLayer(program, target);
15
+ const coordinate = layer === undefined
16
+ ? undefined
17
+ : findCoordinate(program, layer.coordinate);
18
+ if (
19
+ layer?.mark?.type !== "line" ||
20
+ coordinate?.type !== "parallel" ||
21
+ layer.encoding?.parallel?.dimensions?.length < 2
22
+ ) {
23
+ throw new Error(`Parallel axes require an encoded Parallel line "${target}".`);
24
+ }
25
+ return { coordinate, dimensions: layer.encoding.parallel.dimensions, layer };
26
+ }
27
+
28
+ export function resolveParallelAxisTarget(program, requested) {
29
+ if (requested !== undefined) {
30
+ const target = validateUserId(requested, "Parallel axes target");
31
+ requireParallelAxisLayer(program, target);
32
+ return target;
33
+ }
34
+ const candidates = program.semanticSpec.layers.filter(layer => {
35
+ const coordinate = findCoordinate(program, layer.coordinate);
36
+ return layer.encoding?.parallel !== undefined && coordinate?.type === "parallel";
37
+ });
38
+ if (candidates.length !== 1) {
39
+ throw new Error(
40
+ "Parallel axes require target when one Parallel layer cannot be inferred."
41
+ );
42
+ }
43
+ return candidates[0].id;
44
+ }
45
+
46
+ function ticksForScale(scale) {
47
+ if (["ordinal", "band", "point"].includes(scale.type)) return scale.domain;
48
+ if (isTransformedScaleType(scale.type)) {
49
+ return transformedTicks(scale.type, scale.domain, 5, {
50
+ ...(scale.base === undefined ? {} : { base: scale.base }),
51
+ ...(scale.exponent === undefined ? {} : { exponent: scale.exponent }),
52
+ ...(scale.constant === undefined ? {} : { constant: scale.constant })
53
+ });
54
+ }
55
+ return niceTicks(scale.domain, 5);
56
+ }
57
+
58
+ function formatValue(value) {
59
+ if (Number.isFinite(value) && Math.abs(value) >= 1000 && value % 1000 === 0) {
60
+ return `${value / 1000}k`;
61
+ }
62
+ return String(value);
63
+ }
64
+
65
+ export function resolveParallelAxisValues(program, dimensions) {
66
+ const bounds = resolveGraphicBounds(program);
67
+ const step = bounds.width / (dimensions.length - 1);
68
+ const axes = dimensions.map((dimension, index) => {
69
+ const scale = program.resolvedScales[dimension.scale];
70
+ if (scale === undefined) {
71
+ throw new Error(`Parallel axis requires resolved scale "${dimension.scale}".`);
72
+ }
73
+ const values = ticksForScale(scale);
74
+ const y = ["ordinal", "band", "point"].includes(scale.type)
75
+ ? mapOrdinalPositionValues(values, scale)
76
+ : mapContinuousScaleValues(values, scale);
77
+ return {
78
+ ...dimension,
79
+ x: bounds.x + step * index,
80
+ values,
81
+ y,
82
+ labels: values.map(formatValue)
83
+ };
84
+ });
85
+ return { axes, bounds };
86
+ }
@@ -0,0 +1,228 @@
1
+ import { action } from "../../../core/action.js";
2
+ import { validateOptionObject } from "../../../core/validation.js";
3
+ import { resolvePlotGraphicPlacement } from
4
+ "../../../materialization/graphicHierarchy.js";
5
+ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
6
+ "../../../theme/defaults.js";
7
+ import {
8
+ requireParallelAxisLayer,
9
+ resolveParallelAxisTarget,
10
+ resolveParallelAxisValues
11
+ } from "./parallel/resolve.js";
12
+
13
+ const CREATE_OPTIONS = Object.freeze(["target", "coordinate"]);
14
+ const REMATERIALIZE_OPTIONS = Object.freeze(["target"]);
15
+
16
+ export const rematerializeParallelAxes = action(
17
+ {
18
+ op: "rematerializeParallelAxes",
19
+ description: "Recompute concrete Parallel dimension axes."
20
+ },
21
+ function (args = {}) {
22
+ validateOptionObject(
23
+ args,
24
+ REMATERIALIZE_OPTIONS,
25
+ "rematerializeParallelAxes"
26
+ );
27
+ const target = resolveParallelAxisTarget(
28
+ this,
29
+ args.target ?? this.guideConfigs.axis?.parallel?.axes?.target
30
+ );
31
+ const { dimensions } = requireParallelAxisLayer(this, target);
32
+ const { axes, bounds } = resolveParallelAxisValues(this, dimensions);
33
+ const ticks = axes.flatMap(axis => axis.values.map((value, index) => ({
34
+ x: axis.x,
35
+ y: axis.y[index],
36
+ value,
37
+ text: axis.labels[index]
38
+ })));
39
+ let next = this
40
+ .editGraphics({
41
+ target: "parallelAxisLines",
42
+ property: "length",
43
+ value: axes.length
44
+ })
45
+ .editGraphics({
46
+ target: "parallelAxisLines",
47
+ property: "x1",
48
+ value: axes.map(axis => axis.x)
49
+ })
50
+ .editGraphics({ target: "parallelAxisLines", property: "y1", value: bounds.y })
51
+ .editGraphics({
52
+ target: "parallelAxisLines",
53
+ property: "x2",
54
+ value: axes.map(axis => axis.x)
55
+ })
56
+ .editGraphics({
57
+ target: "parallelAxisLines",
58
+ property: "y2",
59
+ value: bounds.y + bounds.height
60
+ })
61
+ .editGraphics({
62
+ target: "parallelAxisLines",
63
+ property: "stroke",
64
+ value: DEFAULT_COLORS.axis
65
+ })
66
+ .editGraphics({
67
+ target: "parallelAxisLines",
68
+ property: "strokeWidth",
69
+ value: 1.25
70
+ })
71
+ .editGraphics({
72
+ target: "parallelAxisTicks",
73
+ property: "length",
74
+ value: ticks.length
75
+ })
76
+ .editGraphics({
77
+ target: "parallelAxisTicks",
78
+ property: "x1",
79
+ value: ticks.map(tick => tick.x - 4)
80
+ })
81
+ .editGraphics({
82
+ target: "parallelAxisTicks",
83
+ property: "y1",
84
+ value: ticks.map(tick => tick.y)
85
+ })
86
+ .editGraphics({
87
+ target: "parallelAxisTicks",
88
+ property: "x2",
89
+ value: ticks.map(tick => tick.x + 4)
90
+ })
91
+ .editGraphics({
92
+ target: "parallelAxisTicks",
93
+ property: "y2",
94
+ value: ticks.map(tick => tick.y)
95
+ })
96
+ .editGraphics({
97
+ target: "parallelAxisTicks",
98
+ property: "stroke",
99
+ value: DEFAULT_COLORS.mutedText
100
+ })
101
+ .editGraphics({
102
+ target: "parallelAxisTicks",
103
+ property: "strokeWidth",
104
+ value: 1
105
+ })
106
+ .editGraphics({
107
+ target: "parallelAxisLabels",
108
+ property: "length",
109
+ value: ticks.length
110
+ })
111
+ .editGraphics({
112
+ target: "parallelAxisLabels",
113
+ property: "x",
114
+ value: ticks.map(tick => tick.x - 9)
115
+ })
116
+ .editGraphics({
117
+ target: "parallelAxisLabels",
118
+ property: "y",
119
+ value: ticks.map(tick => tick.y)
120
+ })
121
+ .editGraphics({
122
+ target: "parallelAxisLabels",
123
+ property: "text",
124
+ value: ticks.map(tick => tick.text)
125
+ })
126
+ .editGraphics({ target: "parallelAxisLabels", property: "fill", value: DEFAULT_COLORS.axis })
127
+ .editGraphics({ target: "parallelAxisLabels", property: "fontSize", value: 11 })
128
+ .editGraphics({ target: "parallelAxisLabels", property: "fontFamily", value: DEFAULT_FONT_FAMILY })
129
+ .editGraphics({ target: "parallelAxisLabels", property: "fontWeight", value: "normal" })
130
+ .editGraphics({ target: "parallelAxisLabels", property: "textAlign", value: "right" })
131
+ .editGraphics({ target: "parallelAxisLabels", property: "textBaseline", value: "middle" })
132
+ .editGraphics({
133
+ target: "parallelAxisTitles",
134
+ property: "length",
135
+ value: axes.length
136
+ })
137
+ .editGraphics({
138
+ target: "parallelAxisTitles",
139
+ property: "x",
140
+ value: axes.map(axis => axis.x)
141
+ })
142
+ .editGraphics({
143
+ target: "parallelAxisTitles",
144
+ property: "y",
145
+ value: bounds.y - 20
146
+ })
147
+ .editGraphics({
148
+ target: "parallelAxisTitles",
149
+ property: "text",
150
+ value: axes.map(axis => axis.title)
151
+ })
152
+ .editGraphics({ target: "parallelAxisTitles", property: "fill", value: DEFAULT_COLORS.axisTitle })
153
+ .editGraphics({ target: "parallelAxisTitles", property: "fontSize", value: 13 })
154
+ .editGraphics({ target: "parallelAxisTitles", property: "fontFamily", value: DEFAULT_FONT_FAMILY })
155
+ .editGraphics({ target: "parallelAxisTitles", property: "fontWeight", value: 600 })
156
+ .editGraphics({ target: "parallelAxisTitles", property: "textAlign", value: "center" })
157
+ .editGraphics({ target: "parallelAxisTitles", property: "textBaseline", value: "middle" });
158
+ return next._withGuideConfig("parallel", "axes", {
159
+ target,
160
+ scales: dimensions.map(dimension => dimension.scale)
161
+ });
162
+ }
163
+ );
164
+
165
+ export const createParallelAxes = action(
166
+ {
167
+ op: "createParallelAxes",
168
+ description: "Create ordinary line and text graphics for Parallel dimensions."
169
+ },
170
+ function (args = {}) {
171
+ validateOptionObject(args, CREATE_OPTIONS, "createParallelAxes");
172
+ const target = resolveParallelAxisTarget(this, args.target);
173
+ const { coordinate, dimensions } = requireParallelAxisLayer(this, target);
174
+ if (args.coordinate !== undefined && args.coordinate !== coordinate.id) {
175
+ throw new Error(
176
+ `Parallel layer "${target}" uses coordinate "${coordinate.id}".`
177
+ );
178
+ }
179
+ for (const id of [
180
+ "parallelAxisLines", "parallelAxisTicks", "parallelAxisLabels",
181
+ "parallelAxisTitles"
182
+ ]) {
183
+ if (this.graphicSpec.objects[id] !== undefined) {
184
+ throw new Error("createParallelAxes requires missing Parallel axes.");
185
+ }
186
+ }
187
+ const placement = resolvePlotGraphicPlacement(this);
188
+ let next = this
189
+ .editSemantic({
190
+ property: "guide.axis.parallel.target",
191
+ value: target
192
+ })
193
+ .editSemantic({
194
+ property: "guide.axis.parallel.coordinate",
195
+ value: coordinate.id
196
+ })
197
+ .editSemantic({
198
+ property: "guide.axis.parallel.scales",
199
+ value: dimensions.map(dimension => dimension.scale)
200
+ })
201
+ .createGraphics({ id: "parallelAxisLines", type: "line", length: 0, ...placement })
202
+ .editGraphics({ target: "parallelAxisLines", property: "stroke", value: DEFAULT_COLORS.axis })
203
+ .editGraphics({ target: "parallelAxisLines", property: "strokeWidth", value: 1.25 })
204
+ .createGraphics({ id: "parallelAxisTicks", type: "line", length: 0, ...placement })
205
+ .editGraphics({ target: "parallelAxisTicks", property: "stroke", value: DEFAULT_COLORS.mutedText })
206
+ .editGraphics({ target: "parallelAxisTicks", property: "strokeWidth", value: 1 })
207
+ .createGraphics({ id: "parallelAxisLabels", type: "text", length: 0, ...placement })
208
+ .editGraphics({ target: "parallelAxisLabels", property: "fill", value: DEFAULT_COLORS.axis })
209
+ .editGraphics({ target: "parallelAxisLabels", property: "fontSize", value: 11 })
210
+ .editGraphics({ target: "parallelAxisLabels", property: "fontFamily", value: DEFAULT_FONT_FAMILY })
211
+ .editGraphics({ target: "parallelAxisLabels", property: "fontWeight", value: "normal" })
212
+ .editGraphics({ target: "parallelAxisLabels", property: "textAlign", value: "right" })
213
+ .editGraphics({ target: "parallelAxisLabels", property: "textBaseline", value: "middle" })
214
+ .createGraphics({ id: "parallelAxisTitles", type: "text", length: 0, ...placement })
215
+ .editGraphics({ target: "parallelAxisTitles", property: "fill", value: DEFAULT_COLORS.axisTitle })
216
+ .editGraphics({ target: "parallelAxisTitles", property: "fontSize", value: 13 })
217
+ .editGraphics({ target: "parallelAxisTitles", property: "fontFamily", value: DEFAULT_FONT_FAMILY })
218
+ .editGraphics({ target: "parallelAxisTitles", property: "fontWeight", value: 600 })
219
+ .editGraphics({ target: "parallelAxisTitles", property: "textAlign", value: "center" })
220
+ .editGraphics({ target: "parallelAxisTitles", property: "textBaseline", value: "middle" });
221
+ return next.rematerializeParallelAxes({ target });
222
+ }
223
+ );
224
+
225
+ export function registerParallelAxisActions(ProgramClass) {
226
+ ProgramClass.prototype.createParallelAxes = createParallelAxes;
227
+ ProgramClass.prototype.rematerializeParallelAxes = rematerializeParallelAxes;
228
+ }
@@ -24,6 +24,7 @@ import {
24
24
  } from "./policy.js";
25
25
  import { findCanvasGraphic, resolvePlotGraphicPlacement } from
26
26
  "../../../materialization/graphicHierarchy.js";
27
+ import { resolveTextBounds } from "../../../core/textMetrics.js";
27
28
 
28
29
  const CREATE_OPTIONS = Object.freeze([
29
30
  "text", "scale", "position", "at", "offset", "rotation", "color",
@@ -132,17 +133,19 @@ function resolveGeometry(program, channel, config) {
132
133
  offset: config.offset
133
134
  });
134
135
  const text = program.semanticSpec.guides.axis?.[channel]?.title ?? "";
135
- const width = [...text].length * config.fontSize * 0.6;
136
- const height = config.fontSize;
137
- const cosine = Math.cos(config.rotation);
138
- const sine = Math.sin(config.rotation);
139
- const extentX = Math.abs(cosine) * width / 2 + Math.abs(sine) * height / 2;
140
- const extentY = Math.abs(sine) * width / 2 + Math.abs(cosine) * height / 2;
136
+ const titleBounds = resolveTextBounds({
137
+ ...geometry,
138
+ text,
139
+ fontSize: config.fontSize,
140
+ textAlign: "center",
141
+ textBaseline: "middle",
142
+ rotation: config.rotation
143
+ });
141
144
  const canvas = findCanvasGraphic(program)?.properties;
142
145
  const newEdgeFits = config.position === "top"
143
- ? geometry.y - extentY >= 0
146
+ ? titleBounds.top >= 0
144
147
  : config.position === "right"
145
- ? geometry.x + extentX <= canvas?.width
148
+ ? titleBounds.right <= canvas?.width
146
149
  : true;
147
150
  if (!canvas || !newEdgeFits) {
148
151
  throw new Error(`The ${channel}-axis title does not fit the Canvas margin.`);
@@ -5,6 +5,9 @@ import {
5
5
  resolveAutomaticGridOptions,
6
6
  resolveGuideApplicability
7
7
  } from "./applicability.js";
8
+ import { findDataset } from "../../selectors/datasets.js";
9
+ import { findUpstreamTransform } from
10
+ "../../materialization/dataProvenance.js";
8
11
 
9
12
  const OPTIONS = Object.freeze(["axes", "grid", "legend"]);
10
13
 
@@ -21,16 +24,51 @@ function validateOptions(args) {
21
24
  validateGuideOption(args.legend, "createGuides legend");
22
25
  }
23
26
 
24
- function inferAxesOptions(program) {
27
+ function inferAxesOptions(program, applicability) {
25
28
  const horizontalInterval = program.semanticSpec.layers.some(layer =>
26
29
  layer.mark?.type === "rule" &&
27
30
  layer.encoding?.x?.fieldType === "quantitative" &&
28
31
  layer.encoding?.x2?.fieldType === "quantitative" &&
29
32
  ["nominal", "ordinal", "temporal"].includes(layer.encoding?.y?.fieldType)
30
33
  );
31
- return horizontalInterval
34
+ const inferred = horizontalInterval
32
35
  ? { x: { ticksAndLabels: { count: 7 } } }
33
36
  : {};
37
+ const directions = applicability.axes.directions;
38
+ const hasStoredY = program.semanticSpec.layers.some(
39
+ layer => layer.encoding?.y !== undefined
40
+ );
41
+ if (!directions.y && hasStoredY) inferred.y = false;
42
+ const horizonLayers = program.semanticSpec.layers.filter(layer =>
43
+ findUpstreamTransform(
44
+ program,
45
+ findDataset(program, layer.data),
46
+ "horizon"
47
+ ) !== undefined
48
+ );
49
+ if (horizonLayers.length === 1 && directions.x && !directions.y) {
50
+ const scaleId = horizonLayers[0].encoding?.x?.scale;
51
+ const domain = program.resolvedScales?.[scaleId]?.domain;
52
+ if (
53
+ Array.isArray(domain) &&
54
+ domain.length === 2 &&
55
+ domain.every(Number.isFinite) &&
56
+ domain[0] !== domain[1]
57
+ ) {
58
+ const low = Math.min(...domain);
59
+ const high = Math.max(...domain);
60
+ inferred.x = {
61
+ ...(inferred.x ?? {}),
62
+ ticksAndLabels: {
63
+ values: Array.from(
64
+ { length: 6 },
65
+ (_, index) => low + (high - low) * index / 5
66
+ )
67
+ }
68
+ };
69
+ }
70
+ }
71
+ return inferred;
34
72
  }
35
73
 
36
74
  function selectOption(explicit, applicable) {
@@ -39,6 +77,17 @@ function selectOption(explicit, applicable) {
39
77
  return applicable ? {} : undefined;
40
78
  }
41
79
 
80
+ function mergeInferredOptions(inferred, explicit) {
81
+ if (!isPlainObject(inferred) || !isPlainObject(explicit)) return explicit;
82
+ const merged = { ...inferred };
83
+ for (const [key, value] of Object.entries(explicit)) {
84
+ merged[key] = isPlainObject(value) && isPlainObject(inferred[key])
85
+ ? mergeInferredOptions(inferred[key], value)
86
+ : value;
87
+ }
88
+ return merged;
89
+ }
90
+
42
91
  const createGuides = action(
43
92
  {
44
93
  op: "createGuides",
@@ -47,10 +96,16 @@ const createGuides = action(
47
96
  function (args = {}) {
48
97
  validateOptions(args);
49
98
  const applicability = resolveGuideApplicability(this);
50
- const hasAxes = applicability.axes.cartesian || applicability.axes.polar;
99
+ const hasAxes = applicability.axes.cartesian || applicability.axes.polar ||
100
+ applicability.axes.parallel;
101
+ const inferredAxes = applicability.axes.cartesian
102
+ ? inferAxesOptions(this, applicability)
103
+ : {};
51
104
  const axes = args.axes === undefined && applicability.axes.cartesian
52
- ? inferAxesOptions(this)
53
- : selectOption(args.axes, hasAxes);
105
+ ? inferredAxes
106
+ : args.axes !== undefined && args.axes !== false && hasAxes
107
+ ? mergeInferredOptions(inferredAxes, args.axes)
108
+ : selectOption(args.axes, hasAxes);
54
109
  const grid = args.grid === undefined &&
55
110
  (applicability.grid.cartesian || applicability.grid.polar)
56
111
  ? resolveAutomaticGridOptions(this)