@jbrowse/cli 5.0.0-beta.13 → 5.0.0-beta.14
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/README.md +19 -38
- package/dist/commands/add-track-utils/adapter-utils.js +0 -45
- package/dist/commands/add-track.js +5 -28
- package/dist/commands/make-density/density-generator.js +1 -1
- package/dist/commands/make-density/index.js +14 -26
- package/dist/commands/make-pif/index.js +1 -1
- package/dist/commands/text-index/file-list.js +1 -1
- package/dist/commands/validate/configManifest.generated.js +1027 -273
- package/dist/commands/validate/configSchema.generated.js +3227 -1445
- package/dist/commands/validate/index.js +4 -3
- package/dist/commands/validate/markRules/colorScale.js +124 -26
- package/dist/commands/validate/markRules/markProblems.js +49 -99
- package/dist/commands/validate/markRules/markRuleFacts.js +1 -1
- package/dist/commands/validate/markRules/markSpecs.js +4 -2
- package/dist/commands/validate/markRules/markVocabulary.js +21 -24
- package/dist/commands/validate/markRules/stepChannels.js +13 -47
- package/dist/commands/validate/markRules/stepOutputs.js +202 -0
- package/dist/commands/validate/schemaValidate.js +5 -2
- package/dist/commands/validate/validateConfig.js +47 -13
- package/dist/index.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/dist/commands/shared/density.js +0 -12
- package/dist/commands/validate/markRules/aggregateFieldName.js +0 -9
|
@@ -2,9 +2,10 @@ import { parseArgs } from 'node:util';
|
|
|
2
2
|
import { printHelp, readConfigFile } from '../../utils.js';
|
|
3
3
|
import { validateConfig } from './validateConfig.js';
|
|
4
4
|
const description = 'Check a JBrowse configuration for errors, including the ones JBrowse itself accepts silently';
|
|
5
|
-
const notes = `A config key JBrowse does not recognize
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const notes = `A config key JBrowse does not recognize does nothing: a track, an adapter or
|
|
6
|
+
a display names it on the browser console and loads without it, and a session
|
|
7
|
+
snapshot drops it in silence. This command reports each one offline, before
|
|
8
|
+
anyone opens the config.
|
|
8
9
|
|
|
9
10
|
Two levels are reported:
|
|
10
11
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Generated by scripts/generateMarkRules.ts from packages/core/src/util/colorScale.ts: do not edit, `pnpm autogen` rewrites it.
|
|
2
2
|
/**
|
|
3
|
-
* The scales a
|
|
3
|
+
* The scales a color object paints through, in a module that imports
|
|
4
4
|
* nothing: `jbrowse validate` carries a copy of it, so the validator and the
|
|
5
5
|
* displays read one statement.
|
|
6
6
|
*/
|
|
7
|
-
/** How a
|
|
7
|
+
/** How a color object maps its `field`; `none` paints `value`. */
|
|
8
8
|
export const COLOR_SCALES = [
|
|
9
9
|
'none',
|
|
10
10
|
'categorical',
|
|
@@ -13,20 +13,23 @@ export const COLOR_SCALES = [
|
|
|
13
13
|
'threshold',
|
|
14
14
|
];
|
|
15
15
|
/**
|
|
16
|
-
* The scale that maps no field: each feature paints the
|
|
17
|
-
* the key names the
|
|
18
|
-
* carry
|
|
16
|
+
* The scale that maps no field: each feature paints the color it carries, and
|
|
17
|
+
* the key names the colors `domain` lists. Only a display whose features
|
|
18
|
+
* carry colors of their own declares it.
|
|
19
19
|
*/
|
|
20
20
|
export const IDENTITY_SCALE = 'identity';
|
|
21
21
|
const CATEGORICAL_PRESET = { scale: 'categorical' };
|
|
22
22
|
/**
|
|
23
|
-
* The fields whose values have names, an order and
|
|
23
|
+
* The fields whose values have names, an order and colors of their own on
|
|
24
24
|
* every display, beneath each display's own presets. The synteny ribbons
|
|
25
25
|
* paint strand from this range too. The SV classes are
|
|
26
26
|
* `svClassOf`'s, deletion red and duplication blue as dbVar and gnomAD-SV
|
|
27
27
|
* paint them, insertion the pileup's purple, and the rest kept apart under
|
|
28
28
|
* deuteranopia and protanopia. A record with no class files under `''`, so
|
|
29
|
-
* each display keeps its own no-value
|
|
29
|
+
* each display keeps its own no-value color for it. The impact tiers are a
|
|
30
|
+
* variant's most severe SnpEff (ANN) or VEP (CSQ) consequence, and a record
|
|
31
|
+
* with neither files under Unannotated, its own color: "nobody looked" is
|
|
32
|
+
* not MODIFIER's "we looked and it is harmless".
|
|
30
33
|
*/
|
|
31
34
|
export const UNIVERSAL_FIELD_PRESETS = {
|
|
32
35
|
strand: {
|
|
@@ -63,6 +66,13 @@ export const UNIVERSAL_FIELD_PRESETS = {
|
|
|
63
66
|
],
|
|
64
67
|
title: 'SV type',
|
|
65
68
|
},
|
|
69
|
+
impact: {
|
|
70
|
+
scale: 'categorical',
|
|
71
|
+
missing: 'Unannotated',
|
|
72
|
+
domain: ['HIGH', 'MODERATE', 'LOW', 'MODIFIER', 'Unannotated'],
|
|
73
|
+
range: ['#d32f2f', '#f57c00', '#fbc02d', '#9e9e9e', '#607d8b'],
|
|
74
|
+
title: 'Consequence impact',
|
|
75
|
+
},
|
|
66
76
|
};
|
|
67
77
|
/** The universal preset of `field`, if it has one. */
|
|
68
78
|
export function universalPresetOf(field) {
|
|
@@ -82,6 +92,63 @@ export const FEATURE_FIELD_PRESETS = {
|
|
|
82
92
|
score: { scale: 'linear' },
|
|
83
93
|
'*': CATEGORICAL_PRESET,
|
|
84
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
* Mapping quality's bins, each keyed by its lowest score and confident reads
|
|
97
|
+
* first: real MAPQ is bimodal at the aligner's ceiling and at 0, and these
|
|
98
|
+
* are the cuts people filter on (`samtools view -q 10` / `-q 30`).
|
|
99
|
+
*/
|
|
100
|
+
export const MAPQ_BINS = [
|
|
101
|
+
{ key: '30', min: 30, label: 'MAPQ 30+ (high confidence)' },
|
|
102
|
+
{ key: '10', min: 10, label: 'MAPQ 10-29' },
|
|
103
|
+
{ key: '1', min: 1, label: 'MAPQ 1-9 (low)' },
|
|
104
|
+
{ key: '0', min: 0, label: 'MAPQ 0 (multi-mapping)' },
|
|
105
|
+
];
|
|
106
|
+
const MAPQ_BINS_ASCENDING = [...MAPQ_BINS].reverse();
|
|
107
|
+
/**
|
|
108
|
+
* The measurements whose scale is part of what they mean, for a display
|
|
109
|
+
* whose features carry them to spread into its own presets: `mapq` in the
|
|
110
|
+
* bins above; `identity` a fraction from 0 to 1, red where it diverges and
|
|
111
|
+
* blue where it is conserved, as conservation tracks paint it, through a grey
|
|
112
|
+
* middle that stays visible on a white page and under a faint ribbon, where
|
|
113
|
+
* viridis's bright top washed out; and `dnds` read against 1, its ramp's pale
|
|
114
|
+
* middle, up to 2, past which a few fast-evolving genes would flatten the
|
|
115
|
+
* rest into one blue; and `syntenicDepth` grey where a gene pair is
|
|
116
|
+
* one-to-one, so genes sharing one ortholog are what takes color. Opt-in rather
|
|
117
|
+
* than universal, since a column another file names `identity` need not be a
|
|
118
|
+
* fraction.
|
|
119
|
+
*/
|
|
120
|
+
export const MEASURE_FIELD_PRESETS = {
|
|
121
|
+
mapq: {
|
|
122
|
+
scale: 'threshold',
|
|
123
|
+
domain: MAPQ_BINS_ASCENDING.slice(1).map(bin => bin.key),
|
|
124
|
+
scheme: 'cividis',
|
|
125
|
+
labels: MAPQ_BINS_ASCENDING.map(bin => bin.label),
|
|
126
|
+
title: 'Mapping quality',
|
|
127
|
+
descending: true,
|
|
128
|
+
},
|
|
129
|
+
identity: {
|
|
130
|
+
scale: 'linear',
|
|
131
|
+
domainMin: 0,
|
|
132
|
+
domainMax: 1,
|
|
133
|
+
scheme: 'redgreyblue',
|
|
134
|
+
title: 'Identity',
|
|
135
|
+
},
|
|
136
|
+
dnds: {
|
|
137
|
+
scale: 'linear',
|
|
138
|
+
domainMin: 0,
|
|
139
|
+
domainMax: 2,
|
|
140
|
+
domainMid: 1,
|
|
141
|
+
scheme: 'blueyellowred',
|
|
142
|
+
title: 'dN/dS',
|
|
143
|
+
},
|
|
144
|
+
syntenicDepth: {
|
|
145
|
+
scale: 'threshold',
|
|
146
|
+
domain: ['2', '3'],
|
|
147
|
+
range: ['#bdbdbd', '#fdae6b', '#e6550d'],
|
|
148
|
+
labels: ['One-to-one', 'Shared by 2 genes', 'Shared by 3 or more'],
|
|
149
|
+
title: 'Syntenic depth',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
85
152
|
/**
|
|
86
153
|
* A field's preset under `presets`: its own, else a universal one, else
|
|
87
154
|
* `*`'s, else categorical.
|
|
@@ -99,13 +166,14 @@ function unwritten(value) {
|
|
|
99
166
|
return value === undefined || (Array.isArray(value) && value.length === 0);
|
|
100
167
|
}
|
|
101
168
|
/**
|
|
102
|
-
* A
|
|
169
|
+
* A color object with its field's preset under it: each member the preset
|
|
103
170
|
* names and the config leaves unwritten, an empty list counting as unwritten.
|
|
104
|
-
* Only while the
|
|
171
|
+
* Only while the color paints through the preset's own scale, so the cuts of
|
|
105
172
|
* a threshold preset never reach a field read through a ramp. A categorical
|
|
106
|
-
* preset's `range` and `labels` name its `domain`'s values in order,
|
|
107
|
-
*
|
|
108
|
-
*
|
|
173
|
+
* preset's `range` and `labels` name its `domain`'s values in order, and a
|
|
174
|
+
* threshold preset's `labels` its intervals, so they fill only while `domain`
|
|
175
|
+
* does too: a written order keeps each value's own color and name, and written
|
|
176
|
+
* cuts keep the preset's palette and lose its interval names.
|
|
109
177
|
*/
|
|
110
178
|
export function withPreset(color, presets) {
|
|
111
179
|
const field = color.field ?? '';
|
|
@@ -114,17 +182,27 @@ export function withPreset(color, presets) {
|
|
|
114
182
|
return color;
|
|
115
183
|
}
|
|
116
184
|
const written = new Map(Object.entries(color));
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
unwritten(written.get('domain'));
|
|
185
|
+
const ownDomain = !preset.domain || unwritten(written.get('domain'));
|
|
186
|
+
const named = preset.scale === 'categorical' ? ['range', 'labels'] : ['labels'];
|
|
120
187
|
const filled = Object.entries(preset).filter(([key]) => key !== 'scale' &&
|
|
121
188
|
key !== 'missing' &&
|
|
122
|
-
(
|
|
189
|
+
(ownDomain || !named.includes(key)) &&
|
|
123
190
|
unwritten(written.get(key)));
|
|
124
191
|
return filled.length > 0 ? { ...color, ...Object.fromEntries(filled) } : color;
|
|
125
192
|
}
|
|
126
193
|
/**
|
|
127
|
-
*
|
|
194
|
+
* A color object with `implied` as its field while it names neither a field
|
|
195
|
+
* nor a constant, for a display whose bare color object still maps one (a
|
|
196
|
+
* quantitative display's `score`), so the display and `jbrowse validate`
|
|
197
|
+
* read it alike. A written `value` keeps the object a constant.
|
|
198
|
+
*/
|
|
199
|
+
export function withImpliedField(color, implied) {
|
|
200
|
+
return implied && !color.field && color.value === undefined
|
|
201
|
+
? { ...color, field: implied }
|
|
202
|
+
: color;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The scale a color object paints through: `none` while no `field` is
|
|
128
206
|
* named, else its own `scale`, or `fieldScale` where that is unset.
|
|
129
207
|
*/
|
|
130
208
|
export function paintedScale({ scale, field }, fieldScale) {
|
|
@@ -134,11 +212,22 @@ export function paintedScale({ scale, field }, fieldScale) {
|
|
|
134
212
|
* What a quantitative scale's ends say together that neither can refuse
|
|
135
213
|
* alone: a `domainMax` below its `domainMin`, which every such scale draws in
|
|
136
214
|
* order either way, and a `domainQuantile` outside 0.5 to 1, where the quantile
|
|
137
|
-
* an open end follows is clamped
|
|
138
|
-
*
|
|
215
|
+
* an open end follows is clamped; and a log scale's `domainMin` at or below 0,
|
|
216
|
+
* which no log scale can hold, so the end floors above it. `reverse` is whether
|
|
217
|
+
* the scale turns round by a member of its own, which is then the spelling to
|
|
218
|
+
* point at.
|
|
139
219
|
*/
|
|
140
|
-
export function scaleEndProblems({ domainMin, domainMax, domainQuantile }, { reverse = false } = {}) {
|
|
220
|
+
export function scaleEndProblems({ domainMin, domainMax, domainQuantile, type }, { reverse = false } = {}) {
|
|
141
221
|
return [
|
|
222
|
+
...(type === 'log' && domainMin !== undefined && domainMin <= 0
|
|
223
|
+
? [
|
|
224
|
+
{
|
|
225
|
+
rule: 'log-floor',
|
|
226
|
+
slot: 'domainMin',
|
|
227
|
+
message: `a log scale has no ${domainMin}: the end floors to a positive value, so leave domainMin unset to follow the data or pin it above 0`,
|
|
228
|
+
},
|
|
229
|
+
]
|
|
230
|
+
: []),
|
|
142
231
|
...(domainMin !== undefined &&
|
|
143
232
|
domainMax !== undefined &&
|
|
144
233
|
domainMin > domainMax
|
|
@@ -181,6 +270,7 @@ export function scaleEndsOf(member) {
|
|
|
181
270
|
domainMin: numberOf(member('domainMin')),
|
|
182
271
|
domainMax: numberOf(member('domainMax')),
|
|
183
272
|
domainQuantile: numberOf(member('domainQuantile')),
|
|
273
|
+
type: stringOf(member('type')),
|
|
184
274
|
};
|
|
185
275
|
}
|
|
186
276
|
/**
|
|
@@ -190,6 +280,7 @@ export function scaleEndsOf(member) {
|
|
|
190
280
|
export function colorSlotsOf(member) {
|
|
191
281
|
return {
|
|
192
282
|
...scaleEndsOf(member),
|
|
283
|
+
value: stringOf(member('value')),
|
|
193
284
|
field: stringOf(member('field')) ?? '',
|
|
194
285
|
scale: stringOf(member('scale')),
|
|
195
286
|
domain: listOf(member('domain')),
|
|
@@ -201,7 +292,7 @@ function pinned(entry) {
|
|
|
201
292
|
return entry !== '' && Number.isFinite(Number(entry));
|
|
202
293
|
}
|
|
203
294
|
/**
|
|
204
|
-
* What a
|
|
295
|
+
* What a color object's slots say together that no single slot can refuse,
|
|
205
296
|
* since the config editor writes one slot at a time (ADR-133), read with its
|
|
206
297
|
* field's preset under it. A display shows these as a notice and draws what
|
|
207
298
|
* it can. A threshold naming no cuts leaves its intervals to the display —
|
|
@@ -230,7 +321,7 @@ export function colorProblems(written, presets) {
|
|
|
230
321
|
problems.push({
|
|
231
322
|
rule: 'threshold-range',
|
|
232
323
|
slot: 'range',
|
|
233
|
-
message: `${domain.length} threshold ${domain.length === 1 ? 'cut makes' : 'cuts make'} ${domain.length + 1} intervals, one
|
|
324
|
+
message: `${domain.length} threshold ${domain.length === 1 ? 'cut makes' : 'cuts make'} ${domain.length + 1} intervals, one color each, and range lists ${range.length}: a missing color comes from the default palette and an extra one is never read`,
|
|
234
325
|
});
|
|
235
326
|
}
|
|
236
327
|
}
|
|
@@ -239,10 +330,17 @@ export function colorProblems(written, presets) {
|
|
|
239
330
|
problems.push({
|
|
240
331
|
rule: 'ramp-domain',
|
|
241
332
|
slot: 'domain',
|
|
242
|
-
message: "a linear or log scale reads no domain, which is a categorical scale's order and a threshold scale's cuts; a ramp's ends are domainMin and domainMax where the
|
|
333
|
+
message: "a linear or log scale reads no domain, which is a categorical scale's order and a threshold scale's cuts; a ramp's ends are domainMin and domainMax where the color has them",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
if (range.length === 1) {
|
|
337
|
+
problems.push({
|
|
338
|
+
rule: 'ramp-range',
|
|
339
|
+
slot: 'range',
|
|
340
|
+
message: 'a linear or log ramp runs between at least two colors, and one paints every value alike; a single color is value',
|
|
243
341
|
});
|
|
244
342
|
}
|
|
245
|
-
problems.push(...scaleEndProblems(color, { reverse: true }));
|
|
343
|
+
problems.push(...scaleEndProblems({ ...color, type: scale }, { reverse: true }));
|
|
246
344
|
}
|
|
247
345
|
const { labels = [] } = color;
|
|
248
346
|
const named = scale === 'categorical' || scale === IDENTITY_SCALE
|
|
@@ -257,13 +355,13 @@ export function colorProblems(written, presets) {
|
|
|
257
355
|
const what = scale === 'categorical'
|
|
258
356
|
? 'value'
|
|
259
357
|
: scale === IDENTITY_SCALE
|
|
260
|
-
? '
|
|
358
|
+
? 'color'
|
|
261
359
|
: 'interval';
|
|
262
360
|
problems.push({
|
|
263
361
|
rule: 'labels-domain',
|
|
264
362
|
slot: 'labels',
|
|
265
363
|
message: named === undefined
|
|
266
|
-
? "labels names a categorical scale's values, a threshold scale's intervals or an identity scale's
|
|
364
|
+
? "labels names a categorical scale's values, a threshold scale's intervals or an identity scale's colors, and this color paints none of them"
|
|
267
365
|
: `labels names one ${what} each, and ${labels.length} ${labels.length === 1 ? 'label names' : 'labels name'} ${named} ${named === 1 ? what : `${what}s`}: a label past them names nothing`,
|
|
268
366
|
});
|
|
269
367
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/markProblems.ts: do not edit, `pnpm autogen` rewrites it.
|
|
2
2
|
import { AUTO_BIN } from './autoBin.js';
|
|
3
|
-
import { FEATURE_FIELD_PRESETS,
|
|
3
|
+
import { FEATURE_FIELD_PRESETS, colorProblems, fieldScaleOf, isJexl, SHAPE_NAMES, isNamedColor, paintedScale, scaleEndProblems, stepsOutput, universalPresetOf, } from './markRuleFacts.js';
|
|
4
4
|
import { MARK_SPECS, readsValue } from './markSpecs.js';
|
|
5
|
-
import { DEFAULT_AGGREGATE_OP,
|
|
5
|
+
import { DEFAULT_AGGREGATE_OP, DEFAULT_BIN_AS, DEFAULT_BIN_FIELD, DEFAULT_LINE_INTERPOLATE, DEFAULT_ROW_PROPORTION, DEFAULT_LINK_SHAPE, DEFAULT_MARK_TYPE, DEFAULT_PILEUP_FIELDS, DEFAULT_TEXT_FIELD, DEFAULT_WINDOW_FRAME, DEFAULT_X2, isWindowFrame, MAX_THRESHOLD_CUTS, } from './markVocabulary.js';
|
|
6
6
|
import { stepChannels } from './stepChannels.js';
|
|
7
7
|
/**
|
|
8
8
|
* The level of every rule of the list, by the stable id a report and a test
|
|
9
9
|
* refer to it by.
|
|
10
10
|
*/
|
|
11
11
|
export const MARK_RULES = {
|
|
12
|
-
/** A bar, point or
|
|
12
|
+
/** A bar, point, rule or line naming no `y`, with no step before it writing one it reads by default. */
|
|
13
13
|
'mark-without-value': 'error',
|
|
14
14
|
/** A channel the mark's type does not read, such as `y` on a `span` or a size field on a point. */
|
|
15
15
|
'unread-channel': 'warning',
|
|
16
|
-
/** An `encoding.size` on a mark that draws no point or
|
|
16
|
+
/** An `encoding.size` on a mark that draws no point, rule or line and strokes no link. */
|
|
17
17
|
'unread-size': 'warning',
|
|
18
18
|
/** A `linkShape` on a mark that draws no link. */
|
|
19
19
|
'unread-link-shape': 'warning',
|
|
@@ -21,25 +21,27 @@ export const MARK_RULES = {
|
|
|
21
21
|
'unread-interpolate': 'warning',
|
|
22
22
|
/** A `rowProportion` on a mark that draws no span. */
|
|
23
23
|
'unread-row-proportion': 'warning',
|
|
24
|
-
/** `source: "density"` on a `span`, a `text` or a `link`, which cannot draw the sidecar's bins. */
|
|
25
|
-
'span-density-source': 'warning',
|
|
26
24
|
/** Threshold cuts that repeat, leaving an interval no value falls in. */
|
|
27
25
|
'threshold-cuts': 'warning',
|
|
28
|
-
/** A threshold
|
|
26
|
+
/** A threshold color naming no cut, so every value paints one color. */
|
|
29
27
|
'threshold-no-cuts': 'warning',
|
|
30
|
-
/** A threshold
|
|
28
|
+
/** A threshold color naming more cuts than it paints, so a value past the eighth takes the ninth color. */
|
|
31
29
|
'threshold-cut-count': 'warning',
|
|
32
|
-
/** A threshold `range` not one
|
|
30
|
+
/** A threshold `range` not one color longer than its cuts. */
|
|
33
31
|
'threshold-range': 'warning',
|
|
34
|
-
/** A `domain` on a linear or log
|
|
32
|
+
/** A `domain` on a linear or log color, whose ends are `domainMin` and `domainMax`. */
|
|
35
33
|
'ramp-domain': 'warning',
|
|
36
|
-
/** A
|
|
34
|
+
/** A linear or log color whose `range` lists one color, which paints every value alike. */
|
|
35
|
+
'ramp-range': 'warning',
|
|
36
|
+
/** A scale's `domainMax` below its `domainMin`: a color ramp's, a width's or `scales.y`'s. */
|
|
37
37
|
'domain-ends': 'warning',
|
|
38
|
-
/** A
|
|
38
|
+
/** A color ramp's or `scales.y`'s `domainQuantile` outside 0.5 to 1, a percent among them. */
|
|
39
39
|
'domain-quantile': 'warning',
|
|
40
|
-
/** A
|
|
40
|
+
/** A log scale's `domainMin` at or below 0, which it cannot hold, so the end floors above it. */
|
|
41
|
+
'log-floor': 'warning',
|
|
42
|
+
/** A color's or a shape's `field` spelling a CSS color or a shape name, which is a constant written `{ value }`. */
|
|
41
43
|
'field-spells-constant': 'warning',
|
|
42
|
-
/** A
|
|
44
|
+
/** A color's or a shape's `labels` naming values its `domain` does not list, or no categorical scale's. */
|
|
43
45
|
'labels-domain': 'warning',
|
|
44
46
|
/** A `minBpPerPx` not below the mark's `maxBpPerPx`, so the mark never draws. */
|
|
45
47
|
'empty-zoom-range': 'error',
|
|
@@ -47,6 +49,10 @@ export const MARK_RULES = {
|
|
|
47
49
|
'step-expression': 'error',
|
|
48
50
|
/** A `bin` whose `step` is neither `"auto"` nor a positive width. */
|
|
49
51
|
'bin-width': 'error',
|
|
52
|
+
/** A `window`'s `frame` other than two whole row offsets or `null`, the first not past the second, so the step reads its default. */
|
|
53
|
+
'window-frame': 'warning',
|
|
54
|
+
/** A `gapBp` on a mark that draws no line. */
|
|
55
|
+
'unread-gap': 'warning',
|
|
50
56
|
/** A `bin`'s `as` or `fields`, or a `pileup`'s `fields`, naming other than two fields, so the step reads its defaults. */
|
|
51
57
|
'step-pair': 'warning',
|
|
52
58
|
/** A `bin` naming a `field` beside the `fields` it cuts at the bin edges, which leaves the `field` unread. */
|
|
@@ -67,8 +73,6 @@ export const MARK_RULES = {
|
|
|
67
73
|
'two-packings': 'warning',
|
|
68
74
|
/** A `pileup` in the display's `transform` under a `facet`, packing across every section. */
|
|
69
75
|
'cross-section-packing': 'warning',
|
|
70
|
-
/** A second mark standing in for the density sidecar at a zoom where one already does. */
|
|
71
|
-
'second-density-mark': 'warning',
|
|
72
76
|
/** `rows` beside a `facet` on another field, where the facet draws alone. */
|
|
73
77
|
'rows-beside-facet': 'warning',
|
|
74
78
|
/** A `pileup` or a `row` field under `rows`, whose packed rows share their value's one row. */
|
|
@@ -129,9 +133,9 @@ function sizeFieldOf(size) {
|
|
|
129
133
|
? (size.field ?? '')
|
|
130
134
|
: '';
|
|
131
135
|
}
|
|
132
|
-
// A shape's labels name its domain values in order, as a
|
|
136
|
+
// A shape's labels name its domain values in order, as a color's do, and a
|
|
133
137
|
// constant shape names none.
|
|
134
|
-
// What a reader writes as a
|
|
138
|
+
// What a reader writes as a color, without the color parser the rule list
|
|
135
139
|
// cannot carry into `jbrowse validate`.
|
|
136
140
|
function spellsColor(text) {
|
|
137
141
|
return (/^(#[\da-f]{3,8}|(rgb|hsl|hwb|lab|lch|oklab|oklch|color)a?\(.*\)|transparent)$/i.test(text) || isNamedColor(text.toLowerCase()));
|
|
@@ -143,14 +147,14 @@ function fieldOf(channel) {
|
|
|
143
147
|
return typeof field === 'string' && !isJexl(field) ? field : '';
|
|
144
148
|
}
|
|
145
149
|
// Inside `encoding` a bare string is a field, so a constant written the way a
|
|
146
|
-
// display-level
|
|
150
|
+
// display-level color takes one reads as a field no feature holds.
|
|
147
151
|
function constantAsFieldProblems(encoding) {
|
|
148
152
|
const color = fieldOf(encoding?.color);
|
|
149
153
|
const shape = fieldOf(encoding?.shape);
|
|
150
154
|
return [
|
|
151
155
|
...(color && spellsColor(color)
|
|
152
156
|
? [
|
|
153
|
-
found('field-spells-constant', 'encoding.color.field', `${JSON.stringify(color)} is a
|
|
157
|
+
found('field-spells-constant', 'encoding.color.field', `${JSON.stringify(color)} is a color, and a string in encoding is a field: a constant color is { "value": ${JSON.stringify(color)} }`),
|
|
154
158
|
]
|
|
155
159
|
: []),
|
|
156
160
|
...(shape && SHAPE_NAMES.includes(shape)
|
|
@@ -177,9 +181,6 @@ function shapeLabelProblems(shape) {
|
|
|
177
181
|
]
|
|
178
182
|
: [];
|
|
179
183
|
}
|
|
180
|
-
function drawsDensity(mark) {
|
|
181
|
-
return mark.source === 'density' && readsValue(markTypeOf(mark));
|
|
182
|
-
}
|
|
183
184
|
function drawTogether(a, b) {
|
|
184
185
|
const upper = (m) => m.maxBpPerPx || Infinity;
|
|
185
186
|
const lower = (m) => m.minBpPerPx ?? 0;
|
|
@@ -197,6 +198,7 @@ function fieldRefs(step) {
|
|
|
197
198
|
case 'pileup':
|
|
198
199
|
return list('fields', step.fields);
|
|
199
200
|
case 'aggregate':
|
|
201
|
+
case 'window':
|
|
200
202
|
return [
|
|
201
203
|
...list('groupby', step.groupby),
|
|
202
204
|
...(step.ops ?? []).flatMap((o, k) => [
|
|
@@ -266,69 +268,10 @@ function banded(mark, shared) {
|
|
|
266
268
|
stepChannels([...shared, ...stepsOf(mark)]).row !== undefined);
|
|
267
269
|
}
|
|
268
270
|
// The fields the last step that makes its features from nothing leaves behind,
|
|
269
|
-
// with what the steps after it add
|
|
270
|
-
//
|
|
271
|
-
// there.
|
|
271
|
+
// with what the steps after it add; undefined where no step does, and every
|
|
272
|
+
// field of the file is still there.
|
|
272
273
|
function madeFields(steps) {
|
|
273
|
-
|
|
274
|
-
const last = steps[made];
|
|
275
|
-
if (!last) {
|
|
276
|
-
return undefined;
|
|
277
|
-
}
|
|
278
|
-
const fields = new Set(['refName', 'start', 'end']);
|
|
279
|
-
if (last.type === 'coverage') {
|
|
280
|
-
fields.add(last.as ?? DEFAULT_COVERAGE_AS);
|
|
281
|
-
}
|
|
282
|
-
else if (last.type === 'aggregate') {
|
|
283
|
-
const bin = steps
|
|
284
|
-
.slice(0, made)
|
|
285
|
-
.findLast((s) => s.type === 'bin');
|
|
286
|
-
const edges = bin?.as ?? DEFAULT_BIN_AS;
|
|
287
|
-
for (const field of last.groupby?.length
|
|
288
|
-
? last.groupby
|
|
289
|
-
: edges.length === 2
|
|
290
|
-
? edges
|
|
291
|
-
: []) {
|
|
292
|
-
fields.add(field);
|
|
293
|
-
}
|
|
294
|
-
for (const { op = DEFAULT_AGGREGATE_OP, field, as } of last.ops ?? []) {
|
|
295
|
-
fields.add(aggregateFieldName({ op, field, as }));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
for (const step of steps.slice(made + 1)) {
|
|
299
|
-
if (step.type === 'formula') {
|
|
300
|
-
fields.add(step.as ?? DEFAULT_FORMULA_AS);
|
|
301
|
-
}
|
|
302
|
-
else if (step.type === 'pileup') {
|
|
303
|
-
fields.add(step.as ?? DEFAULT_PILEUP_AS);
|
|
304
|
-
}
|
|
305
|
-
else if (step.type === 'flatten') {
|
|
306
|
-
for (const field of [step.index, step.key]) {
|
|
307
|
-
if (field) {
|
|
308
|
-
fields.add(field);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
else if (step.type === 'mate') {
|
|
313
|
-
for (const field of MATE_FIELDS) {
|
|
314
|
-
fields.add(field);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
else if (step.type === 'cells') {
|
|
318
|
-
for (const field of CELLS_FIELDS) {
|
|
319
|
-
fields.add(field);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
else if (step.type === 'bin') {
|
|
323
|
-
for (const edge of step.as?.length === 2 ? step.as : DEFAULT_BIN_AS) {
|
|
324
|
-
fields.add(edge);
|
|
325
|
-
}
|
|
326
|
-
if (step.fields?.length === 2) {
|
|
327
|
-
fields.add(BIN_OVERLAP_FIELD);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return fields;
|
|
274
|
+
return stepsOutput(steps).surviving;
|
|
332
275
|
}
|
|
333
276
|
// What a list of steps cannot run as written, a mark's, the facet's or the
|
|
334
277
|
// display's, each slot under the list's own name.
|
|
@@ -349,6 +292,11 @@ function stepProblems(steps, list = 'transform') {
|
|
|
349
292
|
!(Number(step.step) > 0)) {
|
|
350
293
|
problems.push(found('bin-width', `${at}.step`, 'a bin is a positive width in bp'));
|
|
351
294
|
}
|
|
295
|
+
if (type === 'window' &&
|
|
296
|
+
step.frame !== undefined &&
|
|
297
|
+
!isWindowFrame(step.frame)) {
|
|
298
|
+
problems.push(found('window-frame', `${at}.frame`, `a window's frame is two whole row offsets, either null for unbounded, the first not past the second, so it reads [${DEFAULT_WINDOW_FRAME.join(', ')}]`));
|
|
299
|
+
}
|
|
352
300
|
for (const pair of pairSlots(step)) {
|
|
353
301
|
if (pair.names && pair.names.length !== 2) {
|
|
354
302
|
problems.push(found('step-pair', `${at}.${pair.slot}`, `a ${type} reads two field names from ${pair.slot} and this names ${pair.names.length}, so it reads ${pair.reads}`));
|
|
@@ -360,7 +308,7 @@ function stepProblems(steps, list = 'transform') {
|
|
|
360
308
|
step.field !== DEFAULT_BIN_FIELD) {
|
|
361
309
|
problems.push(found('bin-field-and-fields', `${at}.field`, `a bin over fields cuts each interval at the bin edges and reads no field, so ${step.field} is unread`));
|
|
362
310
|
}
|
|
363
|
-
if (type === 'aggregate') {
|
|
311
|
+
if (type === 'aggregate' || type === 'window') {
|
|
364
312
|
for (const [k, { op = DEFAULT_AGGREGATE_OP, field, weight }] of (step.ops ?? []).entries()) {
|
|
365
313
|
if (op !== 'count' && !field) {
|
|
366
314
|
problems.push(found('op-field', `${at}.ops.${k}.field`, `${op} reads a field and names none`));
|
|
@@ -379,7 +327,7 @@ function stepProblems(steps, list = 'transform') {
|
|
|
379
327
|
return problems;
|
|
380
328
|
}
|
|
381
329
|
// Each field a mark's channels read besides `y`, by the slot naming it: the
|
|
382
|
-
// channels its type reads, a
|
|
330
|
+
// channels its type reads, a color's or a shape's field where it is a scale,
|
|
383
331
|
// a text's field as its default leaves it, and a link's size field.
|
|
384
332
|
function channelFields(mark, spec) {
|
|
385
333
|
const encoding = mark.encoding ?? {};
|
|
@@ -423,7 +371,7 @@ function ownProblems(mark, display, section, split) {
|
|
|
423
371
|
}
|
|
424
372
|
const size = mark.encoding?.size;
|
|
425
373
|
if (!unwritten(size) && spec.size === undefined) {
|
|
426
|
-
problems.push(found('unread-size', 'encoding.size', `a ${type} draws no point or
|
|
374
|
+
problems.push(found('unread-size', 'encoding.size', `a ${type} draws no point, rule or line and strokes no link, so it reads no size`));
|
|
427
375
|
}
|
|
428
376
|
else if (spec.size === 'constant' && sizeFieldOf(size)) {
|
|
429
377
|
problems.push(found('unread-channel', 'encoding.size.field', `a ${type} reads its size as a number, and only a link maps a field to it`));
|
|
@@ -438,25 +386,25 @@ function ownProblems(mark, display, section, split) {
|
|
|
438
386
|
type !== 'line') {
|
|
439
387
|
problems.push(found('unread-interpolate', 'interpolate', `a ${type} draws no line, so it reads no interpolate`));
|
|
440
388
|
}
|
|
389
|
+
if (mark.gapBp !== undefined && type !== 'line') {
|
|
390
|
+
problems.push(found('unread-gap', 'gapBp', `a ${type} draws no line, so it reads no gapBp`));
|
|
391
|
+
}
|
|
441
392
|
if (mark.rowProportion !== undefined &&
|
|
442
393
|
mark.rowProportion !== DEFAULT_ROW_PROPORTION &&
|
|
443
394
|
type !== 'span') {
|
|
444
395
|
problems.push(found('unread-row-proportion', 'rowProportion', `a ${type} draws no span, so it reads no rowProportion`));
|
|
445
396
|
}
|
|
446
|
-
if (!readsValue(type) && mark.source === 'density') {
|
|
447
|
-
problems.push(found('span-density-source', 'source', `a ${type} does not draw the density sidecar`));
|
|
448
|
-
}
|
|
449
397
|
const color = mark.encoding?.color ?? {};
|
|
450
398
|
for (const { rule, slot, message } of colorProblems(color, FEATURE_FIELD_PRESETS)) {
|
|
451
399
|
problems.push(found(rule, `encoding.color.${slot}`, message));
|
|
452
400
|
}
|
|
453
401
|
if (colorScaleOf(color) === 'threshold' &&
|
|
454
402
|
(color.domain ?? []).length === 0) {
|
|
455
|
-
problems.push(found('threshold-no-cuts', 'encoding.color.domain', 'a threshold names its cut points in domain, and with none every value paints the first
|
|
403
|
+
problems.push(found('threshold-no-cuts', 'encoding.color.domain', 'a threshold names its cut points in domain, and with none every value paints the first color'));
|
|
456
404
|
}
|
|
457
405
|
const cutCount = (color.domain ?? []).filter(cut => Number.isFinite(Number(cut))).length;
|
|
458
406
|
if (colorScaleOf(color) === 'threshold' && cutCount > MAX_THRESHOLD_CUTS) {
|
|
459
|
-
problems.push(found('threshold-cut-count', 'encoding.color.domain', `a threshold paints ${MAX_THRESHOLD_CUTS} cuts, and these ${cutCount} leave a value past the ${MAX_THRESHOLD_CUTS}th in the ${MAX_THRESHOLD_CUTS + 1}th
|
|
407
|
+
problems.push(found('threshold-cut-count', 'encoding.color.domain', `a threshold paints ${MAX_THRESHOLD_CUTS} cuts, and these ${cutCount} leave a value past the ${MAX_THRESHOLD_CUTS}th in the ${MAX_THRESHOLD_CUTS + 1}th color`));
|
|
460
408
|
}
|
|
461
409
|
problems.push(...shapeLabelProblems(mark.encoding?.shape));
|
|
462
410
|
problems.push(...constantAsFieldProblems(mark.encoding));
|
|
@@ -509,8 +457,16 @@ export function markProblems({ marks, facet, transform = [], rows, scales, }) {
|
|
|
509
457
|
const section = readable(facet?.transform ?? []);
|
|
510
458
|
const display = readable(transform);
|
|
511
459
|
const shared = [...display, ...section];
|
|
460
|
+
const madeBeforeSplit = split
|
|
461
|
+
? madeFields(drawsRows ? shared : display)
|
|
462
|
+
: undefined;
|
|
512
463
|
const problems = [
|
|
513
464
|
...stepProblems(transform),
|
|
465
|
+
...(split && madeBeforeSplit && !madeBeforeSplit.has(split)
|
|
466
|
+
? [
|
|
467
|
+
found('unwritten-field', faceted ? 'facet.field' : 'rows.field', `reads "${split}", which no step before it writes; they leave ${[...madeBeforeSplit].join(', ')}`),
|
|
468
|
+
]
|
|
469
|
+
: []),
|
|
514
470
|
...stepProblems(facet?.transform ?? [], 'facet.transform'),
|
|
515
471
|
...scaleEndProblems(scales?.y ?? {}).map(({ rule, slot, message }) => found(rule, `scales.y.${slot}`, message)),
|
|
516
472
|
...(faceted
|
|
@@ -554,12 +510,6 @@ export function markProblems({ marks, facet, transform = [], rows, scales, }) {
|
|
|
554
510
|
...found('two-packings', 'transform', `packs rows of its own, as mark ${j} does, and the two share row numbers; one pileup in the ${faceted ? "facet's transform packs them together per section" : "display's transform packs them together"}`),
|
|
555
511
|
});
|
|
556
512
|
}
|
|
557
|
-
if (i > j && drawsDensity(mark) && drawsDensity(other)) {
|
|
558
|
-
problems.push({
|
|
559
|
-
mark: i,
|
|
560
|
-
...found('second-density-mark', 'source', `mark ${j} already stands in for the density sidecar here`),
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
513
|
}
|
|
564
514
|
}
|
|
565
515
|
return problems;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/markRuleFacts.ts: do not edit, `pnpm autogen` rewrites it.
|
|
2
|
-
export { aggregateFieldName } from './
|
|
2
|
+
export { BIN_OVERLAP_FIELD, DEFAULT_AGGREGATE_OP, DEFAULT_BIN_AS, DEFAULT_COVERAGE_AS, DEFAULT_FORMULA_AS, DEFAULT_PILEUP_AS, aggregateFieldName, stepsOutput, } from './stepOutputs.js';
|
|
3
3
|
export { isNamedColor } from './cssColorsLevel4.js';
|
|
4
4
|
export { isJexl } from './jexlStrings.js';
|
|
5
5
|
export { FEATURE_FIELD_PRESETS, colorProblems, fieldScaleOf, paintedScale, scaleEndProblems, universalPresetOf, } from './colorScale.js';
|
|
@@ -31,6 +31,7 @@ export const MARK_SPECS = {
|
|
|
31
31
|
value: 'required',
|
|
32
32
|
hit: 'rows',
|
|
33
33
|
size: 'constant',
|
|
34
|
+
path: true,
|
|
34
35
|
},
|
|
35
36
|
span: {
|
|
36
37
|
channels: ['row', 'color'],
|
|
@@ -62,11 +63,11 @@ export function plotsValue(type) {
|
|
|
62
63
|
return specOf(type).value !== 'none';
|
|
63
64
|
}
|
|
64
65
|
/** Whether a mark type answers a hover through the hit index the worker builds. */
|
|
65
|
-
|
|
66
|
+
function hitsByIndex(type) {
|
|
66
67
|
return specOf(type).hit === 'index';
|
|
67
68
|
}
|
|
68
69
|
/**
|
|
69
|
-
* The lanes a mark asks the worker to fill: its type's channels, the
|
|
70
|
+
* The lanes a mark asks the worker to fill: its type's channels, the color
|
|
70
71
|
* lanes `colorLanesOf` chose for its declaration, and the hit index where the
|
|
71
72
|
* mark answers a hover through one.
|
|
72
73
|
*/
|
|
@@ -79,6 +80,7 @@ export function markLanes(type, colorLanes) {
|
|
|
79
80
|
? colorLanes
|
|
80
81
|
: [channel]),
|
|
81
82
|
...(spec.farFoot ? ['x2Ref'] : []),
|
|
83
|
+
...(spec.path ? ['series'] : []),
|
|
82
84
|
...(hitsByIndex(type) ? ['index'] : []),
|
|
83
85
|
];
|
|
84
86
|
}
|