@jbrowse/cli 4.3.0 → 5.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +451 -72
  2. package/dist/base.js +0 -3
  3. package/dist/bin.js +2 -1
  4. package/dist/cliFetch.js +3 -0
  5. package/dist/commands/add-assembly/index.js +46 -19
  6. package/dist/commands/add-assembly/utils.js +204 -290
  7. package/dist/commands/add-connection.js +73 -79
  8. package/dist/commands/add-track-json.js +24 -12
  9. package/dist/commands/add-track-utils/adapter-utils.js +189 -288
  10. package/dist/commands/add-track-utils/file-operations.js +27 -11
  11. package/dist/commands/add-track-utils/multiwig.js +47 -0
  12. package/dist/commands/add-track-utils/track-config.js +66 -25
  13. package/dist/commands/add-track-utils/validators.js +36 -20
  14. package/dist/commands/add-track.js +200 -62
  15. package/dist/commands/admin-server/index.js +39 -50
  16. package/dist/commands/admin-server/utils.js +238 -126
  17. package/dist/commands/create.js +18 -34
  18. package/dist/commands/make-density/density-generator.js +180 -0
  19. package/dist/commands/make-density/index.js +139 -0
  20. package/dist/commands/make-pif/index.js +133 -11
  21. package/dist/commands/make-pif/pif-generator.js +243 -43
  22. package/dist/commands/process-utils.js +13 -2
  23. package/dist/commands/remove-track.js +20 -13
  24. package/dist/commands/set-default-session.js +35 -31
  25. package/dist/commands/shared/config-operations.js +3 -10
  26. package/dist/commands/shared/density.js +12 -0
  27. package/dist/commands/shared/sort-utils.js +67 -10
  28. package/dist/commands/shared/validators.js +38 -3
  29. package/dist/commands/sort-bed.js +2 -33
  30. package/dist/commands/sort-gff.js +2 -33
  31. package/dist/commands/text-index/adapter-utils.js +19 -26
  32. package/dist/commands/text-index/aggregate.js +40 -34
  33. package/dist/commands/text-index/command.js +37 -7
  34. package/dist/commands/text-index/config-utils.js +62 -85
  35. package/dist/commands/text-index/file-list.js +32 -18
  36. package/dist/commands/text-index/index.js +4 -4
  37. package/dist/commands/text-index/indexing-utils.js +44 -96
  38. package/dist/commands/text-index/per-track.js +44 -31
  39. package/dist/commands/text-index/validators.js +1 -1
  40. package/dist/commands/upgrade.js +20 -32
  41. package/dist/commands/validate/configManifest.generated.js +7613 -0
  42. package/dist/commands/validate/configSchema.generated.js +20400 -0
  43. package/dist/commands/validate/displayDefaultKeys.js +28 -0
  44. package/dist/commands/validate/index.js +113 -0
  45. package/dist/commands/validate/liftConfig.js +34 -0
  46. package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
  47. package/dist/commands/validate/markRules/autoBin.js +27 -0
  48. package/dist/commands/validate/markRules/colorScale.js +244 -0
  49. package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
  50. package/dist/commands/validate/markRules/jexlStrings.js +23 -0
  51. package/dist/commands/validate/markRules/markProblems.js +532 -0
  52. package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
  53. package/dist/commands/validate/markRules/markSpecs.js +96 -0
  54. package/dist/commands/validate/markRules/markVocabulary.js +57 -0
  55. package/dist/commands/validate/markRules/shapeNameList.js +7 -0
  56. package/dist/commands/validate/markRules/stepChannels.js +55 -0
  57. package/dist/commands/validate/schemaValidate.js +425 -0
  58. package/dist/commands/validate/suggest.js +35 -0
  59. package/dist/commands/validate/types.js +6 -0
  60. package/dist/commands/validate/validateConfig.js +502 -0
  61. package/dist/index.js +104 -50
  62. package/dist/types/common.js +3 -27
  63. package/dist/utils.js +309 -128
  64. package/dist/version.js +1 -1
  65. package/package.json +8 -14
  66. package/bin/run +0 -4
  67. package/bundle/index.js +0 -3686
  68. package/dist/commands/make-pif/cigar-utils.js +0 -24
  69. package/dist/commands/make-pif/file-utils.js +0 -30
  70. package/dist/commands/track-utils.js +0 -74
  71. package/dist/util.js +0 -35
