@jbrowse/cli 5.0.0-beta.1 → 5.0.0-beta.11

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 (39) hide show
  1. package/README.md +149 -44
  2. package/dist/commands/add-track-utils/adapter-utils.js +47 -39
  3. package/dist/commands/add-track-utils/track-config.js +5 -7
  4. package/dist/commands/add-track.js +31 -9
  5. package/dist/commands/admin-server/index.js +13 -5
  6. package/dist/commands/admin-server/utils.js +8 -6
  7. package/dist/commands/make-density/density-generator.js +180 -0
  8. package/dist/commands/make-density/index.js +139 -0
  9. package/dist/commands/make-pif/index.js +62 -27
  10. package/dist/commands/make-pif/pif-generator.js +98 -132
  11. package/dist/commands/shared/density.js +12 -0
  12. package/dist/commands/text-index/adapter-utils.js +17 -7
  13. package/dist/commands/text-index/aggregate.js +5 -5
  14. package/dist/commands/text-index/command.js +3 -1
  15. package/dist/commands/text-index/config-utils.js +7 -7
  16. package/dist/commands/text-index/indexing-utils.js +21 -49
  17. package/dist/commands/validate/configManifest.generated.js +4071 -1845
  18. package/dist/commands/validate/configSchema.generated.js +20482 -0
  19. package/dist/commands/validate/index.js +21 -5
  20. package/dist/commands/validate/liftConfig.js +34 -0
  21. package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
  22. package/dist/commands/validate/markRules/autoBin.js +27 -0
  23. package/dist/commands/validate/markRules/colorScale.js +244 -0
  24. package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
  25. package/dist/commands/validate/markRules/jexlStrings.js +23 -0
  26. package/dist/commands/validate/markRules/markProblems.js +573 -0
  27. package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
  28. package/dist/commands/validate/markRules/markSpecs.js +84 -0
  29. package/dist/commands/validate/markRules/markVocabulary.js +59 -0
  30. package/dist/commands/validate/markRules/shapeNameList.js +7 -0
  31. package/dist/commands/validate/markRules/stepChannels.js +55 -0
  32. package/dist/commands/validate/schemaValidate.js +425 -0
  33. package/dist/commands/validate/validateConfig.js +317 -258
  34. package/dist/index.js +5 -0
  35. package/dist/version.js +1 -1
  36. package/package.json +6 -10
  37. package/bin/run +0 -4
  38. package/bundle/index.js +0 -1569
  39. package/dist/commands/make-pif/cigar-utils.js +0 -67
