@jbrowse/cli 5.0.0-beta.11 → 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.
@@ -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 is ignored rather than reported, so a
6
- misspelled slot leaves the track loading normally with the setting doing
7
- nothing. That is what this command is mainly for.
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
 
@@ -24,7 +25,7 @@ what it cannot draw in a corner notice rather than refusing the track.
24
25
  This command runs the display's own rule list over every "marks" list in the
25
26
  file: an error where a mark draws nothing, never draws or a step cannot run,
26
27
  and a warning where a slot waits unread or the marks draw in an arrangement
27
- the author may not have meant. With --json each finding carries the stable
28
+ the author may not have meant. With --json each finding has the stable
28
29
  "rule" id of the rule that made it.
29
30
 
30
31
  Types registered by plugins are not known to this command, so they come through
@@ -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 colour object paints through, in a module that imports
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 colour object maps its `field`; `none` paints `value`. */
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 colour it carries, and
17
- * the key names the colours `domain` lists. Only a display whose features
18
- * carry colours of their own declares it.
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 colours of their own on
24
- * every display, beneath each display's own presets. Red forward and blue
25
- * reverse is what the synteny ribbons paint too. The SV classes are
23
+ * The fields whose values have names, an order and colors of their own on
24
+ * every display, beneath each display's own presets. The synteny ribbons
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 colour for it.
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 colour object with its field's preset under it: each member the preset
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 colour paints through the preset's own scale, so the cuts of
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, so they
107
- * fill only while `domain` does too: a written order keeps each value's own
108
- * colour and name.
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 ordered = preset.scale !== 'categorical' ||
118
- !preset.domain ||
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
- (ordered || (key !== 'range' && key !== 'labels')) &&
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
- * The scale a colour object paints through: `none` while no `field` is
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. `reverse` is whether the scale turns round by
138
- * a member of its own, which is then the spelling to point at.
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
@@ -166,11 +255,44 @@ export function scaleEndProblems({ domainMin, domainMax, domainQuantile }, { rev
166
255
  export function noticeLines(setting, problems) {
167
256
  return problems.map(({ slot, message }) => `${setting}.${slot}: ${message}`);
168
257
  }
258
+ function numberOf(value) {
259
+ return typeof value === 'number' ? value : undefined;
260
+ }
261
+ function stringOf(value) {
262
+ return typeof value === 'string' ? value : undefined;
263
+ }
264
+ function listOf(value) {
265
+ return Array.isArray(value) ? value : undefined;
266
+ }
267
+ /** A quantitative scale's ends among the members `member` reads, each a number or unset. */
268
+ export function scaleEndsOf(member) {
269
+ return {
270
+ domainMin: numberOf(member('domainMin')),
271
+ domainMax: numberOf(member('domainMax')),
272
+ domainQuantile: numberOf(member('domainQuantile')),
273
+ type: stringOf(member('type')),
274
+ };
275
+ }
276
+ /**
277
+ * The members `colorProblems` reads among the ones `member` reads, each of the
278
+ * type it reads or unset, so a config snapshot and a config node answer alike.
279
+ */
280
+ export function colorSlotsOf(member) {
281
+ return {
282
+ ...scaleEndsOf(member),
283
+ value: stringOf(member('value')),
284
+ field: stringOf(member('field')) ?? '',
285
+ scale: stringOf(member('scale')),
286
+ domain: listOf(member('domain')),
287
+ range: listOf(member('range')),
288
+ labels: listOf(member('labels')),
289
+ };
290
+ }
169
291
  function pinned(entry) {
170
292
  return entry !== '' && Number.isFinite(Number(entry));
171
293
  }
172
294
  /**
173
- * What a colour object's slots say together that no single slot can refuse,
295
+ * What a color object's slots say together that no single slot can refuse,
174
296
  * since the config editor writes one slot at a time (ADR-133), read with its
175
297
  * field's preset under it. A display shows these as a notice and draws what
176
298
  * it can. A threshold naming no cuts leaves its intervals to the display —
@@ -199,7 +321,7 @@ export function colorProblems(written, presets) {
199
321
  problems.push({
200
322
  rule: 'threshold-range',
201
323
  slot: 'range',
202
- message: `${domain.length} threshold ${domain.length === 1 ? 'cut makes' : 'cuts make'} ${domain.length + 1} intervals, one colour each, and range lists ${range.length}: a missing colour comes from the default palette and an extra one is never read`,
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`,
203
325
  });
204
326
  }
205
327
  }
@@ -208,10 +330,17 @@ export function colorProblems(written, presets) {
208
330
  problems.push({
209
331
  rule: 'ramp-domain',
210
332
  slot: 'domain',
211
- 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 colour has them",
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',
212
341
  });
213
342
  }
214
- problems.push(...scaleEndProblems(color, { reverse: true }));
343
+ problems.push(...scaleEndProblems({ ...color, type: scale }, { reverse: true }));
215
344
  }
216
345
  const { labels = [] } = color;
217
346
  const named = scale === 'categorical' || scale === IDENTITY_SCALE
@@ -226,13 +355,13 @@ export function colorProblems(written, presets) {
226
355
  const what = scale === 'categorical'
227
356
  ? 'value'
228
357
  : scale === IDENTITY_SCALE
229
- ? 'colour'
358
+ ? 'color'
230
359
  : 'interval';
231
360
  problems.push({
232
361
  rule: 'labels-domain',
233
362
  slot: 'labels',
234
363
  message: named === undefined
235
- ? "labels names a categorical scale's values, a threshold scale's intervals or an identity scale's colours, and this colour paints none of them"
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"
236
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`,
237
366
  });
238
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, aggregateFieldName, colorProblems, fieldScaleOf, isJexl, SHAPE_NAMES, isNamedColor, paintedScale, scaleEndProblems, universalPresetOf, } from './markRuleFacts.js';
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, 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';
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 rule naming no `y`, with no step before it writing one it reads by default. */
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 rule and strokes no link. */
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 colour naming no cut, so every value paints one colour. */
26
+ /** A threshold color naming no cut, so every value paints one color. */
29
27
  'threshold-no-cuts': 'warning',
30
- /** A threshold colour naming more cuts than it paints, so a value past the eighth takes the ninth colour. */
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 colour longer than its cuts. */
30
+ /** A threshold `range` not one color longer than its cuts. */
33
31
  'threshold-range': 'warning',
34
- /** A `domain` on a linear or log colour, whose ends are `domainMin` and `domainMax`. */
32
+ /** A `domain` on a linear or log color, whose ends are `domainMin` and `domainMax`. */
35
33
  'ramp-domain': 'warning',
36
- /** A scale's `domainMax` below its `domainMin`: a colour ramp's, a width's or `scales.y`'s. */
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 colour ramp's or `scales.y`'s `domainQuantile` outside 0.5 to 1, a percent among them. */
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 colour's or a shape's `field` spelling a CSS colour or a shape name, which is a constant written `{ value }`. */
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 colour's or a shape's `labels` naming values its `domain` does not list, or no categorical scale's. */
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 colour's do, and 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 colour, without the colour parser the rule list
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 colour takes one reads as a field no feature holds.
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 colour, and a string in encoding is a field: a constant colour is { "value": ${JSON.stringify(color)} }`),
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, 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.
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
- 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;
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 colour's or a shape's field where it is a scale,
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 rule and strokes no link, so it reads no size`));
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 colour'));
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 colour`));
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 './aggregateFieldName.js';
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
- export function hitsByIndex(type) {
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 colour
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
  }