@opendata-ai/openchart-core 7.11.0 → 8.0.0-rc.1
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/dist/index.d.ts +1154 -119
- package/dist/index.js +530 -41
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2 -1
- package/dist/styles.css.map +1 -0
- package/package.json +9 -3
- package/schema/chart.schema.json +8957 -0
- package/schema/table.schema.json +861 -0
- package/schema/vizspec.schema.json +17694 -0
- package/src/accessibility/alt-text.ts +39 -1
- package/src/accessibility/aria.ts +4 -2
- package/src/colors/__tests__/colorblind.test.ts +12 -0
- package/src/colors/colorblind.ts +11 -2
- package/src/colors/contrast.ts +11 -0
- package/src/colors/index.ts +9 -1
- package/src/colors/palettes.ts +47 -0
- package/src/helpers/__tests__/deep-merge.test.ts +54 -0
- package/src/helpers/__tests__/spec-builders.test.ts +3 -3
- package/src/helpers/deep-merge.ts +29 -0
- package/src/helpers/spec-builders.ts +7 -7
- package/src/index.ts +29 -1
- package/src/labels/collision.ts +1 -0
- package/src/layout/chrome.ts +24 -0
- package/src/layout/index.ts +1 -1
- package/src/locale/__tests__/format.test.ts +347 -11
- package/src/locale/format.ts +243 -18
- package/src/locale/index.ts +10 -1
- package/src/responsive/index.ts +2 -0
- package/src/responsive/metrics.ts +74 -9
- package/src/schema/__tests__/generated-artifacts.test.ts +42 -0
- package/src/schema/__tests__/schema.test.ts +179 -0
- package/src/styles/__tests__/reduced-motion-selectors.test.ts +28 -0
- package/src/styles/animation.css +197 -168
- package/src/styles/base.css +89 -62
- package/src/styles/chrome.css +173 -179
- package/src/styles/dark.css +38 -35
- package/src/styles/graph.css +116 -111
- package/src/styles/index.css +5 -0
- package/src/styles/keyframes.css +115 -100
- package/src/styles/legend.css +14 -12
- package/src/styles/reduced-motion.css +61 -40
- package/src/styles/series-search.css +153 -0
- package/src/styles/sparkline.css +14 -12
- package/src/styles/table-animation.css +82 -80
- package/src/styles/table.css +314 -302
- package/src/styles/token-definitions.ts +199 -0
- package/src/styles/tokens.css +132 -128
- package/src/styles/tooltip.css +109 -115
- package/src/styles/you-draw-it.css +126 -0
- package/src/types/__tests__/encoding.test.ts +86 -17
- package/src/types/__tests__/spec.test.ts +16 -12
- package/src/types/encoding.ts +69 -25
- package/src/types/events.ts +12 -0
- package/src/types/index.ts +39 -3
- package/src/types/layout.ts +414 -8
- package/src/types/schema-roots.ts +32 -0
- package/src/types/spec.ts +643 -84
- package/src/types/table.ts +3 -2
package/src/styles/tooltip.css
CHANGED
|
@@ -1,132 +1,126 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
@layer oc.components {
|
|
2
|
+
/* ---------------------------------------------------------------------------
|
|
3
|
+
* Tooltip
|
|
4
|
+
* Editorial card matching the new design system: zinc surface, cyan accent,
|
|
5
|
+
* Inter Variable, mono numerics. Sized for a slice tooltip that lists every
|
|
6
|
+
* series at the snapped x.
|
|
7
|
+
* --------------------------------------------------------------------------- */
|
|
7
8
|
|
|
8
|
-
.oc-tooltip {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
.oc-tooltip {
|
|
10
|
+
position: absolute;
|
|
11
|
+
display: none;
|
|
12
|
+
pointer-events: none;
|
|
13
|
+
z-index: 1000;
|
|
14
|
+
background: var(--oc-tooltip-bg);
|
|
15
|
+
backdrop-filter: blur(14px);
|
|
16
|
+
border: 1px solid var(--oc-tooltip-border);
|
|
17
|
+
border-radius: var(--oc-border-radius, 2px);
|
|
18
|
+
box-shadow: var(--oc-tooltip-shadow);
|
|
19
|
+
color: var(--oc-tooltip-text);
|
|
20
|
+
font-family: var(--oc-font-family);
|
|
21
|
+
font-size: 12px;
|
|
22
|
+
padding: 0;
|
|
23
|
+
max-width: 280px;
|
|
24
|
+
min-width: 160px;
|
|
25
|
+
line-height: 1.4;
|
|
26
|
+
animation: oc-tooltip-in 120ms ease-out;
|
|
27
|
+
transition:
|
|
28
|
+
left 100ms ease-in-out,
|
|
29
|
+
top 100ms ease-in-out;
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
& .oc-tooltip-header {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: 8px;
|
|
35
|
+
padding: 8px 12px 6px;
|
|
36
|
+
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
& .oc-tooltip-dot {
|
|
39
|
+
width: 8px;
|
|
40
|
+
height: 8px;
|
|
41
|
+
border-radius: 50%;
|
|
42
|
+
flex-shrink: 0;
|
|
43
|
+
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
& .oc-tooltip-title {
|
|
46
|
+
font-weight: 590;
|
|
47
|
+
font-size: 11px;
|
|
48
|
+
letter-spacing: 0.04em;
|
|
49
|
+
text-transform: uppercase;
|
|
50
|
+
color: var(--oc-text-muted);
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
& .oc-tooltip-body {
|
|
57
|
+
padding: 6px 12px 10px;
|
|
58
|
+
border-top: 1px solid var(--oc-tooltip-border);
|
|
59
|
+
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
/* No separator when body is the only child */
|
|
62
|
+
& .oc-tooltip-body:first-child {
|
|
63
|
+
border-top: none;
|
|
64
|
+
padding-top: 10px;
|
|
65
|
+
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
& .oc-tooltip-row {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: baseline;
|
|
70
|
+
justify-content: space-between;
|
|
71
|
+
gap: 16px;
|
|
72
|
+
padding: 2px 0;
|
|
73
|
+
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
& .oc-tooltip-row-swatch {
|
|
76
|
+
display: inline-block;
|
|
77
|
+
width: 8px;
|
|
78
|
+
height: 8px;
|
|
79
|
+
border-radius: 50%;
|
|
80
|
+
margin-right: 6px;
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
transform: translateY(-1px);
|
|
83
|
+
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
& .oc-tooltip-label {
|
|
86
|
+
color: var(--oc-text-secondary);
|
|
87
|
+
font-size: 12px;
|
|
88
|
+
font-weight: 400;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
& .oc-tooltip-value {
|
|
96
|
+
font-weight: 510;
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
font-variant-numeric: tabular-nums;
|
|
99
|
+
color: var(--oc-tooltip-text);
|
|
100
|
+
text-align: right;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
text-overflow: ellipsis;
|
|
103
|
+
white-space: nowrap;
|
|
104
|
+
}
|
|
103
105
|
}
|
|
104
|
-
}
|
|
105
106
|
|
|
106
|
-
/* ---------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
/* ---------------------------------------------------------------------------
|
|
108
|
+
* Crosshair guideline + per-series snap dots (line/area charts)
|
|
109
|
+
* Animate the snap between adjacent points so the indicator slides instead
|
|
110
|
+
* of jumping. ~50ms ease-in-out keeps the motion crisp without lagging.
|
|
111
|
+
* --------------------------------------------------------------------------- */
|
|
111
112
|
|
|
112
|
-
.oc-crosshair {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.oc-snap-dots circle {
|
|
120
|
-
pointer-events: none;
|
|
121
|
-
transition:
|
|
122
|
-
cx 50ms ease-in-out,
|
|
123
|
-
cy 50ms ease-in-out;
|
|
124
|
-
}
|
|
113
|
+
.oc-crosshair {
|
|
114
|
+
pointer-events: none;
|
|
115
|
+
transition:
|
|
116
|
+
x1 50ms ease-in-out,
|
|
117
|
+
x2 50ms ease-in-out;
|
|
118
|
+
}
|
|
125
119
|
|
|
126
|
-
@media (prefers-reduced-motion: reduce) {
|
|
127
|
-
.oc-tooltip,
|
|
128
|
-
.oc-crosshair,
|
|
129
120
|
.oc-snap-dots circle {
|
|
130
|
-
|
|
121
|
+
pointer-events: none;
|
|
122
|
+
transition:
|
|
123
|
+
cx 50ms ease-in-out,
|
|
124
|
+
cy 50ms ease-in-out;
|
|
131
125
|
}
|
|
132
126
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
@layer oc.components {
|
|
2
|
+
/* ---------------------------------------------------------------------------
|
|
3
|
+
* You draw it (youDrawIt)
|
|
4
|
+
*
|
|
5
|
+
* NYT-style draw-then-reveal. SVG-space elements (hatched region, guess path,
|
|
6
|
+
* reveal clip) render inside the chart SVG; the prompt + skip-to-reveal button
|
|
7
|
+
* are an absolutely-positioned HTML overlay the vanilla adapter sizes over the
|
|
8
|
+
* drawing region (inline left/top/width/height). All colors come from --oc-*
|
|
9
|
+
* tokens so they flip in .oc-dark.
|
|
10
|
+
* --------------------------------------------------------------------------- */
|
|
11
|
+
|
|
12
|
+
/* Hatched "draw here" region. Fill comes from an SVG <pattern> (line color =
|
|
13
|
+
* the target series color); this only governs overall opacity + fade-out. */
|
|
14
|
+
.oc-ydi-region {
|
|
15
|
+
opacity: 0.35;
|
|
16
|
+
transition: opacity 0.4s ease;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.oc-ydi-region[data-ydi-hidden="true"] {
|
|
20
|
+
opacity: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* The vertical rule at `from` where drawing begins. */
|
|
24
|
+
.oc-ydi-boundary {
|
|
25
|
+
stroke: var(--oc-text-muted);
|
|
26
|
+
stroke-width: 1;
|
|
27
|
+
stroke-dasharray: 3 3;
|
|
28
|
+
stroke-opacity: 0.6;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Reader's guess: a distinct pen-like stroke that reads as "yours" against the
|
|
32
|
+
* solid real line. Dashed + focus-blue so it stays legible in both modes. */
|
|
33
|
+
.oc-ydi-guess {
|
|
34
|
+
stroke: var(--oc-focus);
|
|
35
|
+
stroke-width: 2.5;
|
|
36
|
+
stroke-linecap: round;
|
|
37
|
+
stroke-linejoin: round;
|
|
38
|
+
stroke-dasharray: 1 6;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Host-supplied comparison line ("what everyone else guessed"). Muted so it
|
|
42
|
+
* sits behind the reader's guess and the real line. */
|
|
43
|
+
.oc-ydi-comparison {
|
|
44
|
+
stroke: var(--oc-text-muted);
|
|
45
|
+
stroke-width: 1.5;
|
|
46
|
+
stroke-opacity: 0.5;
|
|
47
|
+
stroke-dasharray: 4 4;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Reveal clip rect: the width animates from the `from` boundary to the full
|
|
51
|
+
* area on reveal, wiping the real line in left-to-right. The vanilla adapter
|
|
52
|
+
* adds .oc-ydi-clip-animate right before setting the final width; reduced
|
|
53
|
+
* motion skips the class so the width snaps. */
|
|
54
|
+
.oc-ydi-clip-rect.oc-ydi-clip-animate {
|
|
55
|
+
transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* HTML controls overlay: prompt (top) + reveal button (bottom-right). */
|
|
59
|
+
.oc-ydi-controls {
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
align-items: flex-end;
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
font-family: var(--oc-font-family);
|
|
66
|
+
padding: 8px;
|
|
67
|
+
box-sizing: border-box;
|
|
68
|
+
z-index: 10;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.oc-ydi-prompt {
|
|
72
|
+
align-self: center;
|
|
73
|
+
max-width: 100%;
|
|
74
|
+
padding: 6px 12px;
|
|
75
|
+
border-radius: 6px;
|
|
76
|
+
background: var(--oc-card);
|
|
77
|
+
border: 1px solid var(--oc-border);
|
|
78
|
+
color: var(--oc-text-secondary);
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
font-weight: 500;
|
|
81
|
+
line-height: 16px;
|
|
82
|
+
text-align: center;
|
|
83
|
+
box-shadow: var(--oc-tooltip-shadow);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.oc-ydi-controls.oc-ydi-revealed .oc-ydi-prompt {
|
|
87
|
+
display: none;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Skip-to-reveal button. pointer-events re-enabled (the overlay disables it)
|
|
91
|
+
* so it stays clickable and keyboard-reachable. Min height enforced inline by
|
|
92
|
+
* the adapter (>= 24px effective touch target). */
|
|
93
|
+
.oc-ydi-reveal-button {
|
|
94
|
+
pointer-events: auto;
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
padding: 6px 14px;
|
|
99
|
+
border: 1px solid var(--oc-border);
|
|
100
|
+
border-radius: 6px;
|
|
101
|
+
background: var(--oc-bg);
|
|
102
|
+
color: var(--oc-text);
|
|
103
|
+
font-family: inherit;
|
|
104
|
+
font-size: 13px;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
box-shadow: var(--oc-tooltip-shadow);
|
|
108
|
+
transition:
|
|
109
|
+
background-color 0.15s,
|
|
110
|
+
border-color 0.15s;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.oc-ydi-reveal-button:hover {
|
|
114
|
+
background: var(--oc-hover-bg);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.oc-ydi-reveal-button:focus-visible {
|
|
118
|
+
outline: 2px solid var(--oc-focus);
|
|
119
|
+
outline-offset: 2px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.oc-ydi-reveal-button:disabled {
|
|
123
|
+
opacity: 0.5;
|
|
124
|
+
cursor: default;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -27,7 +27,7 @@ function getOptionalChannels(markType: MarkType): string[] {
|
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
29
|
describe('MARK_ENCODING_RULES', () => {
|
|
30
|
-
it('has entries for all
|
|
30
|
+
it('has entries for all 16 mark types', () => {
|
|
31
31
|
const expectedTypes: MarkType[] = [
|
|
32
32
|
'bar',
|
|
33
33
|
'line',
|
|
@@ -40,6 +40,11 @@ describe('MARK_ENCODING_RULES', () => {
|
|
|
40
40
|
'tick',
|
|
41
41
|
'rect',
|
|
42
42
|
'lollipop',
|
|
43
|
+
'beeswarm',
|
|
44
|
+
'range',
|
|
45
|
+
'waffle',
|
|
46
|
+
'calendar',
|
|
47
|
+
'parliament',
|
|
43
48
|
];
|
|
44
49
|
for (const type of expectedTypes) {
|
|
45
50
|
expect(MARK_ENCODING_RULES[type]).toBeDefined();
|
|
@@ -67,21 +72,21 @@ describe('bar encoding rules', () => {
|
|
|
67
72
|
expect(getOptionalChannels('bar')).toContain('y2');
|
|
68
73
|
});
|
|
69
74
|
|
|
70
|
-
it('supports opacity, tooltip,
|
|
75
|
+
it('supports opacity, tooltip, detail', () => {
|
|
71
76
|
const optionals = getOptionalChannels('bar');
|
|
72
77
|
expect(optionals).toContain('opacity');
|
|
73
78
|
expect(optionals).toContain('tooltip');
|
|
74
|
-
expect(optionals).toContain('href');
|
|
75
|
-
expect(optionals).toContain('order');
|
|
76
79
|
expect(optionals).toContain('detail');
|
|
77
80
|
});
|
|
78
81
|
|
|
79
|
-
it('does not support shape, strokeDash, text, theta, radius', () => {
|
|
82
|
+
it('does not support shape, strokeDash, text, theta, href, order, radius', () => {
|
|
80
83
|
const channels = getChannelNames('bar');
|
|
81
84
|
expect(channels).not.toContain('shape');
|
|
82
85
|
expect(channels).not.toContain('strokeDash');
|
|
83
86
|
expect(channels).not.toContain('text');
|
|
84
87
|
expect(channels).not.toContain('theta');
|
|
88
|
+
expect(channels).not.toContain('href');
|
|
89
|
+
expect(channels).not.toContain('order');
|
|
85
90
|
expect(channels).not.toContain('radius');
|
|
86
91
|
});
|
|
87
92
|
});
|
|
@@ -108,6 +113,12 @@ describe('line encoding rules', () => {
|
|
|
108
113
|
expect(channels).not.toContain('x2');
|
|
109
114
|
expect(channels).not.toContain('y2');
|
|
110
115
|
});
|
|
116
|
+
|
|
117
|
+
it('does not support href or order (removed dead channels)', () => {
|
|
118
|
+
const channels = getChannelNames('line');
|
|
119
|
+
expect(channels).not.toContain('href');
|
|
120
|
+
expect(channels).not.toContain('order');
|
|
121
|
+
});
|
|
111
122
|
});
|
|
112
123
|
|
|
113
124
|
describe('point encoding rules', () => {
|
|
@@ -122,8 +133,8 @@ describe('point encoding rules', () => {
|
|
|
122
133
|
}
|
|
123
134
|
});
|
|
124
135
|
|
|
125
|
-
it('
|
|
126
|
-
expect(
|
|
136
|
+
it('does not support shape (removed dead channel)', () => {
|
|
137
|
+
expect(getChannelNames('point')).not.toContain('shape');
|
|
127
138
|
});
|
|
128
139
|
|
|
129
140
|
it('supports size as optional quantitative', () => {
|
|
@@ -134,18 +145,18 @@ describe('point encoding rules', () => {
|
|
|
134
145
|
});
|
|
135
146
|
|
|
136
147
|
describe('arc encoding rules', () => {
|
|
137
|
-
it('requires
|
|
148
|
+
it('requires theta (quantitative) and color (nominal/ordinal)', () => {
|
|
138
149
|
const rules = MARK_ENCODING_RULES.arc;
|
|
139
|
-
expect(rules.
|
|
140
|
-
expect(rules.
|
|
150
|
+
expect(rules.theta.required).toBe(true);
|
|
151
|
+
expect(rules.theta.allowedTypes).toContain('quantitative');
|
|
141
152
|
expect(rules.color.required).toBe(true);
|
|
142
153
|
expect(rules.color.allowedTypes).toContain('nominal');
|
|
143
154
|
});
|
|
144
155
|
|
|
145
|
-
it('supports
|
|
156
|
+
it('supports y as optional deprecated alias (radius was removed)', () => {
|
|
146
157
|
const optionals = getOptionalChannels('arc');
|
|
147
|
-
expect(optionals).toContain('
|
|
148
|
-
expect(
|
|
158
|
+
expect(optionals).toContain('y');
|
|
159
|
+
expect(getChannelNames('arc')).not.toContain('radius');
|
|
149
160
|
});
|
|
150
161
|
});
|
|
151
162
|
|
|
@@ -188,11 +199,10 @@ describe('tick encoding rules', () => {
|
|
|
188
199
|
expect(getRequiredChannels('tick')).toContain('y');
|
|
189
200
|
});
|
|
190
201
|
|
|
191
|
-
it('supports opacity, tooltip,
|
|
202
|
+
it('supports opacity, tooltip, detail', () => {
|
|
192
203
|
const optionals = getOptionalChannels('tick');
|
|
193
204
|
expect(optionals).toContain('opacity');
|
|
194
205
|
expect(optionals).toContain('tooltip');
|
|
195
|
-
expect(optionals).toContain('href');
|
|
196
206
|
expect(optionals).toContain('detail');
|
|
197
207
|
});
|
|
198
208
|
});
|
|
@@ -203,12 +213,11 @@ describe('rect encoding rules', () => {
|
|
|
203
213
|
expect(getRequiredChannels('rect')).toContain('y');
|
|
204
214
|
});
|
|
205
215
|
|
|
206
|
-
it('supports x2, y2, opacity
|
|
216
|
+
it('supports x2, y2, opacity', () => {
|
|
207
217
|
const optionals = getOptionalChannels('rect');
|
|
208
218
|
expect(optionals).toContain('x2');
|
|
209
219
|
expect(optionals).toContain('y2');
|
|
210
220
|
expect(optionals).toContain('opacity');
|
|
211
|
-
expect(optionals).toContain('order');
|
|
212
221
|
});
|
|
213
222
|
});
|
|
214
223
|
|
|
@@ -229,6 +238,51 @@ describe('lollipop encoding rules', () => {
|
|
|
229
238
|
});
|
|
230
239
|
});
|
|
231
240
|
|
|
241
|
+
describe('beeswarm encoding rules', () => {
|
|
242
|
+
it('marks both axes optional (the compiler enforces the combination)', () => {
|
|
243
|
+
const rules = MARK_ENCODING_RULES.beeswarm;
|
|
244
|
+
expect(rules.x.required).toBe(false);
|
|
245
|
+
expect(rules.y.required).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('accepts quantitative and nominal/ordinal on both axes', () => {
|
|
249
|
+
const rules = MARK_ENCODING_RULES.beeswarm;
|
|
250
|
+
for (const axis of [rules.x, rules.y]) {
|
|
251
|
+
expect(axis.allowedTypes).toContain('quantitative');
|
|
252
|
+
expect(axis.allowedTypes).toContain('nominal');
|
|
253
|
+
expect(axis.allowedTypes).toContain('ordinal');
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('supports optional size (quantitative) for sized dots', () => {
|
|
258
|
+
const rules = MARK_ENCODING_RULES.beeswarm;
|
|
259
|
+
expect(rules.size.required).toBe(false);
|
|
260
|
+
expect(rules.size.allowedTypes).toEqual(['quantitative']);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('waffle encoding rules', () => {
|
|
265
|
+
it('requires a quantitative y (the share value) and a categorical color', () => {
|
|
266
|
+
const rules = MARK_ENCODING_RULES.waffle;
|
|
267
|
+
expect(rules.y.required).toBe(true);
|
|
268
|
+
expect(rules.y.allowedTypes).toEqual(['quantitative']);
|
|
269
|
+
expect(rules.color.required).toBe(true);
|
|
270
|
+
expect(rules.color.allowedTypes).toContain('nominal');
|
|
271
|
+
expect(rules.color.allowedTypes).toContain('ordinal');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('accepts theta as an optional quantitative channel (arc parity)', () => {
|
|
275
|
+
const rules = MARK_ENCODING_RULES.waffle;
|
|
276
|
+
expect(rules.theta?.required).toBe(false);
|
|
277
|
+
expect(rules.theta?.allowedTypes).toEqual(['quantitative']);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('does not require positional x (no axes)', () => {
|
|
281
|
+
const rules = MARK_ENCODING_RULES.waffle;
|
|
282
|
+
expect(rules.x.required).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
232
286
|
describe('common channels across marks', () => {
|
|
233
287
|
it('tooltip is optional on all mark types', () => {
|
|
234
288
|
const allTypes: MarkType[] = [
|
|
@@ -243,6 +297,11 @@ describe('common channels across marks', () => {
|
|
|
243
297
|
'tick',
|
|
244
298
|
'rect',
|
|
245
299
|
'lollipop',
|
|
300
|
+
'beeswarm',
|
|
301
|
+
'range',
|
|
302
|
+
'waffle',
|
|
303
|
+
'calendar',
|
|
304
|
+
'parliament',
|
|
246
305
|
];
|
|
247
306
|
for (const type of allTypes) {
|
|
248
307
|
const rules = MARK_ENCODING_RULES[type];
|
|
@@ -265,6 +324,11 @@ describe('common channels across marks', () => {
|
|
|
265
324
|
'tick',
|
|
266
325
|
'rect',
|
|
267
326
|
'lollipop',
|
|
327
|
+
'beeswarm',
|
|
328
|
+
'range',
|
|
329
|
+
'waffle',
|
|
330
|
+
'calendar',
|
|
331
|
+
'parliament',
|
|
268
332
|
];
|
|
269
333
|
for (const type of allTypes) {
|
|
270
334
|
const rules = MARK_ENCODING_RULES[type];
|
|
@@ -287,6 +351,11 @@ describe('common channels across marks', () => {
|
|
|
287
351
|
'tick',
|
|
288
352
|
'rect',
|
|
289
353
|
'lollipop',
|
|
354
|
+
'beeswarm',
|
|
355
|
+
'range',
|
|
356
|
+
'waffle',
|
|
357
|
+
'calendar',
|
|
358
|
+
'parliament',
|
|
290
359
|
];
|
|
291
360
|
for (const type of allTypes) {
|
|
292
361
|
const rules = MARK_ENCODING_RULES[type];
|
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
VizSpec,
|
|
9
9
|
} from '../spec';
|
|
10
10
|
import {
|
|
11
|
-
CHART_TYPES,
|
|
12
11
|
isChartSpec,
|
|
13
12
|
isGraphSpec,
|
|
14
13
|
isRangeAnnotation,
|
|
@@ -139,8 +138,8 @@ describe('type guard mutual exclusivity', () => {
|
|
|
139
138
|
// ---------------------------------------------------------------------------
|
|
140
139
|
|
|
141
140
|
describe('MARK_TYPES', () => {
|
|
142
|
-
it('contains all
|
|
143
|
-
expect(MARK_TYPES.size).toBe(
|
|
141
|
+
it('contains all 16 mark types', () => {
|
|
142
|
+
expect(MARK_TYPES.size).toBe(16);
|
|
144
143
|
});
|
|
145
144
|
|
|
146
145
|
it('contains expected types', () => {
|
|
@@ -156,6 +155,11 @@ describe('MARK_TYPES', () => {
|
|
|
156
155
|
'tick',
|
|
157
156
|
'rect',
|
|
158
157
|
'lollipop',
|
|
158
|
+
'beeswarm',
|
|
159
|
+
'range',
|
|
160
|
+
'waffle',
|
|
161
|
+
'calendar',
|
|
162
|
+
'parliament',
|
|
159
163
|
];
|
|
160
164
|
for (const t of expected) {
|
|
161
165
|
expect(MARK_TYPES.has(t)).toBe(true);
|
|
@@ -167,10 +171,6 @@ describe('MARK_TYPES', () => {
|
|
|
167
171
|
expect(MARK_TYPES.has('graph')).toBe(false);
|
|
168
172
|
expect(MARK_TYPES.has('map')).toBe(false);
|
|
169
173
|
});
|
|
170
|
-
|
|
171
|
-
it('CHART_TYPES is an alias for MARK_TYPES', () => {
|
|
172
|
-
expect(CHART_TYPES).toBe(MARK_TYPES);
|
|
173
|
-
});
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
// ---------------------------------------------------------------------------
|
|
@@ -424,13 +424,13 @@ describe('type-level spec construction', () => {
|
|
|
424
424
|
|
|
425
425
|
describe('type-level rejection', () => {
|
|
426
426
|
it('compiles with @ts-expect-error annotations intact (runtime no-op)', () => {
|
|
427
|
-
// Arc requires
|
|
427
|
+
// Arc requires theta + color. Missing color must error.
|
|
428
428
|
const _arcMissingColor: ChartSpec = {
|
|
429
429
|
mark: 'arc',
|
|
430
430
|
data: [],
|
|
431
431
|
// @ts-expect-error ArcEncoding requires color channel
|
|
432
432
|
encoding: {
|
|
433
|
-
|
|
433
|
+
theta: { field: 'value', type: 'quantitative' },
|
|
434
434
|
},
|
|
435
435
|
};
|
|
436
436
|
|
|
@@ -464,10 +464,14 @@ describe('type-level rejection', () => {
|
|
|
464
464
|
},
|
|
465
465
|
};
|
|
466
466
|
|
|
467
|
-
// Arc
|
|
468
|
-
|
|
467
|
+
// Arc with only the deprecated y alias (no theta) must error: ArcEncoding
|
|
468
|
+
// requires theta, the canonical value channel in v8. y is still accepted
|
|
469
|
+
// at runtime (spec-sugar expands it into theta with a warning), but the
|
|
470
|
+
// type no longer allows authoring it directly.
|
|
471
|
+
const _arcDeprecatedYOnly: ChartSpec = {
|
|
469
472
|
mark: 'arc',
|
|
470
473
|
data: [],
|
|
474
|
+
// @ts-expect-error ArcEncoding requires theta; y alone is not enough
|
|
471
475
|
encoding: {
|
|
472
476
|
y: { field: 'value', type: 'quantitative' },
|
|
473
477
|
color: { field: 'category', type: 'nominal' },
|
|
@@ -488,7 +492,7 @@ describe('type-level rejection', () => {
|
|
|
488
492
|
void _textMissingText;
|
|
489
493
|
void _typoField;
|
|
490
494
|
void _validTyped;
|
|
491
|
-
void
|
|
495
|
+
void _arcDeprecatedYOnly;
|
|
492
496
|
void _untypedSpec;
|
|
493
497
|
expect(true).toBe(true);
|
|
494
498
|
});
|