ggaction 0.0.7 → 0.0.8
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 +20 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/actions/categoryOrder/index.js +111 -0
- package/src/actions/data/index.js +6 -0
- package/src/actions/data/timeUnit.js +48 -0
- package/src/actions/encodings/angle.js +77 -0
- package/src/actions/encodings/color/index.js +17 -13
- package/src/actions/encodings/color/layout.js +2 -0
- package/src/actions/encodings/color/policy.js +3 -0
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/position/policies/area.js +40 -2
- package/src/actions/encodings/position/policies/bar.js +6 -0
- package/src/actions/encodings/position/policies/index.js +2 -0
- package/src/actions/encodings/position/policies/tick.js +19 -0
- package/src/actions/encodings/remove.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +23 -6
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/continuous/common.js +17 -5
- package/src/actions/guides/legends/continuous/gradient.js +12 -8
- package/src/actions/guides/legends/continuous/opacity.js +70 -17
- package/src/actions/guides/legends/edit.js +13 -8
- package/src/actions/guides/legends/lane.js +468 -0
- package/src/actions/guides/legends/remove.js +3 -7
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +3 -1
- package/src/actions/marks/area/materialize.js +12 -3
- package/src/actions/marks/index.js +2 -0
- package/src/actions/marks/point/materialize.js +17 -5
- package/src/actions/marks/tick/actions.js +225 -0
- package/src/actions/marks/tick/index.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +2 -0
- package/src/actions/scales/consumers/index.js +8 -0
- package/src/actions/scales/consumers/seriesLayout.js +22 -2
- package/src/actions/scales/materialize.js +2 -0
- package/src/actions/selection/actions.js +5 -2
- package/src/core/vocabulary.js +7 -3
- package/src/grammar/areaSeries.js +112 -2
- package/src/grammar/categoryOrder.js +138 -0
- package/src/grammar/direction.js +46 -0
- package/src/grammar/facets/index.js +1 -1
- package/src/grammar/pointShapes.js +34 -6
- package/src/grammar/positionCompatibility.js +4 -0
- package/src/grammar/schemas/semanticPath.js +2 -1
- package/src/grammar/seriesLayout.js +10 -2
- package/src/grammar/timeUnit.js +108 -0
- package/src/grammar/transforms.js +6 -0
- package/src/grammar/window.js +73 -7
- package/src/layout/legendLane.js +338 -0
- package/src/materialization/marks/capabilities.js +9 -0
- package/src/materialization/marks/index.js +2 -1
- package/src/materialization/marks/policies.js +13 -0
- package/src/materialization/scales/policies/series.js +9 -0
- package/src/materialization/scales/resolve.js +25 -3
- package/src/materialization/selection/items/index.js +1 -0
- package/src/materialization/selection/items/path.js +4 -1
- package/src/materialization/selection/items/tick.js +42 -0
- package/src/materialization/selection/policies/index.js +2 -0
- package/src/materialization/selection/policies/tick.js +10 -0
- package/types/index.d.ts +10 -0
- package/types/program.d.ts +96 -3
|
@@ -105,12 +105,21 @@ export function normalizeContinuousLegend(args, kind) {
|
|
|
105
105
|
}
|
|
106
106
|
const offset = args.offset ?? 30;
|
|
107
107
|
validateNonNegative(offset, "Legend offset");
|
|
108
|
-
const
|
|
108
|
+
const titlePosition = args.titlePosition ?? "top";
|
|
109
|
+
if (!["top", "left"].includes(titlePosition)) {
|
|
110
|
+
throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
titlePosition === "left" &&
|
|
114
|
+
(!["top", "bottom"].includes(position) || kind !== "opacity")
|
|
115
|
+
) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"Only horizontal opacity legends support left titlePosition."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const itemGap = args.itemGap ?? (titlePosition === "left" ? 20 : 28);
|
|
109
121
|
validatePositive(itemGap, "Legend itemGap");
|
|
110
122
|
if (args.title !== undefined) validateNonEmptyString(args.title, "Legend title");
|
|
111
|
-
if (args.titlePosition !== undefined && args.titlePosition !== "top") {
|
|
112
|
-
throw new Error("Continuous legends currently require top titlePosition.");
|
|
113
|
-
}
|
|
114
123
|
if (kind === "gradient") {
|
|
115
124
|
for (const key of ["symbol", "columns", "direction", "itemGap"]) {
|
|
116
125
|
if (Object.hasOwn(args, key)) {
|
|
@@ -135,7 +144,9 @@ export function normalizeContinuousLegend(args, kind) {
|
|
|
135
144
|
labels: normalizeLegendTextOptions(
|
|
136
145
|
args.labels,
|
|
137
146
|
"createLegend.labels",
|
|
138
|
-
|
|
147
|
+
titlePosition === "left"
|
|
148
|
+
? { ...DEFAULT_LABELS, offset: 8 }
|
|
149
|
+
: DEFAULT_LABELS
|
|
139
150
|
),
|
|
140
151
|
titleStyle: normalizeLegendTextOptions(
|
|
141
152
|
args.titleStyle,
|
|
@@ -143,6 +154,7 @@ export function normalizeContinuousLegend(args, kind) {
|
|
|
143
154
|
DEFAULT_TITLE
|
|
144
155
|
),
|
|
145
156
|
itemGap,
|
|
157
|
+
titlePosition,
|
|
146
158
|
border: normalizeBorder(args.border)
|
|
147
159
|
};
|
|
148
160
|
}
|
|
@@ -39,12 +39,18 @@ function resolveGradientLayout(program, config, scale) {
|
|
|
39
39
|
: config.align === "right" ? plot.x + plot.width - length
|
|
40
40
|
: plot.x + (plot.width - length) / 2;
|
|
41
41
|
y = config.position === "top"
|
|
42
|
-
? plot.y - config.offset - thickness
|
|
43
|
-
|
|
42
|
+
? plot.y - config.offset - thickness - config.labels.offset -
|
|
43
|
+
config.labels.fontSize
|
|
44
|
+
: plot.y + plot.height + config.offset +
|
|
45
|
+
config.titleStyle.fontSize + 12;
|
|
44
46
|
}
|
|
45
47
|
const title = vertical
|
|
46
48
|
? { x, y: plot.y + 20, align: "left" }
|
|
47
|
-
: {
|
|
49
|
+
: {
|
|
50
|
+
x: x + length / 2,
|
|
51
|
+
y: y - 12 - config.titleStyle.fontSize / 2,
|
|
52
|
+
align: "center"
|
|
53
|
+
};
|
|
48
54
|
const values = sampleContinuousValues(scale.domain, config.count);
|
|
49
55
|
const fractions = values.map((_, index) => index / (values.length - 1));
|
|
50
56
|
const labelOffset = config.labels.offset;
|
|
@@ -58,9 +64,7 @@ function resolveGradientLayout(program, config, scale) {
|
|
|
58
64
|
}))
|
|
59
65
|
: fractions.map(fraction => ({
|
|
60
66
|
x: x + length * fraction,
|
|
61
|
-
y: config.
|
|
62
|
-
? y - labelOffset
|
|
63
|
-
: y + thickness + labelOffset,
|
|
67
|
+
y: y + thickness + labelOffset + config.labels.fontSize / 2,
|
|
64
68
|
align: "center"
|
|
65
69
|
}));
|
|
66
70
|
const ticks = vertical
|
|
@@ -72,9 +76,9 @@ function resolveGradientLayout(program, config, scale) {
|
|
|
72
76
|
}))
|
|
73
77
|
: labels.map(label => ({
|
|
74
78
|
x1: label.x,
|
|
75
|
-
y1:
|
|
79
|
+
y1: y + thickness,
|
|
76
80
|
x2: label.x,
|
|
77
|
-
y2:
|
|
81
|
+
y2: y + thickness + 6
|
|
78
82
|
}));
|
|
79
83
|
assertLegendInsideCanvas([
|
|
80
84
|
title,
|
|
@@ -2,6 +2,7 @@ import { action } from "../../../../core/action.js";
|
|
|
2
2
|
import { isPlainObject } from "../../../../core/immutable.js";
|
|
3
3
|
import { validateKeys } from "../../../../core/validation.js";
|
|
4
4
|
import { mapLinearValues } from "../../../../grammar/scales/index.js";
|
|
5
|
+
import { measureTextWidth } from "../../../../core/textMetrics.js";
|
|
5
6
|
import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
|
|
6
7
|
import {
|
|
7
8
|
assertLegendInsideCanvas,
|
|
@@ -84,6 +85,7 @@ function resolveOpacityLayout(program, config, scale) {
|
|
|
84
85
|
const { plot, canvas } = resolveContinuousBounds(program);
|
|
85
86
|
const vertical = ["right", "left"].includes(config.position);
|
|
86
87
|
const values = sampleContinuousValues(scale.domain, config.count);
|
|
88
|
+
const texts = formatContinuousValues(values, scale.domain, "quantitative");
|
|
87
89
|
const radius = config.symbol.radius;
|
|
88
90
|
let symbols;
|
|
89
91
|
let labels;
|
|
@@ -109,29 +111,81 @@ function resolveOpacityLayout(program, config, scale) {
|
|
|
109
111
|
y: plot.y + 20,
|
|
110
112
|
align: config.position === "right" ? "left" : "right"
|
|
111
113
|
};
|
|
114
|
+
} else if (config.titlePosition === "left") {
|
|
115
|
+
const titleWidth = measureTextWidth(config.title, config.titleStyle);
|
|
116
|
+
const labelWidths = texts.map(text =>
|
|
117
|
+
measureTextWidth(text, config.labels)
|
|
118
|
+
);
|
|
119
|
+
const samplesWidth = labelWidths.reduce(
|
|
120
|
+
(sum, width) => sum + radius * 2 + config.labels.offset + width,
|
|
121
|
+
0
|
|
122
|
+
) + config.itemGap * (values.length - 1);
|
|
123
|
+
const width = titleWidth + 20 + samplesWidth;
|
|
124
|
+
const startX = config.align === "left" ? plot.x
|
|
125
|
+
: config.align === "right" ? plot.x + plot.width - width
|
|
126
|
+
: plot.x + (plot.width - width) / 2;
|
|
127
|
+
const height = Math.max(
|
|
128
|
+
radius * 2,
|
|
129
|
+
config.labels.fontSize,
|
|
130
|
+
config.titleStyle.fontSize
|
|
131
|
+
);
|
|
132
|
+
const centerY = config.position === "top"
|
|
133
|
+
? plot.y - config.offset - height / 2
|
|
134
|
+
: plot.y + plot.height + config.offset + height / 2;
|
|
135
|
+
title = { x: startX, y: centerY, align: "left" };
|
|
136
|
+
let cursor = startX + titleWidth + 20;
|
|
137
|
+
symbols = [];
|
|
138
|
+
labels = [];
|
|
139
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
140
|
+
const symbolX = cursor + radius;
|
|
141
|
+
symbols.push({ x: symbolX, y: centerY });
|
|
142
|
+
const labelX = symbolX + radius + config.labels.offset;
|
|
143
|
+
labels.push({ x: labelX, y: centerY, align: "left" });
|
|
144
|
+
cursor = labelX + labelWidths[index] + config.itemGap;
|
|
145
|
+
}
|
|
112
146
|
} else {
|
|
113
147
|
const width = (values.length - 1) * Math.max(56, config.itemGap * 2);
|
|
114
148
|
const startX = config.align === "left" ? plot.x
|
|
115
149
|
: config.align === "right" ? plot.x + plot.width - width
|
|
116
150
|
: plot.x + (plot.width - width) / 2;
|
|
151
|
+
const titleHalf = config.titleStyle.fontSize / 2;
|
|
152
|
+
const labelHalf = config.labels.fontSize / 2;
|
|
117
153
|
const y = config.position === "top"
|
|
118
|
-
? plot.y - config.offset -
|
|
119
|
-
|
|
154
|
+
? plot.y - config.offset - config.labels.fontSize -
|
|
155
|
+
config.labels.offset - radius
|
|
156
|
+
: plot.y + plot.height + config.offset +
|
|
157
|
+
config.titleStyle.fontSize + 12 + radius;
|
|
120
158
|
symbols = values.map((_, index) => ({
|
|
121
159
|
x: startX + index * width / (values.length - 1),
|
|
122
160
|
y
|
|
123
161
|
}));
|
|
124
162
|
labels = symbols.map(symbol => ({
|
|
125
163
|
x: symbol.x,
|
|
126
|
-
y: config.
|
|
127
|
-
? symbol.y - radius - config.labels.offset
|
|
128
|
-
: symbol.y + radius + config.labels.offset,
|
|
164
|
+
y: symbol.y + radius + config.labels.offset + labelHalf,
|
|
129
165
|
align: "center"
|
|
130
166
|
}));
|
|
131
|
-
title = {
|
|
167
|
+
title = {
|
|
168
|
+
x: startX + width / 2,
|
|
169
|
+
y: y - radius - 12 - titleHalf,
|
|
170
|
+
align: "center"
|
|
171
|
+
};
|
|
132
172
|
}
|
|
173
|
+
const titleWidth = measureTextWidth(config.title, config.titleStyle);
|
|
174
|
+
const titleEdge = {
|
|
175
|
+
x: title.x + (title.align === "left" ? titleWidth : -titleWidth),
|
|
176
|
+
y: title.y
|
|
177
|
+
};
|
|
178
|
+
const labelEdges = labels.map((label, index) => {
|
|
179
|
+
const width = measureTextWidth(texts[index], config.labels);
|
|
180
|
+
return {
|
|
181
|
+
x: label.x + (label.align === "left"
|
|
182
|
+
? width
|
|
183
|
+
: label.align === "right" ? -width : width / 2),
|
|
184
|
+
y: label.y
|
|
185
|
+
};
|
|
186
|
+
});
|
|
133
187
|
assertLegendInsideCanvas(
|
|
134
|
-
[title, ...symbols, ...labels],
|
|
188
|
+
[title, titleEdge, ...symbols, ...labels, ...labelEdges],
|
|
135
189
|
canvas,
|
|
136
190
|
"Opacity legend layout"
|
|
137
191
|
);
|
|
@@ -145,14 +199,17 @@ function resolveOpacityLayout(program, config, scale) {
|
|
|
145
199
|
x: symbol.x - radius,
|
|
146
200
|
y: symbol.y - radius
|
|
147
201
|
})),
|
|
148
|
-
...labels.map(label => ({
|
|
149
|
-
x: label.x + (
|
|
150
|
-
|
|
151
|
-
|
|
202
|
+
...labels.map((label, index) => ({
|
|
203
|
+
x: label.x + (label.align === "left"
|
|
204
|
+
? measureTextWidth(
|
|
205
|
+
texts[index],
|
|
206
|
+
config.labels
|
|
207
|
+
)
|
|
208
|
+
: label.align === "right" ? -42 : 0),
|
|
152
209
|
y: label.y
|
|
153
210
|
}))
|
|
154
211
|
], config.border, canvas, "Opacity legend");
|
|
155
|
-
return { values, symbols, labels, title, background };
|
|
212
|
+
return { values, texts, symbols, labels, title, background };
|
|
156
213
|
}
|
|
157
214
|
|
|
158
215
|
export const rematerializeOpacityLegend = action(
|
|
@@ -229,11 +286,7 @@ export const rematerializeOpacityLegend = action(
|
|
|
229
286
|
.editGraphics({
|
|
230
287
|
target: "opacityLegendLabels",
|
|
231
288
|
property: "text",
|
|
232
|
-
value:
|
|
233
|
-
layout.values,
|
|
234
|
-
scale.domain,
|
|
235
|
-
"quantitative"
|
|
236
|
-
)
|
|
289
|
+
value: layout.texts
|
|
237
290
|
});
|
|
238
291
|
next = editLegendBackground(
|
|
239
292
|
next,
|
|
@@ -54,7 +54,7 @@ function editContinuous(program, kind, previous, args) {
|
|
|
54
54
|
? ["target", "position", "align", "offset", "title", "labels",
|
|
55
55
|
"titleStyle", "border", "count", "gradient"]
|
|
56
56
|
: ["target", "position", "align", "offset", "title", "symbol", "labels",
|
|
57
|
-
"titleStyle", "itemGap", "border", "count"];
|
|
57
|
+
"titleStyle", "titlePosition", "itemGap", "border", "count"];
|
|
58
58
|
for (const key of Object.keys(args)) {
|
|
59
59
|
if (!allowed.includes(key)) throw new Error(`${kind} legend does not accept ${key}.`);
|
|
60
60
|
}
|
|
@@ -69,18 +69,25 @@ function editContinuous(program, kind, previous, args) {
|
|
|
69
69
|
const title = titleMode === "auto"
|
|
70
70
|
? inferred
|
|
71
71
|
: typeof titleMode === "string" ? titleMode : previous.title;
|
|
72
|
+
const titlePosition = args.titlePosition ?? previous.titlePosition;
|
|
73
|
+
const enteringInline = kind === "opacity" &&
|
|
74
|
+
titlePosition === "left" && previous.titlePosition !== "left";
|
|
75
|
+
const labels = mergeObject(previous.labels, args.labels);
|
|
72
76
|
const normalized = normalizeContinuousLegend({
|
|
73
77
|
target: previous.target,
|
|
74
78
|
position: args.position ?? previous.position,
|
|
75
79
|
align: args.align ?? previous.align,
|
|
76
80
|
offset: args.offset ?? previous.offset,
|
|
77
81
|
count: args.count ?? previous.count,
|
|
82
|
+
titlePosition,
|
|
78
83
|
title,
|
|
79
|
-
labels:
|
|
84
|
+
labels: enteringInline && args.labels?.offset === undefined
|
|
85
|
+
? { ...labels, offset: 8 }
|
|
86
|
+
: labels,
|
|
80
87
|
titleStyle: mergeObject(previous.titleStyle, args.titleStyle),
|
|
81
88
|
border: mergeBorder(previous.border, args.border),
|
|
82
89
|
...(kind === "opacity" ? {
|
|
83
|
-
itemGap: args.itemGap ?? previous.itemGap,
|
|
90
|
+
itemGap: args.itemGap ?? (enteringInline ? 20 : previous.itemGap),
|
|
84
91
|
symbol: args.symbol ?? previous.symbol
|
|
85
92
|
} : {})
|
|
86
93
|
}, kind);
|
|
@@ -112,9 +119,7 @@ function editContinuous(program, kind, previous, args) {
|
|
|
112
119
|
before: kind === "gradient" ? "colorGradientStrips" : "opacityLegendSymbols"
|
|
113
120
|
});
|
|
114
121
|
next = reconcileGraphic(next, `${prefix}Title`, titleVisible, { type: "text" });
|
|
115
|
-
return
|
|
116
|
-
? next.rematerializeGradientLegend()
|
|
117
|
-
: next.rematerializeOpacityLegend();
|
|
122
|
+
return next.rematerializeLegend();
|
|
118
123
|
}
|
|
119
124
|
|
|
120
125
|
function editInterval(program, previous, args) {
|
|
@@ -165,7 +170,7 @@ function editInterval(program, previous, args) {
|
|
|
165
170
|
next = reconcileGraphic(next, "colorLegendTitle", titleVisible, {
|
|
166
171
|
type: "text"
|
|
167
172
|
});
|
|
168
|
-
return next.
|
|
173
|
+
return next.rematerializeLegend();
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
function editStrokeWidth(program, previous, args) {
|
|
@@ -220,7 +225,7 @@ function editStrokeWidth(program, previous, args) {
|
|
|
220
225
|
next = reconcileGraphic(next, "strokeWidthLegendTitle", titleVisible, {
|
|
221
226
|
type: "text"
|
|
222
227
|
});
|
|
223
|
-
return next.
|
|
228
|
+
return next.rematerializeLegend();
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
function categoricalSymbolIds(config) {
|