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.
- package/CHANGELOG.md +28 -3
- package/README.md +95 -83
- package/package.json +11 -6
- package/src/actions/data/regression.js +1 -1
- package/src/actions/encodings/appearance.js +1 -1
- package/src/actions/encodings/color/continuous.js +137 -0
- package/src/actions/encodings/color/index.js +151 -0
- package/src/actions/encodings/color/layout.js +91 -0
- package/src/actions/encodings/color/policy.js +88 -0
- package/src/actions/encodings/index.js +2 -2
- package/src/actions/encodings/offset.js +1 -1
- package/src/actions/encodings/{position.js → position/index.js} +6 -6
- package/src/actions/encodings/position/resolve.js +1 -1
- package/src/actions/encodings/ranged.js +2 -2
- package/src/actions/encodings/strokeDash.js +1 -1
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/options.js +1 -1
- package/src/actions/errorBars/options.js +1 -1
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/facets/derive.js +2 -2
- package/src/actions/facets/guides.js +1 -1
- package/src/actions/guides/axes/labels.js +1 -1
- package/src/actions/guides/axes/ticks.js +1 -1
- package/src/actions/guides/axes/titles.js +1 -1
- package/src/actions/guides/grids/resolve.js +1 -1
- package/src/actions/guides/legends/categorical/layout.js +4 -2
- package/src/actions/guides/legends/continuous/interval.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +1 -1
- package/src/actions/guides/legends/edit.js +2 -20
- package/src/actions/guides/legends/remove.js +2 -20
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/target.js +25 -0
- package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
- package/src/actions/guides/polar/index.js +1 -1
- package/src/actions/guides/polar/resolve.js +1 -1
- package/src/actions/guides/tickValues.js +1 -1
- package/src/actions/intervals/resolve.js +1 -1
- package/src/actions/marks/arc/actions.js +2 -2
- package/src/actions/marks/area/actions.js +2 -2
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/line/actions.js +3 -3
- package/src/actions/marks/rect/actions.js +1 -1
- package/src/actions/marks/rule/actions.js +1 -1
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +1 -1
- package/src/actions/primitives/semanticValue.js +1 -1
- package/src/actions/regression/components.js +1 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +1 -1
- package/src/actions/scales/consumers/common.js +91 -0
- package/src/actions/scales/consumers/families.js +47 -0
- package/src/actions/scales/consumers/index.js +41 -0
- package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/definitions.js +2 -2
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/materialize.js +2 -2
- package/src/core/ChartProgram.js +22 -236
- package/src/core/compositionState.js +169 -0
- package/src/core/materializationState.js +65 -0
- package/src/core/programState.js +28 -0
- package/src/grammar/arcs.js +1 -1
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/bars/aggregate.js +1 -1
- package/src/grammar/density.js +1 -1
- package/src/grammar/{facets.js → facets/index.js} +5 -5
- package/src/grammar/interval.js +2 -96
- package/src/grammar/lineSeries.js +1 -1
- package/src/grammar/palettes.js +5 -7
- package/src/grammar/polarLineCommands.js +1 -1
- package/src/grammar/regression/derive.js +97 -0
- package/src/grammar/regression/index.js +22 -0
- package/src/grammar/regression/models.js +282 -0
- package/src/grammar/regression/parameters.js +106 -0
- package/src/grammar/rules.js +1 -1
- package/src/grammar/scales/color.js +4 -2
- package/src/grammar/statistics/studentT.js +98 -0
- package/src/grammar/transforms.js +1 -1
- package/src/materialization/bars/aggregate.js +1 -1
- package/src/materialization/bars/grouped.js +1 -1
- package/src/materialization/bars/histogram.js +1 -1
- package/src/materialization/bars/ranged.js +1 -1
- package/src/materialization/density.js +1 -1
- package/src/materialization/dependencies.js +6 -87
- package/src/materialization/encodings.js +2 -2
- package/src/materialization/facetGuides/index.js +2 -0
- package/src/materialization/facetGuides/legacyCategorical.js +125 -0
- package/src/materialization/facetGuides/placement.js +126 -0
- package/src/materialization/facetGuides/preparation.js +188 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/marks/capabilities.js +141 -0
- package/src/materialization/marks/index.js +105 -0
- package/src/materialization/marks/policies.js +87 -0
- package/src/materialization/rect.js +1 -1
- package/src/materialization/rowEncoding.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +94 -0
- package/src/materialization/scales/map.js +1 -1
- package/src/materialization/scales/policies/series.js +1 -1
- package/src/materialization/scales/resolve.js +1 -1
- package/src/materialization/selection/items/bar.js +1 -1
- package/src/materialization/selection/styles.js +1 -1
- package/src/renderers/canvas/text.js +14 -1
- package/types/extension.d.ts +4 -1
- package/src/actions/encodings/color.js +0 -424
- package/src/grammar/regression.js +0 -578
- package/src/grammar/scales.js +0 -1
- package/src/materialization/facetGuides.js +0 -426
- package/src/materialization/marks.js +0 -316
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const LANCZOS = Object.freeze([
|
|
2
|
+
676.5203681218851,
|
|
3
|
+
-1259.1392167224028,
|
|
4
|
+
771.3234287776531,
|
|
5
|
+
-176.6150291621406,
|
|
6
|
+
12.507343278686905,
|
|
7
|
+
-0.13857109526572012,
|
|
8
|
+
9.984369578019572e-6,
|
|
9
|
+
1.5056327351493116e-7
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function logGamma(value) {
|
|
13
|
+
if (value < 0.5) {
|
|
14
|
+
return Math.log(Math.PI) - Math.log(Math.sin(Math.PI * value)) -
|
|
15
|
+
logGamma(1 - value);
|
|
16
|
+
}
|
|
17
|
+
const shifted = value - 1;
|
|
18
|
+
let sum = 0.9999999999998099;
|
|
19
|
+
for (const [index, coefficient] of LANCZOS.entries()) {
|
|
20
|
+
sum += coefficient / (shifted + index + 1);
|
|
21
|
+
}
|
|
22
|
+
const base = shifted + LANCZOS.length - 0.5;
|
|
23
|
+
return 0.5 * Math.log(2 * Math.PI) +
|
|
24
|
+
(shifted + 0.5) * Math.log(base) - base + Math.log(sum);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function betaContinuedFraction(a, b, value) {
|
|
28
|
+
const maximumIterations = 200;
|
|
29
|
+
const epsilon = 3e-14;
|
|
30
|
+
const floor = 1e-300;
|
|
31
|
+
const sum = a + b;
|
|
32
|
+
let c = 1;
|
|
33
|
+
let d = 1 - sum * value / (a + 1);
|
|
34
|
+
if (Math.abs(d) < floor) d = floor;
|
|
35
|
+
d = 1 / d;
|
|
36
|
+
let result = d;
|
|
37
|
+
|
|
38
|
+
for (let iteration = 1; iteration <= maximumIterations; iteration += 1) {
|
|
39
|
+
const even = 2 * iteration;
|
|
40
|
+
let coefficient = iteration * (b - iteration) * value /
|
|
41
|
+
((a + even - 1) * (a + even));
|
|
42
|
+
d = 1 + coefficient * d;
|
|
43
|
+
if (Math.abs(d) < floor) d = floor;
|
|
44
|
+
c = 1 + coefficient / c;
|
|
45
|
+
if (Math.abs(c) < floor) c = floor;
|
|
46
|
+
d = 1 / d;
|
|
47
|
+
result *= d * c;
|
|
48
|
+
|
|
49
|
+
coefficient = -(a + iteration) * (sum + iteration) * value /
|
|
50
|
+
((a + even) * (a + even + 1));
|
|
51
|
+
d = 1 + coefficient * d;
|
|
52
|
+
if (Math.abs(d) < floor) d = floor;
|
|
53
|
+
c = 1 + coefficient / c;
|
|
54
|
+
if (Math.abs(c) < floor) c = floor;
|
|
55
|
+
d = 1 / d;
|
|
56
|
+
const delta = d * c;
|
|
57
|
+
result *= delta;
|
|
58
|
+
if (Math.abs(delta - 1) <= epsilon) return result;
|
|
59
|
+
}
|
|
60
|
+
throw new Error("Student-t calculation did not converge.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function regularizedIncompleteBeta(value, a, b) {
|
|
64
|
+
if (value === 0 || value === 1) return value;
|
|
65
|
+
const factor = Math.exp(
|
|
66
|
+
logGamma(a + b) - logGamma(a) - logGamma(b) +
|
|
67
|
+
a * Math.log(value) + b * Math.log1p(-value)
|
|
68
|
+
);
|
|
69
|
+
if (value < (a + 1) / (a + b + 2)) {
|
|
70
|
+
return factor * betaContinuedFraction(a, b, value) / a;
|
|
71
|
+
}
|
|
72
|
+
return 1 - factor * betaContinuedFraction(b, a, 1 - value) / b;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function studentTCdf(value, degreesOfFreedom) {
|
|
76
|
+
if (value === 0) return 0.5;
|
|
77
|
+
const ratio = degreesOfFreedom /
|
|
78
|
+
(degreesOfFreedom + value * value);
|
|
79
|
+
const tail = regularizedIncompleteBeta(
|
|
80
|
+
ratio,
|
|
81
|
+
degreesOfFreedom / 2,
|
|
82
|
+
0.5
|
|
83
|
+
) / 2;
|
|
84
|
+
return value > 0 ? 1 - tail : tail;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function studentTCriticalValue(level, degreesOfFreedom) {
|
|
88
|
+
const probability = (1 + level) / 2;
|
|
89
|
+
let low = 0;
|
|
90
|
+
let high = 1;
|
|
91
|
+
while (studentTCdf(high, degreesOfFreedom) < probability) high *= 2;
|
|
92
|
+
for (let iteration = 0; iteration < 100; iteration += 1) {
|
|
93
|
+
const midpoint = (low + high) / 2;
|
|
94
|
+
if (studentTCdf(midpoint, degreesOfFreedom) < probability) low = midpoint;
|
|
95
|
+
else high = midpoint;
|
|
96
|
+
}
|
|
97
|
+
return (low + high) / 2;
|
|
98
|
+
}
|
|
@@ -4,7 +4,7 @@ import { validateDensityTransform } from "./density.js";
|
|
|
4
4
|
import { validateFilterTransform } from "./filter.js";
|
|
5
5
|
import { validateIntervalTransform } from "./interval.js";
|
|
6
6
|
import { validateMarkFilterTransform } from "./markFilter.js";
|
|
7
|
-
import { validateRegressionTransform } from "./regression.js";
|
|
7
|
+
import { validateRegressionTransform } from "./regression/index.js";
|
|
8
8
|
|
|
9
9
|
const TRANSFORM_VALIDATORS = Object.freeze({
|
|
10
10
|
boxOutlier: validateBoxTransform,
|
|
@@ -2,7 +2,7 @@ import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
|
|
|
2
2
|
import {
|
|
3
3
|
mapContinuousScaleValues,
|
|
4
4
|
mapOrdinalPositionValues
|
|
5
|
-
} from "../../grammar/scales.js";
|
|
5
|
+
} from "../../grammar/scales/index.js";
|
|
6
6
|
import { sameOrderedValues } from "../../core/validation.js";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_BAR_STROKE,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
mapOrdinalValues,
|
|
9
9
|
readNominalField,
|
|
10
10
|
readQuantitativeField
|
|
11
|
-
} from "../../grammar/scales.js";
|
|
11
|
+
} from "../../grammar/scales/index.js";
|
|
12
12
|
import { resolveBarColorLayout } from "../../grammar/bars/policy.js";
|
|
13
13
|
import { layoutSeriesPartition } from "../../grammar/seriesLayout.js";
|
|
14
14
|
import {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales.js";
|
|
1
|
+
import { mapContinuousScaleValues, mapOrdinalPositionValues, mapOrdinalValues } from "../../grammar/scales/index.js";
|
|
2
2
|
import { resolveBarWidth } from "../../grammar/bars/geometry.js";
|
|
3
3
|
import { DEFAULT_BAR_FILL, DEFAULT_BAR_STROKE, DEFAULT_BAR_STROKE_WIDTH } from "./resolve.js";
|
|
4
4
|
|
|
@@ -3,48 +3,17 @@ import {
|
|
|
3
3
|
getExistingMarkRematerializationStep,
|
|
4
4
|
getMarkMaterializationStep,
|
|
5
5
|
getSourceDependentMarkSteps
|
|
6
|
-
} from "./marks.js";
|
|
6
|
+
} from "./marks/index.js";
|
|
7
7
|
import { requireLayer } from "../selectors/layers.js";
|
|
8
8
|
import {
|
|
9
9
|
applyMaterializationPlan,
|
|
10
10
|
buildMaterializationPlan
|
|
11
11
|
} from "./planner.js";
|
|
12
|
+
import { hasMaterializedLegend } from "./legends.js";
|
|
12
13
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from "./
|
|
16
|
-
import { POSITION_ENCODING_CHANNELS } from "../core/vocabulary.js";
|
|
17
|
-
|
|
18
|
-
function usesPositionalScale(program, id) {
|
|
19
|
-
return program.semanticSpec.layers.some(layer =>
|
|
20
|
-
POSITION_ENCODING_CHANNELS.some(
|
|
21
|
-
channel => layer.encoding?.[channel]?.scale === id
|
|
22
|
-
)
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function usesRadialScale(program, id) {
|
|
27
|
-
return program.semanticSpec.layers.some(layer =>
|
|
28
|
-
layer.encoding?.radius?.scale === id
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function needsCanvasScaleRematerialization(program, scale) {
|
|
33
|
-
return (
|
|
34
|
-
(scale.range === "auto" ||
|
|
35
|
-
usesRadialScale(program, scale.id) ||
|
|
36
|
-
program.semanticSpec.guides.axis?.x?.scale === scale.id ||
|
|
37
|
-
program.semanticSpec.guides.axis?.y?.scale === scale.id ||
|
|
38
|
-
program.semanticSpec.guides.axis?.theta?.scale === scale.id ||
|
|
39
|
-
program.semanticSpec.guides.axis?.radius?.scale === scale.id ||
|
|
40
|
-
program.semanticSpec.guides.grid?.horizontal?.scale === scale.id ||
|
|
41
|
-
program.semanticSpec.guides.grid?.vertical?.scale === scale.id ||
|
|
42
|
-
program.semanticSpec.guides.grid?.theta?.scale === scale.id ||
|
|
43
|
-
program.semanticSpec.guides.grid?.radial?.scale === scale.id) &&
|
|
44
|
-
program.resolvedScales[scale.id] !== undefined &&
|
|
45
|
-
usesPositionalScale(program, scale.id)
|
|
46
|
-
);
|
|
47
|
-
}
|
|
14
|
+
needsCanvasScaleRematerialization,
|
|
15
|
+
planScaleGuideRematerialization
|
|
16
|
+
} from "./scaleGuideDependencies.js";
|
|
48
17
|
|
|
49
18
|
function hasTitle(program) {
|
|
50
19
|
return (
|
|
@@ -96,57 +65,6 @@ export function planCanvasRematerialization(program) {
|
|
|
96
65
|
return buildMaterializationPlan({ scales, marks, guides, layout });
|
|
97
66
|
}
|
|
98
67
|
|
|
99
|
-
export function planScaleGuideRematerialization(program, id) {
|
|
100
|
-
const guides = [];
|
|
101
|
-
const objects = program.graphicSpec?.objects ?? {};
|
|
102
|
-
const guideConfigs = program.guideConfigs ?? {};
|
|
103
|
-
if (
|
|
104
|
-
objects.xAxisLine &&
|
|
105
|
-
program.semanticSpec.guides.axis?.x?.scale === id
|
|
106
|
-
) {
|
|
107
|
-
guides.push({ op: "editXAxisLine" });
|
|
108
|
-
}
|
|
109
|
-
if (
|
|
110
|
-
objects.yAxisLine &&
|
|
111
|
-
program.semanticSpec.guides.axis?.y?.scale === id
|
|
112
|
-
) {
|
|
113
|
-
guides.push({ op: "editYAxisLine" });
|
|
114
|
-
}
|
|
115
|
-
for (const channel of ["x", "y"]) {
|
|
116
|
-
for (const component of ["ticks", "labels", "title"]) {
|
|
117
|
-
if (guideConfigs.axis?.[channel]?.[component]?.scale === id) {
|
|
118
|
-
const suffix = component[0].toUpperCase() + component.slice(1);
|
|
119
|
-
guides.push({ op: `edit${channel.toUpperCase()}Axis${suffix}` });
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
for (const channel of ["theta", "radius"]) {
|
|
124
|
-
const prefix = channel === "theta" ? "Theta" : "Radial";
|
|
125
|
-
for (const component of ["line", "ticks", "labels", "title"]) {
|
|
126
|
-
if (guideConfigs.axis?.[channel]?.[component]?.scale === id) {
|
|
127
|
-
const suffix = component[0].toUpperCase() + component.slice(1);
|
|
128
|
-
guides.push({ op: `edit${prefix}Axis${suffix}` });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (guideConfigs.grid?.horizontal?.scale === id) {
|
|
133
|
-
guides.push({ op: "rematerializeHorizontalGrid" });
|
|
134
|
-
}
|
|
135
|
-
if (guideConfigs.grid?.vertical?.scale === id) {
|
|
136
|
-
guides.push({ op: "rematerializeVerticalGrid" });
|
|
137
|
-
}
|
|
138
|
-
if (guideConfigs.grid?.theta?.scale === id) {
|
|
139
|
-
guides.push({ op: "rematerializeThetaGrid" });
|
|
140
|
-
}
|
|
141
|
-
if (guideConfigs.grid?.radial?.scale === id) {
|
|
142
|
-
guides.push({ op: "rematerializeRadialGrid" });
|
|
143
|
-
}
|
|
144
|
-
if (materializedLegendUsesScale(program, id)) {
|
|
145
|
-
guides.push({ op: "rematerializeLegend" });
|
|
146
|
-
}
|
|
147
|
-
return buildMaterializationPlan({ guides });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
68
|
export function planLayerDataRematerialization(program, id) {
|
|
151
69
|
const layer = requireLayer(program, id);
|
|
152
70
|
const scaleIds = [...new Set(
|
|
@@ -187,3 +105,4 @@ export function applyLayerDataRematerialization(program, id) {
|
|
|
187
105
|
}
|
|
188
106
|
|
|
189
107
|
export { applyMaterializationPlan } from "./planner.js";
|
|
108
|
+
export { planScaleGuideRematerialization };
|
|
@@ -2,9 +2,9 @@ import { hasMaterializedLegend } from "./legends.js";
|
|
|
2
2
|
import { requireLayer } from "../selectors/layers.js";
|
|
3
3
|
import {
|
|
4
4
|
getEncodingMaterializationStages
|
|
5
|
-
} from "./marks.js";
|
|
5
|
+
} from "./marks/index.js";
|
|
6
6
|
import { buildMaterializationPlan } from "./planner.js";
|
|
7
|
-
import { getSourceDependentMarkSteps } from "./marks.js";
|
|
7
|
+
import { getSourceDependentMarkSteps } from "./marks/index.js";
|
|
8
8
|
|
|
9
9
|
export function planEncodingRematerialization(program, {
|
|
10
10
|
target,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveSharedFacetLegends
|
|
3
|
+
} from "../../grammar/facets/guides.js";
|
|
4
|
+
import { mapOrdinalValues } from "../../grammar/scales/index.js";
|
|
5
|
+
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
|
|
6
|
+
|
|
7
|
+
function textItem(text, x, y, options = {}) {
|
|
8
|
+
return {
|
|
9
|
+
type: "text",
|
|
10
|
+
properties: {
|
|
11
|
+
x,
|
|
12
|
+
y,
|
|
13
|
+
text: String(text),
|
|
14
|
+
fill: options.color ?? DEFAULT_COLORS.strongText,
|
|
15
|
+
fontSize: options.fontSize ?? 12,
|
|
16
|
+
fontFamily: options.fontFamily ?? DEFAULT_FONT_FAMILY,
|
|
17
|
+
fontWeight: options.fontWeight ?? "normal",
|
|
18
|
+
textAlign: options.textAlign ?? "left",
|
|
19
|
+
textBaseline: options.textBaseline ?? "middle"
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collection(program, id, items) {
|
|
25
|
+
return program
|
|
26
|
+
.createGraphics({ id, type: "collection", parent: "canvas" })
|
|
27
|
+
.editGraphics({ target: id, property: "items", value: items });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveLegacyCategoricalLegend(program) {
|
|
31
|
+
const encodings = program.semanticSpec.layers.flatMap(layer =>
|
|
32
|
+
layer.encoding?.color === undefined
|
|
33
|
+
? []
|
|
34
|
+
: [{ layer, encoding: layer.encoding.color }]
|
|
35
|
+
);
|
|
36
|
+
if (encodings.length === 0) return undefined;
|
|
37
|
+
const fields = new Set(encodings.map(entry => entry.encoding.field));
|
|
38
|
+
const scales = new Set(encodings.map(entry => entry.encoding.scale));
|
|
39
|
+
const marks = new Set(encodings.map(entry => entry.layer.mark?.type));
|
|
40
|
+
if (fields.size !== 1 || scales.size !== 1 || marks.size !== 1) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Shared facet legend requires one compatible color field, scale, and mark recipe."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (encodings.some(entry =>
|
|
46
|
+
!["nominal", "ordinal"].includes(entry.encoding.fieldType)
|
|
47
|
+
)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const firstId = program.compositionSpec.children[0];
|
|
51
|
+
const scaleId = encodings[0].encoding.scale;
|
|
52
|
+
const scale = program.children[firstId].resolvedScales[scaleId];
|
|
53
|
+
if (scale?.type !== "ordinal") return undefined;
|
|
54
|
+
const config = {
|
|
55
|
+
kind: "color",
|
|
56
|
+
target: encodings[0].layer.id,
|
|
57
|
+
field: encodings[0].encoding.field,
|
|
58
|
+
scale: scaleId,
|
|
59
|
+
title: encodings[0].encoding.field
|
|
60
|
+
};
|
|
61
|
+
const compatibility = resolveSharedFacetLegends(
|
|
62
|
+
program.compositionSpec.children.map(id => ({
|
|
63
|
+
id,
|
|
64
|
+
guideConfigs: { legend: { color: config } },
|
|
65
|
+
resolvedScales: program.children[id].resolvedScales
|
|
66
|
+
}))
|
|
67
|
+
);
|
|
68
|
+
return {
|
|
69
|
+
mode: "legacyCategorical",
|
|
70
|
+
compatibility,
|
|
71
|
+
reservation: { gap: 18, width: 132 },
|
|
72
|
+
legend: {
|
|
73
|
+
field: config.field,
|
|
74
|
+
mark: encodings[0].layer.mark.type,
|
|
75
|
+
domain: scale.domain,
|
|
76
|
+
colors: mapOrdinalValues(scale.domain, scale.domain, scale.range)
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function materializeLegacyCategoricalLegend(program, prepared, layout) {
|
|
82
|
+
const legend = prepared.legend;
|
|
83
|
+
const items = [textItem(legend.field, layout.legend.x, layout.legend.y, {
|
|
84
|
+
fontSize: 12,
|
|
85
|
+
fontWeight: 700,
|
|
86
|
+
textBaseline: "top"
|
|
87
|
+
})];
|
|
88
|
+
legend.domain.forEach((value, index) => {
|
|
89
|
+
const y = layout.legend.y + 29 + index * 26;
|
|
90
|
+
items.push(legend.mark === "point"
|
|
91
|
+
? {
|
|
92
|
+
type: "circle",
|
|
93
|
+
properties: {
|
|
94
|
+
x: layout.legend.x + 7,
|
|
95
|
+
y,
|
|
96
|
+
radius: 5.5,
|
|
97
|
+
fill: legend.colors[index],
|
|
98
|
+
stroke: "#ffffff",
|
|
99
|
+
strokeWidth: 0.35,
|
|
100
|
+
opacity: 1
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
: {
|
|
104
|
+
type: "rect",
|
|
105
|
+
properties: {
|
|
106
|
+
x: layout.legend.x + 1,
|
|
107
|
+
y: y - 6,
|
|
108
|
+
width: 12,
|
|
109
|
+
height: 12,
|
|
110
|
+
fill: legend.colors[index],
|
|
111
|
+
stroke: "#ffffff",
|
|
112
|
+
strokeWidth: 0.6,
|
|
113
|
+
opacity: 1
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
items.push(textItem(value, layout.legend.x + 22, y, {
|
|
117
|
+
fontSize: 10.5
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
return collection(
|
|
121
|
+
program,
|
|
122
|
+
`${program.compositionSpec.id}-legend`,
|
|
123
|
+
items
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { planFacetGuideOwnership } from "../../grammar/facets/guides.js";
|
|
3
|
+
import {
|
|
4
|
+
axisGraphicIds,
|
|
5
|
+
allLegendGraphicIds
|
|
6
|
+
} from "../guides/resources.js";
|
|
7
|
+
import { attachSnapshotObject } from "../composition.js";
|
|
8
|
+
import { namespaceGraphicId } from "../compositionSnapshot.js";
|
|
9
|
+
import {
|
|
10
|
+
materializeLegacyCategoricalLegend
|
|
11
|
+
} from "./legacyCategorical.js";
|
|
12
|
+
import { legendKinds, prepareSharedFacetLegend } from "./preparation.js";
|
|
13
|
+
|
|
14
|
+
function copyLegendConfig(program, source) {
|
|
15
|
+
let next = program;
|
|
16
|
+
for (const [kind, config] of Object.entries(source.guideConfigs.legend ?? {})) {
|
|
17
|
+
next = next._withLegendConfig(kind, config);
|
|
18
|
+
}
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function removeNamespacedGraphics(program, namespace, ids) {
|
|
23
|
+
let next = program;
|
|
24
|
+
for (const id of ids) {
|
|
25
|
+
const target = namespaceGraphicId(namespace, id);
|
|
26
|
+
if (next.graphicSpec.objects[target] !== undefined) {
|
|
27
|
+
next = next.editGraphics({ target, remove: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return next;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function gradientAnchor(prepared, plot, layout) {
|
|
34
|
+
const strips = prepared.source.graphicSpec.objects.colorGradientStrips;
|
|
35
|
+
if (strips?.items?.[0] === undefined) return undefined;
|
|
36
|
+
const properties = strips.items[0].properties;
|
|
37
|
+
const length = prepared.source.guideConfigs.legend.gradient.gradient.length;
|
|
38
|
+
return {
|
|
39
|
+
sourceX: properties.x,
|
|
40
|
+
sourceY: properties.y,
|
|
41
|
+
targetX: layout.legend.x,
|
|
42
|
+
targetY: plot.y + (plot.height - length) / 2
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function legendTranslation(prepared, plot, layout) {
|
|
47
|
+
const gradient = gradientAnchor(prepared, plot, layout);
|
|
48
|
+
if (gradient !== undefined) {
|
|
49
|
+
return {
|
|
50
|
+
x: gradient.targetX - gradient.sourceX,
|
|
51
|
+
y: gradient.targetY - gradient.sourceY
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const height = prepared.bounds.bottom - prepared.bounds.top;
|
|
55
|
+
return {
|
|
56
|
+
x: layout.legend.x - prepared.bounds.left,
|
|
57
|
+
y: plot.y + Math.max(0, (plot.height - height) / 2) - prepared.bounds.top
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function attachParentLegend(program, prepared, plot, layout) {
|
|
62
|
+
const translation = legendTranslation(prepared, plot, layout);
|
|
63
|
+
const canvas = prepared.source.graphicSpec.objects.canvas;
|
|
64
|
+
const id = `${program.compositionSpec.id}-shared-legend`;
|
|
65
|
+
let next = copyLegendConfig(program, prepared.source)
|
|
66
|
+
.createGraphics({ id, type: "canvas", parent: "canvas" });
|
|
67
|
+
for (const [property, value] of Object.entries({
|
|
68
|
+
x: translation.x,
|
|
69
|
+
y: translation.y,
|
|
70
|
+
width: canvas.properties.width,
|
|
71
|
+
height: canvas.properties.height,
|
|
72
|
+
background: "transparent"
|
|
73
|
+
})) {
|
|
74
|
+
next = next.editGraphics({ target: id, property, value });
|
|
75
|
+
}
|
|
76
|
+
for (const root of prepared.roots) {
|
|
77
|
+
next = attachSnapshotObject(next, prepared.source.graphicSpec, root, id);
|
|
78
|
+
}
|
|
79
|
+
return next;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function applyFacetGuideComposition(program, { layout, plot } = {}) {
|
|
83
|
+
if (!isPlainObject(layout) || !Array.isArray(layout.children)) {
|
|
84
|
+
throw new TypeError("Facet guide composition requires a resolved layout.");
|
|
85
|
+
}
|
|
86
|
+
if (!isPlainObject(plot)) {
|
|
87
|
+
throw new TypeError("Facet guide composition requires resolved plot bounds.");
|
|
88
|
+
}
|
|
89
|
+
const prepared = prepareSharedFacetLegend(program);
|
|
90
|
+
const children = layout.children.map(cell => {
|
|
91
|
+
const child = program.children[cell.id];
|
|
92
|
+
return {
|
|
93
|
+
id: cell.id,
|
|
94
|
+
axes: Object.keys(child.guideConfigs.axis ?? {})
|
|
95
|
+
.filter(channel => ["x", "y"].includes(channel)),
|
|
96
|
+
legendKinds: legendKinds(child)
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
const ownership = planFacetGuideOwnership({
|
|
100
|
+
placements: layout.children,
|
|
101
|
+
children,
|
|
102
|
+
axes: program.compositionSpec.facet.guides.axes,
|
|
103
|
+
legend: program.compositionSpec.facet.guides.legend,
|
|
104
|
+
...(prepared === undefined ? {} : { sharedLegends: prepared.compatibility })
|
|
105
|
+
});
|
|
106
|
+
let next = program;
|
|
107
|
+
for (const cell of layout.children) {
|
|
108
|
+
const namespace = `${program.compositionSpec.id}-${cell.id}`;
|
|
109
|
+
const childPlan = ownership.children[cell.id];
|
|
110
|
+
for (const channel of childPlan.removeAxes) {
|
|
111
|
+
next = removeNamespacedGraphics(next, namespace, axisGraphicIds(channel));
|
|
112
|
+
}
|
|
113
|
+
if (childPlan.removeLegends.length > 0) {
|
|
114
|
+
next = removeNamespacedGraphics(
|
|
115
|
+
next,
|
|
116
|
+
namespace,
|
|
117
|
+
allLegendGraphicIds(childPlan.removeLegends)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (prepared === undefined) return next;
|
|
122
|
+
if (prepared.mode === "legacyCategorical") {
|
|
123
|
+
return materializeLegacyCategoricalLegend(next, prepared, layout);
|
|
124
|
+
}
|
|
125
|
+
return attachParentLegend(next, prepared, plot, layout);
|
|
126
|
+
}
|