@@ -0,0 +1,532 @@
1
+ // Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/markProblems.ts: do not edit, `pnpm autogen` rewrites it.
2
+ import { AUTO_BIN } from './autoBin.js';
3
+ import { FEATURE_FIELD_PRESETS, aggregateFieldName, colorProblems, fieldScaleOf, isJexl, SHAPE_NAMES, isNamedColor, paintedScale, scaleEndProblems, universalPresetOf, } from './markRuleFacts.js';
4
+ import { MARK_SPECS, rampResolvesPerRegion, readsValue } from './markSpecs.js';
5
+ import { DEFAULT_AGGREGATE_OP, BIN_OVERLAP_FIELD, DEFAULT_BIN_AS, DEFAULT_BIN_FIELD, DEFAULT_COVERAGE_AS, DEFAULT_FORMULA_AS, DEFAULT_LINE_INTERPOLATE, DEFAULT_ROW_PROPORTION, DEFAULT_LINK_SHAPE, DEFAULT_MARK_TYPE, CELLS_FIELDS, DEFAULT_PILEUP_AS, DEFAULT_PILEUP_FIELDS, DEFAULT_TEXT_FIELD, DEFAULT_X2, MATE_FIELDS, } from './markVocabulary.js';
6
+ import { stepChannels } from './stepChannels.js';
7
+ /**
8
+ * The level of every rule of the list, by the stable id a report and a test
9
+ * refer to it by.
10
+ */
11
+ export const MARK_RULES = {
12
+ /** A bar, point or rule naming no `y`, with no step before it writing one it reads by default. */
13
+ 'mark-without-value': 'error',
14
+ /** A channel the mark's type does not read, such as `y` on a `span` or a size field on a point. */
15
+ 'unread-channel': 'warning',
16
+ /** An `encoding.size` on a mark that draws no point or rule and strokes no link. */
17
+ 'unread-size': 'warning',
18
+ /** A `linkShape` on a mark that draws no link. */
19
+ 'unread-link-shape': 'warning',
20
+ /** An `interpolate` on a mark that draws no line. */
21
+ 'unread-interpolate': 'warning',
22
+ /** A `rowProportion` on a mark that draws no span. */
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
+ /** Threshold cuts that repeat, leaving an interval no value falls in. */
27
+ 'threshold-cuts': 'warning',
28
+ /** A threshold colour naming no cut, so every value paints one colour. */
29
+ 'threshold-no-cuts': 'warning',
30
+ /** A threshold `range` not one colour longer than its cuts. */
31
+ 'threshold-range': 'warning',
32
+ /** A `domain` on a linear or log colour, whose ends are `domainMin` and `domainMax`. */
33
+ 'ramp-domain': 'warning',
34
+ /** A scale's `domainMax` below its `domainMin`: a colour ramp's, a width's or `scales.y`'s. */
35
+ 'domain-ends': 'warning',
36
+ /** A colour ramp's or `scales.y`'s `domainQuantile` outside 0.5 to 1, a percent among them. */
37
+ 'domain-quantile': 'warning',
38
+ /** A colour's or a shape's `field` spelling a CSS colour or a shape name, which is a constant written `{ value }`. */
39
+ 'field-spells-constant': 'warning',
40
+ /** A colour's or a shape's `labels` naming values its `domain` does not list, or no categorical scale's. */
41
+ 'labels-domain': 'warning',
42
+ /** A text's colour ramp with an open end, whose colours then differ from one region to the next. */
43
+ 'unpinned-text-ramp': 'warning',
44
+ /** A `minBpPerPx` not below the mark's `maxBpPerPx`, so the mark never draws. */
45
+ 'empty-zoom-range': 'error',
46
+ /** A `filter` or `formula` whose `expr` is not a `jexl:` expression. */
47
+ 'step-expression': 'error',
48
+ /** A `bin` whose `step` is neither `"auto"` nor a positive width. */
49
+ 'bin-width': 'error',
50
+ /** A `bin`'s `as` or `fields`, or a `pileup`'s `fields`, naming other than two fields, so the step reads its defaults. */
51
+ 'step-pair': 'warning',
52
+ /** A `bin` naming a `field` beside the `fields` it cuts at the bin edges, which leaves the `field` unread. */
53
+ 'bin-field-and-fields': 'warning',
54
+ /** A `sum`, `mean`, `min` or `max` naming no `field`. */
55
+ 'op-field': 'error',
56
+ /** A `weight` on a `min` or a `max`, which no weight moves. */
57
+ 'unread-weight': 'warning',
58
+ /** A step's field written as a `jexl:` expression, where a step reads a name or a dotted path. */
59
+ 'step-field-expression': 'error',
60
+ /** A `y` naming a field that no `aggregate` or `coverage` step before it writes. */
61
+ 'unwritten-y': 'error',
62
+ /** A mark other than a span drawn beside one that stacks rows, standing in the first of them. */
63
+ 'value-beside-rows': 'warning',
64
+ /** Two `pileup` steps packing one plot, whose rows share numbers. */
65
+ 'two-packings': 'warning',
66
+ /** A `pileup` in the display's `transform` under a `facet`, packing across every section. */
67
+ 'cross-section-packing': 'warning',
68
+ /** A second mark standing in for the density sidecar at a zoom where one already does. */
69
+ 'second-density-mark': 'warning',
70
+ /** `rows` beside a `facet` on another field, where the facet draws alone. */
71
+ 'rows-beside-facet': 'warning',
72
+ /** A `pileup` or a `row` field under `rows`, whose packed rows share their value's one row. */
73
+ 'packing-under-rows': 'warning',
74
+ };
75
+ function found(rule, slot, message) {
76
+ return { rule, level: MARK_RULES[rule], slot, message };
77
+ }
78
+ function markTypeOf(mark) {
79
+ return mark.mark ?? DEFAULT_MARK_TYPE;
80
+ }
81
+ // Whether a slot holds nothing a reader could act on: unset, empty text, an
82
+ // empty list, or an object none of whose members holds anything.
83
+ function unwritten(value) {
84
+ return (value === undefined ||
85
+ value === '' ||
86
+ (Array.isArray(value) && value.length === 0) ||
87
+ (typeof value === 'object' &&
88
+ value !== null &&
89
+ Object.values(value).every(unwritten)));
90
+ }
91
+ // Whether a channel is written as its default, which a display's snapshot
92
+ // leaves off and a file may spell out: the two read alike, so a slot at its
93
+ // default is no channel for a rule to find unread.
94
+ function atDefault(channel, value) {
95
+ switch (channel) {
96
+ case 'x':
97
+ return value === DEFAULT_BIN_AS[0];
98
+ case 'x2':
99
+ return (value === DEFAULT_X2 ||
100
+ (typeof value === 'object' &&
101
+ value !== null &&
102
+ unwritten(value.chrom) &&
103
+ (value.pos === DEFAULT_X2 ||
104
+ unwritten(value.pos))));
105
+ case 'text':
106
+ return value === DEFAULT_TEXT_FIELD;
107
+ default:
108
+ return false;
109
+ }
110
+ }
111
+ /** The channels a mark's encoding writes, less those at their default. */
112
+ function writtenChannels(encoding) {
113
+ return Object.entries(encoding ?? {})
114
+ .filter(([channel, value]) => !unwritten(value) && !atDefault(channel, value))
115
+ .map(([channel]) => channel);
116
+ }
117
+ function stepsOf(mark) {
118
+ return mark.transform ?? [];
119
+ }
120
+ function colorScaleOf({ scale, field = '' }) {
121
+ return paintedScale({ scale, field }, fieldScaleOf(FEATURE_FIELD_PRESETS, field));
122
+ }
123
+ function rampColor(mark) {
124
+ const color = mark.encoding?.color ?? {};
125
+ const painted = colorScaleOf(color);
126
+ return painted === 'linear' || painted === 'log' ? color : undefined;
127
+ }
128
+ function sizeFieldOf(size) {
129
+ return typeof size === 'string'
130
+ ? size
131
+ : typeof size === 'object'
132
+ ? (size.field ?? '')
133
+ : '';
134
+ }
135
+ // A shape's labels name its domain values in order, as a colour's do, and a
136
+ // constant shape names none.
137
+ // What a reader writes as a colour, without the colour parser the rule list
138
+ // cannot carry into `jbrowse validate`.
139
+ function spellsColor(text) {
140
+ return (/^(#[\da-f]{3,8}|(rgb|hsl|hwb|lab|lch|oklab|oklch|color)a?\(.*\)|transparent)$/i.test(text) || isNamedColor(text.toLowerCase()));
141
+ }
142
+ function fieldOf(channel) {
143
+ const field = typeof channel === 'object' && channel !== null
144
+ ? channel.field
145
+ : channel;
146
+ return typeof field === 'string' && !isJexl(field) ? field : '';
147
+ }
148
+ // Inside `encoding` a bare string is a field, so a constant written the way a
149
+ // display-level colour takes one reads as a field no feature holds.
150
+ function constantAsFieldProblems(encoding) {
151
+ const color = fieldOf(encoding?.color);
152
+ const shape = fieldOf(encoding?.shape);
153
+ return [
154
+ ...(color && spellsColor(color)
155
+ ? [
156
+ found('field-spells-constant', 'encoding.color.field', `${JSON.stringify(color)} is a colour, and a string in encoding is a field: a constant colour is { "value": ${JSON.stringify(color)} }`),
157
+ ]
158
+ : []),
159
+ ...(shape && SHAPE_NAMES.includes(shape)
160
+ ? [
161
+ found('field-spells-constant', 'encoding.shape.field', `${JSON.stringify(shape)} is a shape, and a string in encoding is a field: a constant shape is { "value": ${JSON.stringify(shape)} }`),
162
+ ]
163
+ : []),
164
+ ];
165
+ }
166
+ function shapeLabelProblems(shape) {
167
+ if (typeof shape !== 'object' || shape === null) {
168
+ return [];
169
+ }
170
+ const { field = '', scale, domain = [], labels = [] } = shape;
171
+ // an unwritten domain pairs the labels with the field's own order, as the
172
+ // key and the worker do
173
+ const order = domain.length > 0 ? domain : (universalPresetOf(field)?.domain ?? []);
174
+ const named = paintedScale({ scale, field }, 'categorical') === 'categorical'
175
+ ? order.length
176
+ : 0;
177
+ return labels.length > named
178
+ ? [
179
+ found('labels-domain', 'encoding.shape.labels', `labels names one domain value each, and ${labels.length} ${labels.length === 1 ? 'label names' : 'labels name'} ${named}: a label past them names nothing`),
180
+ ]
181
+ : [];
182
+ }
183
+ function drawsDensity(mark) {
184
+ return mark.source === 'density' && readsValue(markTypeOf(mark));
185
+ }
186
+ function drawTogether(a, b) {
187
+ const upper = (m) => m.maxBpPerPx || Infinity;
188
+ const lower = (m) => m.minBpPerPx ?? 0;
189
+ return lower(a) < upper(b) && lower(b) < upper(a);
190
+ }
191
+ // The field names a step reads, by the slot each is written in.
192
+ function fieldRefs(step) {
193
+ const list = (slot, refs = []) => refs.map((ref, k) => [`${slot}.${k}`, ref]);
194
+ switch (step.type) {
195
+ case 'bin':
196
+ return [['field', step.field], ...list('fields', step.fields)];
197
+ case 'flatten':
198
+ case 'cells':
199
+ return [['field', step.field]];
200
+ case 'pileup':
201
+ return list('fields', step.fields);
202
+ case 'aggregate':
203
+ return [
204
+ ...list('groupby', step.groupby),
205
+ ...(step.ops ?? []).flatMap((o, k) => [
206
+ [`ops.${k}.field`, o.field],
207
+ [`ops.${k}.weight`, o.weight],
208
+ ]),
209
+ ];
210
+ default:
211
+ return [];
212
+ }
213
+ }
214
+ // The slots naming two fields, and what each reads as when it names another
215
+ // number of them: a bin's `fields` left empty is a bin by `field`.
216
+ function pairSlots(step) {
217
+ return step.type === 'bin'
218
+ ? [
219
+ { slot: 'as', names: step.as, reads: DEFAULT_BIN_AS.join(' and ') },
220
+ {
221
+ slot: 'fields',
222
+ names: step.fields?.length ? step.fields : undefined,
223
+ reads: `its field alone, ${step.field || DEFAULT_BIN_FIELD}`,
224
+ },
225
+ ]
226
+ : step.type === 'pileup'
227
+ ? [
228
+ {
229
+ slot: 'fields',
230
+ names: step.fields,
231
+ reads: DEFAULT_PILEUP_FIELDS.join(' and '),
232
+ },
233
+ ]
234
+ : [];
235
+ }
236
+ function readable(steps) {
237
+ return steps.filter(s => s !== undefined);
238
+ }
239
+ function packsIn(steps) {
240
+ return steps.some(s => s?.type === 'pileup');
241
+ }
242
+ function packs(mark) {
243
+ return packsIn(stepsOf(mark));
244
+ }
245
+ function named(field) {
246
+ return typeof field === 'string' && field !== '';
247
+ }
248
+ const ONE_ROW_PER_VALUE = "rows draws one row per value, so the rows a pileup packs share their value's row; a facet gives each value a section as deep as its pileup";
249
+ // Where a display drawing one row per value packs more than one.
250
+ function packingsUnderRows(marks, transform, section) {
251
+ const pileups = (steps, list) => steps.flatMap((step, i) => step?.type === 'pileup'
252
+ ? [found('packing-under-rows', `${list}.${i}`, ONE_ROW_PER_VALUE)]
253
+ : []);
254
+ return [
255
+ ...pileups(transform, 'transform'),
256
+ ...pileups(section, 'facet.transform'),
257
+ ...marks.flatMap((mark, i) => mark
258
+ ? [
259
+ ...pileups(stepsOf(mark), 'transform'),
260
+ ...(mark.encoding?.row
261
+ ? [found('packing-under-rows', 'encoding.row', ONE_ROW_PER_VALUE)]
262
+ : []),
263
+ ].map(p => ({ mark: i, ...p }))
264
+ : []),
265
+ ];
266
+ }
267
+ function banded(mark, shared) {
268
+ return (!!mark.encoding?.row ||
269
+ stepChannels([...shared, ...stepsOf(mark)]).row !== undefined);
270
+ }
271
+ // The fields the last step that makes its features from nothing leaves behind,
272
+ // with what the steps after it add, over the display's steps and then the
273
+ // mark's; undefined where no step does, and every field of the file is still
274
+ // there.
275
+ function madeFields(steps) {
276
+ const made = steps.findLastIndex(s => s.type === 'aggregate' || s.type === 'coverage');
277
+ const last = steps[made];
278
+ if (!last) {
279
+ return undefined;
280
+ }
281
+ const fields = new Set(['refName', 'start', 'end']);
282
+ if (last.type === 'coverage') {
283
+ fields.add(last.as ?? DEFAULT_COVERAGE_AS);
284
+ }
285
+ else if (last.type === 'aggregate') {
286
+ const bin = steps
287
+ .slice(0, made)
288
+ .findLast((s) => s.type === 'bin');
289
+ const edges = bin?.as ?? DEFAULT_BIN_AS;
290
+ for (const field of last.groupby?.length
291
+ ? last.groupby
292
+ : edges.length === 2
293
+ ? edges
294
+ : []) {
295
+ fields.add(field);
296
+ }
297
+ for (const { op = DEFAULT_AGGREGATE_OP, field, as } of last.ops ?? []) {
298
+ fields.add(aggregateFieldName({ op, field, as }));
299
+ }
300
+ }
301
+ for (const step of steps.slice(made + 1)) {
302
+ if (step.type === 'formula') {
303
+ fields.add(step.as ?? DEFAULT_FORMULA_AS);
304
+ }
305
+ else if (step.type === 'pileup') {
306
+ fields.add(step.as ?? DEFAULT_PILEUP_AS);
307
+ }
308
+ else if (step.type === 'flatten') {
309
+ for (const field of [step.index, step.key]) {
310
+ if (field) {
311
+ fields.add(field);
312
+ }
313
+ }
314
+ }
315
+ else if (step.type === 'mate') {
316
+ for (const field of MATE_FIELDS) {
317
+ fields.add(field);
318
+ }
319
+ }
320
+ else if (step.type === 'cells') {
321
+ for (const field of CELLS_FIELDS) {
322
+ fields.add(field);
323
+ }
324
+ }
325
+ else if (step.type === 'bin') {
326
+ for (const edge of step.as?.length === 2 ? step.as : DEFAULT_BIN_AS) {
327
+ fields.add(edge);
328
+ }
329
+ if (step.fields?.length === 2) {
330
+ fields.add(BIN_OVERLAP_FIELD);
331
+ }
332
+ }
333
+ }
334
+ return fields;
335
+ }
336
+ // What a list of steps cannot run as written, a mark's, the facet's or the
337
+ // display's, each slot under the list's own name.
338
+ function stepProblems(steps, list = 'transform') {
339
+ const problems = [];
340
+ for (const [i, step] of steps.entries()) {
341
+ if (!step) {
342
+ continue;
343
+ }
344
+ const { type } = step;
345
+ const at = `${list}.${i}`;
346
+ if ((type === 'filter' || type === 'formula') && !isJexl(step.expr ?? '')) {
347
+ problems.push(found('step-expression', `${at}.expr`, `a ${type} reads a jexl: expression`));
348
+ }
349
+ if (type === 'bin' &&
350
+ step.step !== undefined &&
351
+ step.step !== AUTO_BIN &&
352
+ !(Number(step.step) > 0)) {
353
+ problems.push(found('bin-width', `${at}.step`, 'a bin is a positive width in bp'));
354
+ }
355
+ for (const pair of pairSlots(step)) {
356
+ if (pair.names && pair.names.length !== 2) {
357
+ 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}`));
358
+ }
359
+ }
360
+ if (type === 'bin' &&
361
+ step.fields?.length === 2 &&
362
+ step.field &&
363
+ step.field !== DEFAULT_BIN_FIELD) {
364
+ 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`));
365
+ }
366
+ if (type === 'aggregate') {
367
+ for (const [k, { op = DEFAULT_AGGREGATE_OP, field, weight }] of (step.ops ?? []).entries()) {
368
+ if (op !== 'count' && !field) {
369
+ problems.push(found('op-field', `${at}.ops.${k}.field`, `${op} reads a field and names none`));
370
+ }
371
+ if ((op === 'min' || op === 'max') && weight) {
372
+ problems.push(found('unread-weight', `${at}.ops.${k}.weight`, `a weight moves no ${op === 'min' ? 'minimum' : 'maximum'}, so the ${op} reads its field alone`));
373
+ }
374
+ }
375
+ }
376
+ for (const [slot, ref] of fieldRefs(step)) {
377
+ if (ref && isJexl(ref)) {
378
+ problems.push(found('step-field-expression', `${at}.${slot}`, `the ${type} step reads a field name or a dotted path; a formula step in front computes one`));
379
+ }
380
+ }
381
+ }
382
+ return problems;
383
+ }
384
+ function ownProblems(mark, display, section) {
385
+ const type = markTypeOf(mark);
386
+ const problems = [];
387
+ const y = mark.encoding?.y;
388
+ if (readsValue(type) &&
389
+ !y &&
390
+ !stepChannels([...display, ...section, ...stepsOf(mark)]).y) {
391
+ problems.push(found('mark-without-value', 'encoding.y', `a ${type} stands at a value, and names no y field while no step before it writes one — a coverage, or an aggregate with one op — so it draws nothing`));
392
+ }
393
+ const spec = MARK_SPECS[type];
394
+ const channels = new Set(['x', 'x2', 'size', ...spec.channels]);
395
+ for (const channel of writtenChannels(mark.encoding)) {
396
+ if (!channels.has(channel)) {
397
+ problems.push(found('unread-channel', `encoding.${channel}`, `a ${type} does not read ${channel}`));
398
+ }
399
+ }
400
+ const size = mark.encoding?.size;
401
+ if (!unwritten(size) && spec.size === undefined) {
402
+ problems.push(found('unread-size', 'encoding.size', `a ${type} draws no point or rule and strokes no link, so it reads no size`));
403
+ }
404
+ else if (spec.size === 'constant' && sizeFieldOf(size)) {
405
+ 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`));
406
+ }
407
+ if (mark.linkShape !== undefined &&
408
+ mark.linkShape !== DEFAULT_LINK_SHAPE &&
409
+ type !== 'link') {
410
+ problems.push(found('unread-link-shape', 'linkShape', `a ${type} draws no link, so it reads no linkShape`));
411
+ }
412
+ if (mark.interpolate !== undefined &&
413
+ mark.interpolate !== DEFAULT_LINE_INTERPOLATE &&
414
+ type !== 'line') {
415
+ problems.push(found('unread-interpolate', 'interpolate', `a ${type} draws no line, so it reads no interpolate`));
416
+ }
417
+ if (mark.rowProportion !== undefined &&
418
+ mark.rowProportion !== DEFAULT_ROW_PROPORTION &&
419
+ type !== 'span') {
420
+ problems.push(found('unread-row-proportion', 'rowProportion', `a ${type} draws no span, so it reads no rowProportion`));
421
+ }
422
+ if (!readsValue(type) && mark.source === 'density') {
423
+ problems.push(found('span-density-source', 'source', `a ${type} does not draw the density sidecar`));
424
+ }
425
+ const color = mark.encoding?.color ?? {};
426
+ for (const { rule, slot, message } of colorProblems(color, FEATURE_FIELD_PRESETS)) {
427
+ problems.push(found(rule, `encoding.color.${slot}`, message));
428
+ }
429
+ if (colorScaleOf(color) === 'threshold' &&
430
+ (color.domain ?? []).length === 0) {
431
+ 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 colour'));
432
+ }
433
+ problems.push(...shapeLabelProblems(mark.encoding?.shape));
434
+ problems.push(...constantAsFieldProblems(mark.encoding));
435
+ if (typeof size === 'object') {
436
+ for (const { rule, slot, message } of scaleEndProblems(size)) {
437
+ problems.push(found(rule, `encoding.size.${slot}`, message));
438
+ }
439
+ }
440
+ const ramp = rampColor(mark);
441
+ if (ramp) {
442
+ const { domainMin, domainMax } = ramp;
443
+ if (rampResolvesPerRegion(type) &&
444
+ (domainMin === undefined || domainMax === undefined)) {
445
+ problems.push(found('unpinned-text-ramp', `encoding.color.${domainMin === undefined ? 'domainMin' : 'domainMax'}`, `a ${type}'s ramp resolves an open end against each region's own extremes, so its colours agree across regions only with domainMin and domainMax both pinned`));
446
+ }
447
+ }
448
+ const { minBpPerPx = 0, maxBpPerPx = 0 } = mark;
449
+ if (minBpPerPx > 0 && maxBpPerPx > 0 && minBpPerPx >= maxBpPerPx) {
450
+ problems.push(found('empty-zoom-range', 'minBpPerPx', `never draws: minBpPerPx ${minBpPerPx} is not below maxBpPerPx ${maxBpPerPx}`));
451
+ }
452
+ problems.push(...stepProblems(stepsOf(mark)));
453
+ if (packs(mark) && (packsIn(section) || packsIn(display))) {
454
+ problems.push(found('two-packings', 'transform', `packs rows of its own over the ${packsIn(section) ? "facet's" : "display's"} pileup, and the two share row numbers; one pileup packs every mark`));
455
+ }
456
+ const fields = madeFields([...display, ...section, ...stepsOf(mark)]);
457
+ if (fields && y && !isJexl(y) && !fields.has(y)) {
458
+ problems.push(found('unwritten-y', 'encoding.y', `reads "${y}", which no step before it writes; they leave ${[...fields].join(', ')}`));
459
+ }
460
+ return problems;
461
+ }
462
+ /**
463
+ * Every rule a plot breaks. The display's step lists are checked as a mark's
464
+ * are and reported with no mark, under `transform` and `facet.transform`, and
465
+ * so are `scales.y`'s ends. A file's own spelling goes through the schema's
466
+ * lift first, which `jbrowse validate` does from the generated manifest.
467
+ */
468
+ export function markProblems({ marks, facet, transform = [], rows, scales, }) {
469
+ const faceted = named(facet?.field);
470
+ const drawsRows = named(rows?.field) && !faceted;
471
+ const section = readable(facet?.transform ?? []);
472
+ const display = readable(transform);
473
+ const shared = [...display, ...section];
474
+ const problems = [
475
+ ...stepProblems(transform),
476
+ ...stepProblems(facet?.transform ?? [], 'facet.transform'),
477
+ ...scaleEndProblems(scales?.y ?? {}).map(({ rule, slot, message }) => found(rule, `scales.y.${slot}`, message)),
478
+ ...(faceted
479
+ ? transform.flatMap((step, i) => step?.type === 'pileup'
480
+ ? [
481
+ found('cross-section-packing', `transform.${i}`, 'runs before the facet splits the features, so it packs across every section and leaves each section the rows the others fill; the same pileup in facet.transform packs each section on its own'),
482
+ ]
483
+ : [])
484
+ : []),
485
+ ...(faceted && named(rows?.field) && rows?.field !== facet?.field
486
+ ? [
487
+ found('rows-beside-facet', 'rows.field', 'facet stacks a labelled section per value and rows one row per value; bands of rows over two fields are not drawn yet, so the facet draws alone'),
488
+ ]
489
+ : []),
490
+ ...(drawsRows ? packingsUnderRows(marks, transform, section) : []),
491
+ ...marks.flatMap((mark, i) => mark
492
+ ? ownProblems(mark, display, section).map(p => ({ mark: i, ...p }))
493
+ : []),
494
+ ];
495
+ for (const [i, mark] of marks.entries()) {
496
+ for (const [j, other] of marks.entries()) {
497
+ if (i === j || !mark || !other || !drawTogether(mark, other)) {
498
+ continue;
499
+ }
500
+ if (!faceted &&
501
+ !drawsRows &&
502
+ markTypeOf(mark) !== 'span' &&
503
+ !banded(mark, shared) &&
504
+ banded(other, shared)) {
505
+ problems.push({
506
+ mark: i,
507
+ ...found('value-beside-rows', 'encoding.row', `stands in the first of the rows mark ${j} bands the plot into, its axis repeated per row`),
508
+ });
509
+ }
510
+ if (i > j && packs(mark) && packs(other)) {
511
+ problems.push({
512
+ mark: i,
513
+ ...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"}`),
514
+ });
515
+ }
516
+ if (i > j && drawsDensity(mark) && drawsDensity(other)) {
517
+ problems.push({
518
+ mark: i,
519
+ ...found('second-density-mark', 'source', `mark ${j} already stands in for the density sidecar here`),
520
+ });
521
+ }
522
+ }
523
+ }
524
+ return problems;
525
+ }
526
+ /**
527
+ * A problem as one line of a notice: `mark 0 encoding.y: …`, or
528
+ * `transform.0.step: …` for the display's own step.
529
+ */
530
+ export function problemText({ mark, slot, message }) {
531
+ return `${mark === undefined ? '' : `mark ${mark} `}${slot}: ${message}`;
532
+ }
@@ -0,0 +1,6 @@
1
+ // Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/markRuleFacts.ts: do not edit, `pnpm autogen` rewrites it.
2
+ export { aggregateFieldName } from './aggregateFieldName.js';
3
+ export { isNamedColor } from './cssColorsLevel4.js';
4
+ export { isJexl } from './jexlStrings.js';
5
+ export { FEATURE_FIELD_PRESETS, colorProblems, fieldScaleOf, paintedScale, scaleEndProblems, universalPresetOf, } from './colorScale.js';
6
+ export { SHAPE_NAMES } from './shapeNameList.js';
@@ -0,0 +1,96 @@
1
+ /**
2
+ * What each mark type reads: the one statement the lane request, the rule list and
3
+ * the model's value tests derive theirs from.
4
+ *
5
+ * Its own module, reaching only the vocabulary beside it: the rule list reads
6
+ * it and `jbrowse validate` carries a copy, and markList.ts takes render-core's
7
+ * marks barrel — shader sources included — which the examples sites' eager
8
+ * budget then pays for. `layerRequests` in model.ts is where each lane is
9
+ * checked to be one the encoder fills.
10
+ */
11
+ export const MARK_SPECS = {
12
+ bar: {
13
+ channels: ['y', 'row', 'color'],
14
+ value: 'required',
15
+ ramp: 'display',
16
+ hit: 'rows',
17
+ },
18
+ point: {
19
+ channels: ['y', 'row', 'color', 'shape'],
20
+ value: 'required',
21
+ ramp: 'display',
22
+ hit: 'index',
23
+ size: 'constant',
24
+ },
25
+ rule: {
26
+ channels: ['y', 'row', 'color'],
27
+ value: 'required',
28
+ ramp: 'display',
29
+ hit: 'rows',
30
+ size: 'constant',
31
+ },
32
+ line: {
33
+ channels: ['y', 'row', 'color'],
34
+ value: 'required',
35
+ ramp: 'display',
36
+ hit: 'rows',
37
+ size: 'constant',
38
+ },
39
+ span: {
40
+ channels: ['row', 'color'],
41
+ value: 'none',
42
+ ramp: 'display',
43
+ hit: 'rows',
44
+ },
45
+ text: {
46
+ channels: ['y', 'row', 'color', 'text'],
47
+ value: 'optional',
48
+ ramp: 'worker',
49
+ hit: false,
50
+ },
51
+ link: {
52
+ channels: ['y', 'row', 'color', 'size'],
53
+ value: 'optional',
54
+ ramp: 'display',
55
+ hit: 'index',
56
+ farFoot: true,
57
+ size: 'channel',
58
+ },
59
+ };
60
+ function specOf(type) {
61
+ return MARK_SPECS[type];
62
+ }
63
+ /** Whether a mark type stands at a `y` value, so a mark of it naming none draws nothing. */
64
+ export function readsValue(type) {
65
+ return specOf(type).value === 'required';
66
+ }
67
+ /** Whether a mark type may plot a `y`, and so folds its values into the axis. */
68
+ export function plotsValue(type) {
69
+ return specOf(type).value !== 'none';
70
+ }
71
+ /** Whether a mark type answers a hover through the hit index the worker builds. */
72
+ export function hitsByIndex(type) {
73
+ return specOf(type).hit === 'index';
74
+ }
75
+ /** Whether a mark type's ramp colour resolves per region, in the worker. */
76
+ export function rampResolvesPerRegion(type) {
77
+ return specOf(type).ramp === 'worker';
78
+ }
79
+ /**
80
+ * The lanes a mark asks the worker to fill: its type's channels, a ramp's raw
81
+ * values beside the colour where the display resolves the ramp, and the hit
82
+ * index where the mark answers a hover through one. The encoder fills whichever of
83
+ * `color` and `colorValue` the colour declaration calls for.
84
+ */
85
+ export function markLanes(type) {
86
+ const spec = specOf(type);
87
+ return [
88
+ ...spec.channels.flatMap((channel) => channel === 'shape'
89
+ ? ['glyph']
90
+ : channel === 'color' && spec.ramp === 'display'
91
+ ? [channel, 'colorValue']
92
+ : [channel]),
93
+ ...(spec.farFoot ? ['x2Ref'] : []),
94
+ ...(hitsByIndex(type) ? ['index'] : []),
95
+ ];
96
+ }