@@ -0,0 +1,573 @@
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, 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, MAX_THRESHOLD_CUTS, } 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 colour naming more cuts than it paints, so a value past the eighth takes the ninth colour. */
31
+ 'threshold-cut-count': 'warning',
32
+ /** A threshold `range` not one colour longer than its cuts. */
33
+ 'threshold-range': 'warning',
34
+ /** A `domain` on a linear or log colour, whose ends are `domainMin` and `domainMax`. */
35
+ 'ramp-domain': 'warning',
36
+ /** A scale's `domainMax` below its `domainMin`: a colour ramp's, a width's or `scales.y`'s. */
37
+ 'domain-ends': 'warning',
38
+ /** A colour ramp's or `scales.y`'s `domainQuantile` outside 0.5 to 1, a percent among them. */
39
+ 'domain-quantile': 'warning',
40
+ /** A colour's or a shape's `field` spelling a CSS colour or a shape name, which is a constant written `{ value }`. */
41
+ 'field-spells-constant': 'warning',
42
+ /** A colour's or a shape's `labels` naming values its `domain` does not list, or no categorical scale's. */
43
+ 'labels-domain': '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
+ /** Another channel naming a field that no `aggregate` or `coverage` step before it writes, so it reads no value; the field a `facet` or `rows` splits on counts as written. */
63
+ 'unwritten-field': 'warning',
64
+ /** A mark other than a span drawn beside one that stacks rows, standing in the first of them. */
65
+ 'value-beside-rows': 'warning',
66
+ /** Two `pileup` steps packing one plot, whose rows share numbers. */
67
+ 'two-packings': 'warning',
68
+ /** A `pileup` in the display's `transform` under a `facet`, packing across every section. */
69
+ '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
+ /** `rows` beside a `facet` on another field, where the facet draws alone. */
73
+ 'rows-beside-facet': 'warning',
74
+ /** A `pileup` or a `row` field under `rows`, whose packed rows share their value's one row. */
75
+ 'packing-under-rows': 'warning',
76
+ };
77
+ function found(rule, slot, message) {
78
+ return { rule, level: MARK_RULES[rule], slot, message };
79
+ }
80
+ function markTypeOf(mark) {
81
+ return mark.mark ?? DEFAULT_MARK_TYPE;
82
+ }
83
+ // Whether a slot holds nothing a reader could act on: unset, empty text, an
84
+ // empty list, or an object none of whose members holds anything.
85
+ function unwritten(value) {
86
+ return (value === undefined ||
87
+ value === '' ||
88
+ (Array.isArray(value) && value.length === 0) ||
89
+ (typeof value === 'object' &&
90
+ value !== null &&
91
+ Object.values(value).every(unwritten)));
92
+ }
93
+ // Whether a channel is written as its default, which a display's snapshot
94
+ // leaves off and a file may spell out: the two read alike, so a slot at its
95
+ // default is no channel for a rule to find unread.
96
+ function atDefault(channel, value) {
97
+ switch (channel) {
98
+ case 'x':
99
+ return value === DEFAULT_BIN_AS[0];
100
+ case 'x2':
101
+ return (value === DEFAULT_X2 ||
102
+ (typeof value === 'object' &&
103
+ value !== null &&
104
+ unwritten(value.chrom) &&
105
+ (value.pos === DEFAULT_X2 ||
106
+ unwritten(value.pos))));
107
+ case 'text':
108
+ return value === DEFAULT_TEXT_FIELD;
109
+ default:
110
+ return false;
111
+ }
112
+ }
113
+ /** The channels a mark's encoding writes, less those at their default. */
114
+ function writtenChannels(encoding) {
115
+ return Object.entries(encoding ?? {})
116
+ .filter(([channel, value]) => !unwritten(value) && !atDefault(channel, value))
117
+ .map(([channel]) => channel);
118
+ }
119
+ function stepsOf(mark) {
120
+ return mark.transform ?? [];
121
+ }
122
+ function colorScaleOf({ scale, field = '' }) {
123
+ return paintedScale({ scale, field }, fieldScaleOf(FEATURE_FIELD_PRESETS, field));
124
+ }
125
+ function sizeFieldOf(size) {
126
+ return typeof size === 'string'
127
+ ? size
128
+ : typeof size === 'object'
129
+ ? (size.field ?? '')
130
+ : '';
131
+ }
132
+ // A shape's labels name its domain values in order, as a colour's do, and a
133
+ // constant shape names none.
134
+ // What a reader writes as a colour, without the colour parser the rule list
135
+ // cannot carry into `jbrowse validate`.
136
+ function spellsColor(text) {
137
+ return (/^(#[\da-f]{3,8}|(rgb|hsl|hwb|lab|lch|oklab|oklch|color)a?\(.*\)|transparent)$/i.test(text) || isNamedColor(text.toLowerCase()));
138
+ }
139
+ function fieldOf(channel) {
140
+ const field = typeof channel === 'object' && channel !== null
141
+ ? channel.field
142
+ : channel;
143
+ return typeof field === 'string' && !isJexl(field) ? field : '';
144
+ }
145
+ // Inside `encoding` a bare string is a field, so a constant written the way a
146
+ // display-level colour takes one reads as a field no feature holds.
147
+ function constantAsFieldProblems(encoding) {
148
+ const color = fieldOf(encoding?.color);
149
+ const shape = fieldOf(encoding?.shape);
150
+ return [
151
+ ...(color && spellsColor(color)
152
+ ? [
153
+ 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)} }`),
154
+ ]
155
+ : []),
156
+ ...(shape && SHAPE_NAMES.includes(shape)
157
+ ? [
158
+ 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)} }`),
159
+ ]
160
+ : []),
161
+ ];
162
+ }
163
+ function shapeLabelProblems(shape) {
164
+ if (typeof shape !== 'object' || shape === null) {
165
+ return [];
166
+ }
167
+ const { field = '', scale, domain = [], labels = [] } = shape;
168
+ // an unwritten domain pairs the labels with the field's own order, as the
169
+ // key and the worker do
170
+ const order = domain.length > 0 ? domain : (universalPresetOf(field)?.domain ?? []);
171
+ const named = paintedScale({ scale, field }, 'categorical') === 'categorical'
172
+ ? order.length
173
+ : 0;
174
+ return labels.length > named
175
+ ? [
176
+ 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`),
177
+ ]
178
+ : [];
179
+ }
180
+ function drawsDensity(mark) {
181
+ return mark.source === 'density' && readsValue(markTypeOf(mark));
182
+ }
183
+ function drawTogether(a, b) {
184
+ const upper = (m) => m.maxBpPerPx || Infinity;
185
+ const lower = (m) => m.minBpPerPx ?? 0;
186
+ return lower(a) < upper(b) && lower(b) < upper(a);
187
+ }
188
+ // The field names a step reads, by the slot each is written in.
189
+ function fieldRefs(step) {
190
+ const list = (slot, refs = []) => refs.map((ref, k) => [`${slot}.${k}`, ref]);
191
+ switch (step.type) {
192
+ case 'bin':
193
+ return [['field', step.field], ...list('fields', step.fields)];
194
+ case 'flatten':
195
+ case 'cells':
196
+ return [['field', step.field]];
197
+ case 'pileup':
198
+ return list('fields', step.fields);
199
+ case 'aggregate':
200
+ return [
201
+ ...list('groupby', step.groupby),
202
+ ...(step.ops ?? []).flatMap((o, k) => [
203
+ [`ops.${k}.field`, o.field],
204
+ [`ops.${k}.weight`, o.weight],
205
+ ]),
206
+ ];
207
+ default:
208
+ return [];
209
+ }
210
+ }
211
+ // The slots naming two fields, and what each reads as when it names another
212
+ // number of them: a bin's `fields` left empty is a bin by `field`.
213
+ function pairSlots(step) {
214
+ return step.type === 'bin'
215
+ ? [
216
+ { slot: 'as', names: step.as, reads: DEFAULT_BIN_AS.join(' and ') },
217
+ {
218
+ slot: 'fields',
219
+ names: step.fields?.length ? step.fields : undefined,
220
+ reads: `its field alone, ${step.field || DEFAULT_BIN_FIELD}`,
221
+ },
222
+ ]
223
+ : step.type === 'pileup'
224
+ ? [
225
+ {
226
+ slot: 'fields',
227
+ names: step.fields,
228
+ reads: DEFAULT_PILEUP_FIELDS.join(' and '),
229
+ },
230
+ ]
231
+ : [];
232
+ }
233
+ function readable(steps) {
234
+ return steps.filter(s => s !== undefined);
235
+ }
236
+ function packsIn(steps) {
237
+ return steps.some(s => s?.type === 'pileup');
238
+ }
239
+ function packs(mark) {
240
+ return packsIn(stepsOf(mark));
241
+ }
242
+ function named(field) {
243
+ return typeof field === 'string' && field !== '';
244
+ }
245
+ 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";
246
+ // Where a display drawing one row per value packs more than one.
247
+ function packingsUnderRows(marks, transform, section) {
248
+ const pileups = (steps, list) => steps.flatMap((step, i) => step?.type === 'pileup'
249
+ ? [found('packing-under-rows', `${list}.${i}`, ONE_ROW_PER_VALUE)]
250
+ : []);
251
+ return [
252
+ ...pileups(transform, 'transform'),
253
+ ...pileups(section, 'facet.transform'),
254
+ ...marks.flatMap((mark, i) => mark
255
+ ? [
256
+ ...pileups(stepsOf(mark), 'transform'),
257
+ ...(mark.encoding?.row
258
+ ? [found('packing-under-rows', 'encoding.row', ONE_ROW_PER_VALUE)]
259
+ : []),
260
+ ].map(p => ({ mark: i, ...p }))
261
+ : []),
262
+ ];
263
+ }
264
+ function banded(mark, shared) {
265
+ return (!!mark.encoding?.row ||
266
+ stepChannels([...shared, ...stepsOf(mark)]).row !== undefined);
267
+ }
268
+ // The fields the last step that makes its features from nothing leaves behind,
269
+ // with what the steps after it add, over the display's steps and then the
270
+ // mark's; undefined where no step does, and every field of the file is still
271
+ // there.
272
+ function madeFields(steps) {
273
+ const made = steps.findLastIndex(s => s.type === 'aggregate' || s.type === 'coverage');
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;
332
+ }
333
+ // What a list of steps cannot run as written, a mark's, the facet's or the
334
+ // display's, each slot under the list's own name.
335
+ function stepProblems(steps, list = 'transform') {
336
+ const problems = [];
337
+ for (const [i, step] of steps.entries()) {
338
+ if (!step) {
339
+ continue;
340
+ }
341
+ const { type } = step;
342
+ const at = `${list}.${i}`;
343
+ if ((type === 'filter' || type === 'formula') && !isJexl(step.expr ?? '')) {
344
+ problems.push(found('step-expression', `${at}.expr`, `a ${type} reads a jexl: expression`));
345
+ }
346
+ if (type === 'bin' &&
347
+ step.step !== undefined &&
348
+ step.step !== AUTO_BIN &&
349
+ !(Number(step.step) > 0)) {
350
+ problems.push(found('bin-width', `${at}.step`, 'a bin is a positive width in bp'));
351
+ }
352
+ for (const pair of pairSlots(step)) {
353
+ if (pair.names && pair.names.length !== 2) {
354
+ 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}`));
355
+ }
356
+ }
357
+ if (type === 'bin' &&
358
+ step.fields?.length === 2 &&
359
+ step.field &&
360
+ step.field !== DEFAULT_BIN_FIELD) {
361
+ 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
+ }
363
+ if (type === 'aggregate') {
364
+ for (const [k, { op = DEFAULT_AGGREGATE_OP, field, weight }] of (step.ops ?? []).entries()) {
365
+ if (op !== 'count' && !field) {
366
+ problems.push(found('op-field', `${at}.ops.${k}.field`, `${op} reads a field and names none`));
367
+ }
368
+ if ((op === 'min' || op === 'max') && weight) {
369
+ problems.push(found('unread-weight', `${at}.ops.${k}.weight`, `a weight moves no ${op === 'min' ? 'minimum' : 'maximum'}, so the ${op} reads its field alone`));
370
+ }
371
+ }
372
+ }
373
+ for (const [slot, ref] of fieldRefs(step)) {
374
+ if (ref && isJexl(ref)) {
375
+ 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`));
376
+ }
377
+ }
378
+ }
379
+ return problems;
380
+ }
381
+ // Each field a mark's channels read besides `y`, by the slot naming it: the
382
+ // channels its type reads, a colour's or a shape's field where it is a scale,
383
+ // a text's field as its default leaves it, and a link's size field.
384
+ function channelFields(mark, spec) {
385
+ const encoding = mark.encoding ?? {};
386
+ const { x, x2, row, color = {}, shape, text = DEFAULT_TEXT_FIELD } = encoding;
387
+ const reads = (channel) => spec.channels.includes(channel);
388
+ const named = [
389
+ ['encoding.x', x],
390
+ ['encoding.x2', typeof x2 === 'string' ? x2 : x2?.pos],
391
+ ['encoding.x2.chrom', typeof x2 === 'object' ? x2.chrom : undefined],
392
+ ['encoding.row', reads('row') ? row : undefined],
393
+ [
394
+ 'encoding.color.field',
395
+ reads('color') && colorScaleOf(color) !== 'none'
396
+ ? fieldOf(color)
397
+ : undefined,
398
+ ],
399
+ ['encoding.shape.field', reads('shape') ? fieldOf(shape) : undefined],
400
+ ['encoding.text', reads('text') ? text : undefined],
401
+ [
402
+ 'encoding.size.field',
403
+ spec.size === 'channel' ? sizeFieldOf(encoding.size) : undefined,
404
+ ],
405
+ ];
406
+ return named.flatMap(([slot, field]) => field && !isJexl(field) ? [[slot, field]] : []);
407
+ }
408
+ function ownProblems(mark, display, section, split) {
409
+ const type = markTypeOf(mark);
410
+ const problems = [];
411
+ const y = mark.encoding?.y;
412
+ if (readsValue(type) &&
413
+ !y &&
414
+ !stepChannels([...display, ...section, ...stepsOf(mark)]).y) {
415
+ 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`));
416
+ }
417
+ const spec = MARK_SPECS[type];
418
+ const channels = new Set(['x', 'x2', 'size', ...spec.channels]);
419
+ for (const channel of writtenChannels(mark.encoding)) {
420
+ if (!channels.has(channel)) {
421
+ problems.push(found('unread-channel', `encoding.${channel}`, `a ${type} does not read ${channel}`));
422
+ }
423
+ }
424
+ const size = mark.encoding?.size;
425
+ if (!unwritten(size) && spec.size === undefined) {
426
+ problems.push(found('unread-size', 'encoding.size', `a ${type} draws no point or rule and strokes no link, so it reads no size`));
427
+ }
428
+ else if (spec.size === 'constant' && sizeFieldOf(size)) {
429
+ 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`));
430
+ }
431
+ if (mark.linkShape !== undefined &&
432
+ mark.linkShape !== DEFAULT_LINK_SHAPE &&
433
+ type !== 'link') {
434
+ problems.push(found('unread-link-shape', 'linkShape', `a ${type} draws no link, so it reads no linkShape`));
435
+ }
436
+ if (mark.interpolate !== undefined &&
437
+ mark.interpolate !== DEFAULT_LINE_INTERPOLATE &&
438
+ type !== 'line') {
439
+ problems.push(found('unread-interpolate', 'interpolate', `a ${type} draws no line, so it reads no interpolate`));
440
+ }
441
+ if (mark.rowProportion !== undefined &&
442
+ mark.rowProportion !== DEFAULT_ROW_PROPORTION &&
443
+ type !== 'span') {
444
+ problems.push(found('unread-row-proportion', 'rowProportion', `a ${type} draws no span, so it reads no rowProportion`));
445
+ }
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
+ const color = mark.encoding?.color ?? {};
450
+ for (const { rule, slot, message } of colorProblems(color, FEATURE_FIELD_PRESETS)) {
451
+ problems.push(found(rule, `encoding.color.${slot}`, message));
452
+ }
453
+ if (colorScaleOf(color) === 'threshold' &&
454
+ (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 colour'));
456
+ }
457
+ const cutCount = (color.domain ?? []).filter(cut => Number.isFinite(Number(cut))).length;
458
+ 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 colour`));
460
+ }
461
+ problems.push(...shapeLabelProblems(mark.encoding?.shape));
462
+ problems.push(...constantAsFieldProblems(mark.encoding));
463
+ if (typeof size === 'object') {
464
+ for (const { rule, slot, message } of scaleEndProblems(size)) {
465
+ problems.push(found(rule, `encoding.size.${slot}`, message));
466
+ }
467
+ }
468
+ const { minBpPerPx = 0, maxBpPerPx = 0 } = mark;
469
+ if (minBpPerPx > 0 && maxBpPerPx > 0 && minBpPerPx >= maxBpPerPx) {
470
+ problems.push(found('empty-zoom-range', 'minBpPerPx', `never draws: minBpPerPx ${minBpPerPx} is not below maxBpPerPx ${maxBpPerPx}`));
471
+ }
472
+ problems.push(...stepProblems(stepsOf(mark)));
473
+ if (packs(mark) && (packsIn(section) || packsIn(display))) {
474
+ 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`));
475
+ }
476
+ const fields = madeFields([...display, ...section, ...stepsOf(mark)]);
477
+ if (fields) {
478
+ if (split) {
479
+ fields.add(split);
480
+ }
481
+ const leaves = `which no step before it writes; they leave ${[...fields].join(', ')}`;
482
+ if (y && !isJexl(y) && !fields.has(y)) {
483
+ problems.push(found('unwritten-y', 'encoding.y', `reads "${y}", ${leaves}`));
484
+ }
485
+ for (const [slot, field] of channelFields(mark, spec)) {
486
+ if (!fields.has(field)) {
487
+ problems.push(found('unwritten-field', slot, `reads "${field}", ${leaves}`));
488
+ }
489
+ }
490
+ }
491
+ return problems;
492
+ }
493
+ /**
494
+ * Every rule a plot breaks. The display's step lists are checked as a mark's
495
+ * are and reported with no mark, under `transform` and `facet.transform`, and
496
+ * so are `scales.y`'s ends. A file's own spelling goes through the schema's
497
+ * lift first, which `jbrowse validate` does from the generated manifest.
498
+ */
499
+ export function markProblems({ marks, facet, transform = [], rows, scales, }) {
500
+ const faceted = named(facet?.field);
501
+ const drawsRows = named(rows?.field) && !faceted;
502
+ // the field the sections or rows are split on, which every section's rows
503
+ // keep through the steps that make rows
504
+ const split = faceted
505
+ ? String(facet?.field)
506
+ : drawsRows
507
+ ? String(rows?.field)
508
+ : undefined;
509
+ const section = readable(facet?.transform ?? []);
510
+ const display = readable(transform);
511
+ const shared = [...display, ...section];
512
+ const problems = [
513
+ ...stepProblems(transform),
514
+ ...stepProblems(facet?.transform ?? [], 'facet.transform'),
515
+ ...scaleEndProblems(scales?.y ?? {}).map(({ rule, slot, message }) => found(rule, `scales.y.${slot}`, message)),
516
+ ...(faceted
517
+ ? transform.flatMap((step, i) => step?.type === 'pileup'
518
+ ? [
519
+ 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'),
520
+ ]
521
+ : [])
522
+ : []),
523
+ ...(faceted && named(rows?.field) && rows?.field !== facet?.field
524
+ ? [
525
+ 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'),
526
+ ]
527
+ : []),
528
+ ...(drawsRows ? packingsUnderRows(marks, transform, section) : []),
529
+ ...marks.flatMap((mark, i) => mark
530
+ ? ownProblems(mark, display, section, split).map(p => ({
531
+ mark: i,
532
+ ...p,
533
+ }))
534
+ : []),
535
+ ];
536
+ for (const [i, mark] of marks.entries()) {
537
+ for (const [j, other] of marks.entries()) {
538
+ if (i === j || !mark || !other || !drawTogether(mark, other)) {
539
+ continue;
540
+ }
541
+ if (!faceted &&
542
+ !drawsRows &&
543
+ markTypeOf(mark) !== 'span' &&
544
+ !banded(mark, shared) &&
545
+ banded(other, shared)) {
546
+ problems.push({
547
+ mark: i,
548
+ ...found('value-beside-rows', 'encoding.row', `stands in the first of the rows mark ${j} bands the plot into, its axis repeated per row`),
549
+ });
550
+ }
551
+ if (i > j && packs(mark) && packs(other)) {
552
+ problems.push({
553
+ mark: i,
554
+ ...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
+ });
556
+ }
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
+ }
564
+ }
565
+ return problems;
566
+ }
567
+ /**
568
+ * A problem as one line of a notice: `mark 0 encoding.y: …`, or
569
+ * `transform.0.step: …` for the display's own step.
570
+ */
571
+ export function problemText({ mark, slot, message }) {
572
+ return `${mark === undefined ? '' : `mark ${mark} `}${slot}: ${message}`;
573
+ }
@@ -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,84 @@
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
+ hit: 'rows',
16
+ },
17
+ point: {
18
+ channels: ['y', 'row', 'color', 'shape'],
19
+ value: 'required',
20
+ hit: 'index',
21
+ size: 'constant',
22
+ },
23
+ rule: {
24
+ channels: ['y', 'row', 'color'],
25
+ value: 'required',
26
+ hit: 'rows',
27
+ size: 'constant',
28
+ },
29
+ line: {
30
+ channels: ['y', 'row', 'color'],
31
+ value: 'required',
32
+ hit: 'rows',
33
+ size: 'constant',
34
+ },
35
+ span: {
36
+ channels: ['row', 'color'],
37
+ value: 'none',
38
+ hit: 'rows',
39
+ },
40
+ text: {
41
+ channels: ['y', 'row', 'color', 'text'],
42
+ value: 'optional',
43
+ hit: false,
44
+ },
45
+ link: {
46
+ channels: ['y', 'row', 'color', 'size'],
47
+ value: 'optional',
48
+ hit: 'index',
49
+ farFoot: true,
50
+ size: 'channel',
51
+ },
52
+ };
53
+ function specOf(type) {
54
+ return MARK_SPECS[type];
55
+ }
56
+ /** Whether a mark type stands at a `y` value, so a mark of it naming none draws nothing. */
57
+ export function readsValue(type) {
58
+ return specOf(type).value === 'required';
59
+ }
60
+ /** Whether a mark type may plot a `y`, and so folds its values into the axis. */
61
+ export function plotsValue(type) {
62
+ return specOf(type).value !== 'none';
63
+ }
64
+ /** Whether a mark type answers a hover through the hit index the worker builds. */
65
+ export function hitsByIndex(type) {
66
+ return specOf(type).hit === 'index';
67
+ }
68
+ /**
69
+ * The lanes a mark asks the worker to fill: its type's channels, the colour
70
+ * lanes `colorLanesOf` chose for its declaration, and the hit index where the
71
+ * mark answers a hover through one.
72
+ */
73
+ export function markLanes(type, colorLanes) {
74
+ const spec = specOf(type);
75
+ return [
76
+ ...spec.channels.flatMap((channel) => channel === 'shape'
77
+ ? ['glyph']
78
+ : channel === 'color'
79
+ ? colorLanes
80
+ : [channel]),
81
+ ...(spec.farFoot ? ['x2Ref'] : []),
82
+ ...(hitsByIndex(type) ? ['index'] : []),
83
+ ];
84
+ }